From c850337f1e21d0650a8b8ed495e341646bd2b337 Mon Sep 17 00:00:00 2001 From: skidoodle Date: Thu, 17 Apr 2025 09:20:34 +0200 Subject: [PATCH] init 4chan --- Rakefile | 109 + admin-test.php | 4447 ++++ admin.php | 4388 ++++ auth-test.php | 422 + auth.php | 451 + block_resync_removed | 0 boardlist.txt | 1 + captcha-test.php | 1127 + captcha.php | 1075 + catalog-test.php | 553 + catalog.php | 553 + clippy.html | 77 + config/ads/adblock.txt | 1 + config/ads/nws.bottom.txt | 1 + config/ads/nws.middle.txt | 1 + config/ads/nws.top.txt | 1 + config/ads/ws.bottom.txt | 1 + config/ads/ws.middle.txt | 1 + config/ads/ws.top.txt | 1 + config/boards/3.config.ini | 14 + config/boards/a.config.ini | 45 + config/boards/aco.config.ini | 27 + config/boards/adv.config.ini | 22 + config/boards/an.config.ini | 14 + config/boards/asp.config.ini | 17 + config/boards/b.config.ini | 101 + config/boards/bant.config.ini | 78 + config/boards/biz.config.ini | 26 + config/boards/c.config.ini | 17 + config/boards/cgl.config.ini | 14 + config/boards/ck.config.ini | 18 + config/boards/cm.config.ini | 20 + config/boards/co.config.ini | 34 + config/boards/d.config.ini | 23 + config/boards/diy.config.ini | 14 + config/boards/e.config.ini | 23 + config/boards/f.config.ini | 50 + config/boards/fa.config.ini | 18 + config/boards/fit.config.ini | 18 + config/boards/g.config.ini | 29 + config/boards/gd.config.ini | 30 + config/boards/gif.config.ini | 47 + config/boards/h.config.ini | 23 + config/boards/hc.config.ini | 32 + config/boards/his.config.ini | 31 + config/boards/hm.config.ini | 28 + config/boards/hr.config.ini | 34 + config/boards/i.config.ini | 33 + config/boards/ic.config.ini | 17 + config/boards/int.config.ini | 29 + config/boards/j.config.ini | 156 + config/boards/jp.config.ini | 49 + config/boards/k.config.ini | 21 + config/boards/lgbt.config.ini | 25 + config/boards/lit.config.ini | 26 + config/boards/m.config.ini | 19 + config/boards/mlp.config.ini | 40 + config/boards/mu.config.ini | 16 + config/boards/n.config.ini | 14 + config/boards/news.config.ini | 43 + config/boards/o.config.ini | 14 + config/boards/out.config.ini | 25 + config/boards/p.config.ini | 26 + config/boards/po.config.ini | 26 + config/boards/pol.config.ini | 121 + config/boards/pw.config.ini | 17 + config/boards/qa.config.ini | 28 + config/boards/qb.config.ini | 16 + config/boards/qst.config.ini | 79 + config/boards/r.config.ini | 26 + config/boards/r9k.config.ini | 42 + config/boards/s.config.ini | 34 + config/boards/s4s.config.ini | 79 + config/boards/sci.config.ini | 33 + config/boards/soc.config.ini | 51 + config/boards/sp.config.ini | 40 + config/boards/t.config.ini | 21 + config/boards/test.config.ini | 217 + config/boards/tg.config.ini | 46 + config/boards/toy.config.ini | 18 + config/boards/trash.config.ini | 42 + config/boards/trv.config.ini | 27 + config/boards/tv.config.ini | 35 + config/boards/u.config.ini | 27 + config/boards/v.config.ini | 59 + config/boards/vg.config.ini | 64 + config/boards/vip.config.ini | 41 + config/boards/vm.config.ini | 39 + config/boards/vmg.config.ini | 39 + config/boards/vp.config.ini | 26 + config/boards/vr.config.ini | 32 + config/boards/vrpg.config.ini | 39 + config/boards/vst.config.ini | 39 + config/boards/vt.config.ini | 47 + config/boards/w.config.ini | 31 + config/boards/wg.config.ini | 31 + config/boards/wsg.config.ini | 44 + config/boards/wsr.config.ini | 23 + config/boards/x.config.ini | 14 + config/boards/xs.config.ini | 14 + config/boards/y.config.ini | 23 + config/captcha_config.ini | 0 config/categories/nws.config.ini | 98 + config/categories/ws.config.ini | 95 + config/cloudflare_config.ini | 0 config/config_db.php | 0 config/global_config.ini | 664 + config/global_strings.ini | 379 + css/0ch.css | 1725 ++ css/burichan.css | 320 + css/burichannew.css | 1682 ++ css/catalog_0ch.css | 1669 ++ css/catalog_burichan_new.css | 1765 ++ css/catalog_futaba_new.css | 1775 ++ css/catalog_mobile.css | 396 + css/catalog_photon.css | 1785 ++ css/catalog_spooky.css | 1888 ++ css/catalog_spooky2017.css | 1894 ++ css/catalog_tomorrow.css | 1791 ++ css/catalog_yotsuba_b_new.css | 1816 ++ css/catalog_yotsuba_new.css | 1804 ++ css/flags.css | 268 + css/futaba.css | 320 + css/futabanew.css | 1676 ++ css/janichan.css | 1400 + css/md2016.css | 435 + css/photon.css | 1706 ++ css/spooky.css | 1821 ++ css/spooky2017.css | 1744 ++ css/tegaki-test.css | 666 + css/tegaki.css | 670 + css/tomorrow.css | 1734 ++ css/yotsuba.css | 457 + css/yotsubamobile.css | 1035 + css/yotsubanew.css | 1731 ++ css/yotsublue.css | 457 + css/yotsubluemobile.css | 1030 + css/yotsubluenew.css | 1733 ++ derefer.php | 48 + emotes_xa22.php | 665 + footer-test.txt | 14 + footer-ws.txt | 14 + footer.txt | 14 + forms/ban.php | 327 + forms/report-test.php | 205 + forms/report.php | 205 + globalmsg.txt | 0 header-sys.txt | 97 + header-test.txt | 100 + header-ws.txt | 106 + header.txt | 106 + imgboard-test.php | 10433 ++++++++ imgboard.php | 10402 ++++++++ imgtop/banned_dev.js | 398 + js/catalog-test.js | 3613 +++ js/catalog.js | 3613 +++ js/catalog.min.js | 2 + js/catalog.min.map | 1 + js/core-test.js | 2549 ++ js/core.js | 2567 ++ js/core.min.js | 2 + js/core.min.map | 1 + js/extension-test-sync2.js | 8967 +++++++ js/extension-test.js | 11227 ++++++++ js/extension.js | 11227 ++++++++ js/extension.min.js | 6 + js/extension.min.map | 1 + js/janitor-unminified.js | 1244 + js/janitor.js | 1 + js/minify.rb | 74 + js/mod-unminified.js | 2208 ++ js/mod.js | 2 + js/tcaptcha.js | 684 + js/tcaptcha.min.js | 1 + js/tegaki-test.js | 7665 ++++++ js/tegaki.min.js | 5 + json-test.php | 725 + json.php | 719 + latest.php | 8 + lib/GoogleAuthenticator.php | 201 + lib/admin-test.php | 612 + lib/admin.php | 603 + lib/ads-test.php | 155 + lib/ads.php | 145 + lib/archives.php | 171 + lib/auth-test.php | 594 + lib/auth.php | 594 + lib/board_flags_lgbt.php | 132 + lib/board_flags_mlp.php | 102 + lib/board_flags_pol.php | 72 + lib/board_flags_test.php | 26 + lib/captcha-test.php | 686 + lib/captcha.php | 686 + lib/db.php | 345 + lib/db_pdo.php | 308 + lib/geoip2-test.php | 115 + lib/geoip2.php | 115 + lib/global_constants.php | 6 + lib/htmlpurifier/HTMLPurifier.standalone.php | 21873 ++++++++++++++++ .../ConfigSchema/Builder/ConfigSchema.php | 48 + .../HTMLPurifier/ConfigSchema/Builder/Xml.php | 144 + .../HTMLPurifier/ConfigSchema/Exception.php | 11 + .../HTMLPurifier/ConfigSchema/Interchange.php | 47 + .../ConfigSchema/Interchange/Directive.php | 89 + .../ConfigSchema/Interchange/Id.php | 58 + .../ConfigSchema/InterchangeBuilder.php | 226 + .../HTMLPurifier/ConfigSchema/Validator.php | 248 + .../ConfigSchema/ValidatorAtom.php | 130 + .../HTMLPurifier/ConfigSchema/schema.ser | Bin 0 -> 15000 bytes .../schema/Attr.AllowedClasses.txt | 8 + .../schema/Attr.AllowedFrameTargets.txt | 12 + .../ConfigSchema/schema/Attr.AllowedRel.txt | 9 + .../ConfigSchema/schema/Attr.AllowedRev.txt | 9 + .../schema/Attr.ClassUseCDATA.txt | 19 + .../schema/Attr.DefaultImageAlt.txt | 11 + .../schema/Attr.DefaultInvalidImage.txt | 9 + .../schema/Attr.DefaultInvalidImageAlt.txt | 8 + .../schema/Attr.DefaultTextDir.txt | 10 + .../ConfigSchema/schema/Attr.EnableID.txt | 16 + .../schema/Attr.ForbiddenClasses.txt | 8 + .../ConfigSchema/schema/Attr.IDBlacklist.txt | 5 + .../schema/Attr.IDBlacklistRegexp.txt | 9 + .../ConfigSchema/schema/Attr.IDPrefix.txt | 12 + .../schema/Attr.IDPrefixLocal.txt | 14 + .../schema/AutoFormat.AutoParagraph.txt | 31 + .../ConfigSchema/schema/AutoFormat.Custom.txt | 12 + .../schema/AutoFormat.DisplayLinkURI.txt | 11 + .../schema/AutoFormat.Linkify.txt | 12 + .../AutoFormat.PurifierLinkify.DocURL.txt | 12 + .../schema/AutoFormat.PurifierLinkify.txt | 12 + ...rmat.RemoveEmpty.RemoveNbsp.Exceptions.txt | 11 + .../AutoFormat.RemoveEmpty.RemoveNbsp.txt | 15 + .../schema/AutoFormat.RemoveEmpty.txt | 46 + ...utoFormat.RemoveSpansWithoutAttributes.txt | 11 + .../schema/CSS.AllowImportant.txt | 8 + .../ConfigSchema/schema/CSS.AllowTricky.txt | 11 + .../ConfigSchema/schema/CSS.AllowedFonts.txt | 12 + .../schema/CSS.AllowedProperties.txt | 18 + .../ConfigSchema/schema/CSS.DefinitionRev.txt | 11 + .../schema/CSS.ForbiddenProperties.txt | 13 + .../ConfigSchema/schema/CSS.MaxImgLength.txt | 16 + .../ConfigSchema/schema/CSS.Proprietary.txt | 10 + .../ConfigSchema/schema/CSS.Trusted.txt | 9 + .../schema/Cache.DefinitionImpl.txt | 14 + .../schema/Cache.SerializerPath.txt | 13 + .../schema/Cache.SerializerPermissions.txt | 11 + .../schema/Core.AggressivelyFixLt.txt | 18 + .../schema/Core.AllowHostnameUnderscore.txt | 16 + .../schema/Core.CollectErrors.txt | 12 + .../schema/Core.ColorKeywords.txt | 29 + .../schema/Core.ConvertDocumentToFragment.txt | 14 + .../Core.DirectLexLineNumberSyncInterval.txt | 17 + .../schema/Core.DisableExcludes.txt | 14 + .../ConfigSchema/schema/Core.EnableIDNA.txt | 9 + .../ConfigSchema/schema/Core.Encoding.txt | 15 + .../schema/Core.EscapeInvalidChildren.txt | 12 + .../schema/Core.EscapeInvalidTags.txt | 7 + .../schema/Core.EscapeNonASCIICharacters.txt | 13 + .../schema/Core.HiddenElements.txt | 19 + .../ConfigSchema/schema/Core.Language.txt | 10 + .../ConfigSchema/schema/Core.LexerImpl.txt | 34 + .../schema/Core.MaintainLineNumbers.txt | 16 + .../schema/Core.NormalizeNewlines.txt | 11 + .../schema/Core.RemoveInvalidImg.txt | 12 + .../Core.RemoveProcessingInstructions.txt | 11 + .../schema/Core.RemoveScriptContents.txt | 12 + .../ConfigSchema/schema/Filter.Custom.txt | 11 + .../Filter.ExtractStyleBlocks.Escaping.txt | 14 + .../Filter.ExtractStyleBlocks.Scope.txt | 29 + .../Filter.ExtractStyleBlocks.TidyImpl.txt | 16 + .../schema/Filter.ExtractStyleBlocks.txt | 74 + .../ConfigSchema/schema/Filter.YouTube.txt | 16 + .../ConfigSchema/schema/HTML.Allowed.txt | 25 + .../schema/HTML.AllowedAttributes.txt | 19 + .../schema/HTML.AllowedComments.txt | 10 + .../schema/HTML.AllowedCommentsRegexp.txt | 15 + .../schema/HTML.AllowedElements.txt | 23 + .../schema/HTML.AllowedModules.txt | 20 + .../schema/HTML.Attr.Name.UseCDATA.txt | 11 + .../ConfigSchema/schema/HTML.BlockWrapper.txt | 18 + .../ConfigSchema/schema/HTML.CoreModules.txt | 23 + .../schema/HTML.CustomDoctype.txt | 9 + .../ConfigSchema/schema/HTML.DefinitionID.txt | 33 + .../schema/HTML.DefinitionRev.txt | 16 + .../ConfigSchema/schema/HTML.Doctype.txt | 11 + .../schema/HTML.FlashAllowFullScreen.txt | 11 + .../schema/HTML.ForbiddenAttributes.txt | 21 + .../schema/HTML.ForbiddenElements.txt | 20 + .../ConfigSchema/schema/HTML.MaxImgLength.txt | 14 + .../ConfigSchema/schema/HTML.Nofollow.txt | 7 + .../ConfigSchema/schema/HTML.Parent.txt | 12 + .../ConfigSchema/schema/HTML.Proprietary.txt | 12 + .../ConfigSchema/schema/HTML.SafeEmbed.txt | 13 + .../ConfigSchema/schema/HTML.SafeIframe.txt | 13 + .../ConfigSchema/schema/HTML.SafeObject.txt | 13 + .../schema/HTML.SafeScripting.txt | 10 + .../ConfigSchema/schema/HTML.Strict.txt | 9 + .../ConfigSchema/schema/HTML.TargetBlank.txt | 8 + .../ConfigSchema/schema/HTML.TidyAdd.txt | 8 + .../ConfigSchema/schema/HTML.TidyLevel.txt | 24 + .../ConfigSchema/schema/HTML.TidyRemove.txt | 8 + .../ConfigSchema/schema/HTML.Trusted.txt | 9 + .../ConfigSchema/schema/HTML.XHTML.txt | 11 + .../schema/Output.CommentScriptContents.txt | 10 + .../schema/Output.FixInnerHTML.txt | 15 + .../schema/Output.FlashCompat.txt | 11 + .../ConfigSchema/schema/Output.Newline.txt | 13 + .../ConfigSchema/schema/Output.SortAttr.txt | 14 + .../ConfigSchema/schema/Output.TidyFormat.txt | 25 + .../ConfigSchema/schema/Test.ForceNoIconv.txt | 7 + .../schema/URI.AllowedSchemes.txt | 17 + .../ConfigSchema/schema/URI.Base.txt | 17 + .../ConfigSchema/schema/URI.DefaultScheme.txt | 10 + .../ConfigSchema/schema/URI.DefinitionID.txt | 11 + .../ConfigSchema/schema/URI.DefinitionRev.txt | 11 + .../ConfigSchema/schema/URI.Disable.txt | 14 + .../schema/URI.DisableExternal.txt | 11 + .../schema/URI.DisableExternalResources.txt | 13 + .../schema/URI.DisableResources.txt | 15 + .../ConfigSchema/schema/URI.Host.txt | 19 + .../ConfigSchema/schema/URI.HostBlacklist.txt | 9 + .../ConfigSchema/schema/URI.MakeAbsolute.txt | 13 + .../ConfigSchema/schema/URI.Munge.txt | 83 + .../schema/URI.MungeResources.txt | 17 + .../schema/URI.MungeSecretKey.txt | 30 + .../schema/URI.OverrideAllowedSchemes.txt | 9 + .../schema/URI.SafeIframeRegexp.txt | 22 + .../HTMLPurifier/ConfigSchema/schema/info.ini | 3 + .../DefinitionCache/Serializer/gitignore | 0 .../HTMLPurifier/EntityLookup/entities.ser | 1 + .../Filter/ExtractStyleBlocks.php | 338 + .../HTMLPurifier/Filter/YouTube.php | 65 + .../Language/classes/en-x-test.php | 9 + .../Language/messages/en-x-test.php | 11 + .../Language/messages/en-x-testmini.php | 12 + .../HTMLPurifier/Language/messages/en.php | 55 + .../standalone/HTMLPurifier/Lexer/PH5P.php | 4788 ++++ .../standalone/HTMLPurifier/Printer.php | 218 + .../HTMLPurifier/Printer/CSSDefinition.php | 44 + .../HTMLPurifier/Printer/ConfigForm.css | 10 + .../HTMLPurifier/Printer/ConfigForm.js | 5 + .../HTMLPurifier/Printer/ConfigForm.php | 447 + .../HTMLPurifier/Printer/HTMLDefinition.php | 324 + lib/ini.php | 124 + lib/json.php | 14 + lib/like_score-test.php | 671 + lib/like_score.php | 671 + lib/oekaki-test.php | 160 + lib/oekaki.php | 160 + lib/perk_options-test.php | 230 + lib/perk_options.php | 230 + lib/phash.php | 201 + lib/postfilter-test.php | 3965 +++ lib/postfilter.php | 3915 +++ lib/rpc.php | 390 + lib/rss.php | 72 + lib/userpwd-test.php | 956 + lib/userpwd.php | 956 + lib/util.php | 516 + modes/report-test.php | 851 + modes/report.php | 665 + plugins/broomcloset.php | 161 + plugins/enhance_q.php | 42 + plugins/robot9000.php | 250 + plugins/yotsuba_plugins.php | 28 + rebuildd-test.php | 130 + rebuildd.php | 130 + rid.php | 25 + signin-test.php | 1027 + signin.php | 1041 + tasks/pass_mailer.php | 138 + tasks/signin_mailer.php | 125 + title_banners.txt | 339 + views/imgboard-test.php | 738 + views/imgboard.php | 739 + views/pass_auth.tpl.php | 67 + views/signin-test.tpl.php | 99 + views/signin.tpl.php | 99 + views/syncframe.html | 88 + views/upboard.php | 993 + wordfilters/asp.php | 103 + wordfilters/ck.php | 38 + wordfilters/global.php | 99 + wordfilters/int.php | 38 + wordfilters/test.php | 154 + wordfilters/v.php | 201 + wordfilters/vg.php | 99 + wordfilters/vp.php | 99 + xa24tb.php | 379 + yotsuba_config.php | 122 + 390 files changed, 195936 insertions(+) create mode 100644 Rakefile create mode 100644 admin-test.php create mode 100644 admin.php create mode 100644 auth-test.php create mode 100644 auth.php create mode 100644 block_resync_removed create mode 100644 boardlist.txt create mode 100644 captcha-test.php create mode 100644 captcha.php create mode 100644 catalog-test.php create mode 100644 catalog.php create mode 100644 clippy.html create mode 100644 config/ads/adblock.txt create mode 100644 config/ads/nws.bottom.txt create mode 100644 config/ads/nws.middle.txt create mode 100644 config/ads/nws.top.txt create mode 100644 config/ads/ws.bottom.txt create mode 100644 config/ads/ws.middle.txt create mode 100644 config/ads/ws.top.txt create mode 100644 config/boards/3.config.ini create mode 100644 config/boards/a.config.ini create mode 100644 config/boards/aco.config.ini create mode 100644 config/boards/adv.config.ini create mode 100644 config/boards/an.config.ini create mode 100644 config/boards/asp.config.ini create mode 100644 config/boards/b.config.ini create mode 100644 config/boards/bant.config.ini create mode 100644 config/boards/biz.config.ini create mode 100644 config/boards/c.config.ini create mode 100644 config/boards/cgl.config.ini create mode 100644 config/boards/ck.config.ini create mode 100644 config/boards/cm.config.ini create mode 100644 config/boards/co.config.ini create mode 100644 config/boards/d.config.ini create mode 100644 config/boards/diy.config.ini create mode 100644 config/boards/e.config.ini create mode 100644 config/boards/f.config.ini create mode 100644 config/boards/fa.config.ini create mode 100644 config/boards/fit.config.ini create mode 100644 config/boards/g.config.ini create mode 100644 config/boards/gd.config.ini create mode 100644 config/boards/gif.config.ini create mode 100644 config/boards/h.config.ini create mode 100644 config/boards/hc.config.ini create mode 100644 config/boards/his.config.ini create mode 100644 config/boards/hm.config.ini create mode 100644 config/boards/hr.config.ini create mode 100644 config/boards/i.config.ini create mode 100644 config/boards/ic.config.ini create mode 100644 config/boards/int.config.ini create mode 100644 config/boards/j.config.ini create mode 100644 config/boards/jp.config.ini create mode 100644 config/boards/k.config.ini create mode 100644 config/boards/lgbt.config.ini create mode 100644 config/boards/lit.config.ini create mode 100644 config/boards/m.config.ini create mode 100644 config/boards/mlp.config.ini create mode 100644 config/boards/mu.config.ini create mode 100644 config/boards/n.config.ini create mode 100644 config/boards/news.config.ini create mode 100644 config/boards/o.config.ini create mode 100644 config/boards/out.config.ini create mode 100644 config/boards/p.config.ini create mode 100644 config/boards/po.config.ini create mode 100644 config/boards/pol.config.ini create mode 100644 config/boards/pw.config.ini create mode 100644 config/boards/qa.config.ini create mode 100644 config/boards/qb.config.ini create mode 100644 config/boards/qst.config.ini create mode 100644 config/boards/r.config.ini create mode 100644 config/boards/r9k.config.ini create mode 100644 config/boards/s.config.ini create mode 100644 config/boards/s4s.config.ini create mode 100644 config/boards/sci.config.ini create mode 100644 config/boards/soc.config.ini create mode 100644 config/boards/sp.config.ini create mode 100644 config/boards/t.config.ini create mode 100644 config/boards/test.config.ini create mode 100644 config/boards/tg.config.ini create mode 100644 config/boards/toy.config.ini create mode 100644 config/boards/trash.config.ini create mode 100644 config/boards/trv.config.ini create mode 100644 config/boards/tv.config.ini create mode 100644 config/boards/u.config.ini create mode 100644 config/boards/v.config.ini create mode 100644 config/boards/vg.config.ini create mode 100644 config/boards/vip.config.ini create mode 100644 config/boards/vm.config.ini create mode 100644 config/boards/vmg.config.ini create mode 100644 config/boards/vp.config.ini create mode 100644 config/boards/vr.config.ini create mode 100644 config/boards/vrpg.config.ini create mode 100644 config/boards/vst.config.ini create mode 100644 config/boards/vt.config.ini create mode 100644 config/boards/w.config.ini create mode 100644 config/boards/wg.config.ini create mode 100644 config/boards/wsg.config.ini create mode 100644 config/boards/wsr.config.ini create mode 100644 config/boards/x.config.ini create mode 100644 config/boards/xs.config.ini create mode 100644 config/boards/y.config.ini create mode 100644 config/captcha_config.ini create mode 100644 config/categories/nws.config.ini create mode 100644 config/categories/ws.config.ini create mode 100644 config/cloudflare_config.ini create mode 100644 config/config_db.php create mode 100644 config/global_config.ini create mode 100644 config/global_strings.ini create mode 100644 css/0ch.css create mode 100644 css/burichan.css create mode 100644 css/burichannew.css create mode 100644 css/catalog_0ch.css create mode 100644 css/catalog_burichan_new.css create mode 100644 css/catalog_futaba_new.css create mode 100644 css/catalog_mobile.css create mode 100644 css/catalog_photon.css create mode 100644 css/catalog_spooky.css create mode 100644 css/catalog_spooky2017.css create mode 100644 css/catalog_tomorrow.css create mode 100644 css/catalog_yotsuba_b_new.css create mode 100644 css/catalog_yotsuba_new.css create mode 100644 css/flags.css create mode 100644 css/futaba.css create mode 100644 css/futabanew.css create mode 100644 css/janichan.css create mode 100644 css/md2016.css create mode 100644 css/photon.css create mode 100644 css/spooky.css create mode 100644 css/spooky2017.css create mode 100644 css/tegaki-test.css create mode 100644 css/tegaki.css create mode 100644 css/tomorrow.css create mode 100644 css/yotsuba.css create mode 100644 css/yotsubamobile.css create mode 100644 css/yotsubanew.css create mode 100644 css/yotsublue.css create mode 100644 css/yotsubluemobile.css create mode 100644 css/yotsubluenew.css create mode 100644 derefer.php create mode 100644 emotes_xa22.php create mode 100644 footer-test.txt create mode 100644 footer-ws.txt create mode 100644 footer.txt create mode 100644 forms/ban.php create mode 100644 forms/report-test.php create mode 100644 forms/report.php create mode 100644 globalmsg.txt create mode 100644 header-sys.txt create mode 100644 header-test.txt create mode 100644 header-ws.txt create mode 100644 header.txt create mode 100644 imgboard-test.php create mode 100644 imgboard.php create mode 100644 imgtop/banned_dev.js create mode 100644 js/catalog-test.js create mode 100644 js/catalog.js create mode 100644 js/catalog.min.js create mode 100644 js/catalog.min.map create mode 100644 js/core-test.js create mode 100644 js/core.js create mode 100644 js/core.min.js create mode 100644 js/core.min.map create mode 100644 js/extension-test-sync2.js create mode 100644 js/extension-test.js create mode 100644 js/extension.js create mode 100644 js/extension.min.js create mode 100644 js/extension.min.map create mode 100644 js/janitor-unminified.js create mode 100644 js/janitor.js create mode 100644 js/minify.rb create mode 100644 js/mod-unminified.js create mode 100644 js/mod.js create mode 100644 js/tcaptcha.js create mode 100644 js/tcaptcha.min.js create mode 100644 js/tegaki-test.js create mode 100644 js/tegaki.min.js create mode 100644 json-test.php create mode 100644 json.php create mode 100644 latest.php create mode 100644 lib/GoogleAuthenticator.php create mode 100644 lib/admin-test.php create mode 100644 lib/admin.php create mode 100644 lib/ads-test.php create mode 100644 lib/ads.php create mode 100644 lib/archives.php create mode 100644 lib/auth-test.php create mode 100644 lib/auth.php create mode 100644 lib/board_flags_lgbt.php create mode 100644 lib/board_flags_mlp.php create mode 100644 lib/board_flags_pol.php create mode 100644 lib/board_flags_test.php create mode 100644 lib/captcha-test.php create mode 100644 lib/captcha.php create mode 100644 lib/db.php create mode 100644 lib/db_pdo.php create mode 100644 lib/geoip2-test.php create mode 100644 lib/geoip2.php create mode 100644 lib/global_constants.php create mode 100644 lib/htmlpurifier/HTMLPurifier.standalone.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/Xml.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Exception.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Directive.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Id.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/InterchangeBuilder.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Validator.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/ValidatorAtom.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema.ser create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.Language.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Base.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Host.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/info.ini create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/gitignore create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/EntityLookup/entities.ser create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Filter/ExtractStyleBlocks.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Filter/YouTube.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Language/classes/en-x-test.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-test.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-testmini.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Lexer/PH5P.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Printer.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Printer/CSSDefinition.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.css create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.js create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.php create mode 100644 lib/htmlpurifier/standalone/HTMLPurifier/Printer/HTMLDefinition.php create mode 100644 lib/ini.php create mode 100644 lib/json.php create mode 100644 lib/like_score-test.php create mode 100644 lib/like_score.php create mode 100644 lib/oekaki-test.php create mode 100644 lib/oekaki.php create mode 100644 lib/perk_options-test.php create mode 100644 lib/perk_options.php create mode 100644 lib/phash.php create mode 100644 lib/postfilter-test.php create mode 100644 lib/postfilter.php create mode 100644 lib/rpc.php create mode 100644 lib/rss.php create mode 100644 lib/userpwd-test.php create mode 100644 lib/userpwd.php create mode 100644 lib/util.php create mode 100644 modes/report-test.php create mode 100644 modes/report.php create mode 100644 plugins/broomcloset.php create mode 100644 plugins/enhance_q.php create mode 100644 plugins/robot9000.php create mode 100644 plugins/yotsuba_plugins.php create mode 100644 rebuildd-test.php create mode 100644 rebuildd.php create mode 100644 rid.php create mode 100644 signin-test.php create mode 100644 signin.php create mode 100644 tasks/pass_mailer.php create mode 100644 tasks/signin_mailer.php create mode 100644 title_banners.txt create mode 100644 views/imgboard-test.php create mode 100644 views/imgboard.php create mode 100644 views/pass_auth.tpl.php create mode 100644 views/signin-test.tpl.php create mode 100644 views/signin.tpl.php create mode 100644 views/syncframe.html create mode 100644 views/upboard.php create mode 100644 wordfilters/asp.php create mode 100644 wordfilters/ck.php create mode 100644 wordfilters/global.php create mode 100644 wordfilters/int.php create mode 100644 wordfilters/test.php create mode 100644 wordfilters/v.php create mode 100644 wordfilters/vg.php create mode 100644 wordfilters/vp.php create mode 100644 xa24tb.php create mode 100644 yotsuba_config.php diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..088dc7b --- /dev/null +++ b/Rakefile @@ -0,0 +1,109 @@ +require 'rake/testtask' +require 'uglifier' +require 'openssl' +require 'json' +require 'open3' +require 'fileutils' + +Encoding.default_external = 'UTF-8' + +include Rake + +def minify_js(basename) + root = 'js' + + u = Uglifier.new( + :harmony => true + #screw_ie8: true, + #source_map: { + # source_filename: "#{basename}#{version}.js", + # output_filename: "#{basename}.min#{version}.js" + #} + ) + + js, sm = u.compile_with_map(File.read("#{root}/#{basename}.js")) + + #hash = OpenSSL::Digest::MD5.hexdigest(sm)[0,8] + #js << "\n//# sourceMappingURL=#{basename}.min.map?#{hash}" + + out = if (basename =~ /-unminified$/) + "#{root}/#{basename.sub(/-.+$/, '')}.js" + else + "#{root}/#{basename}.min.js" + end + + File.open(out, 'w') { |f| f.write js } + #File.open("#{root}/#{basename}.min.map", 'w') { |f| f.write sm } +end + +desc 'Minify JavaScript and generate source maps' +task :minify, [:js] do |t, args| + puts "Compiling #{args[:js]}.js" + minify_js(args[:js]) +end + +task :jshint, [:js] do |t, args| + root = 'js' + + basename = args[:js] + + if !basename + abort 'File not found.' + end + + file = "#{root}/#{basename}.js" + + if !File.exist?(file) + abort 'File not found.' + end + + opts = { + laxbreak: true, + esversion: 6, + boss: true, + expr: true, + sub: true, + browser: true, + devel: true, + strict: 'implied', + multistr: true, + scripturl: true, + unused: 'vars', + evil: true, + '-W079' => true # no-native-reassign + } + + opts[:globals] = Hash[[ + '$', '$L', 'Chart', 'Feedback', 'Tip', 'APP', 'Tegaki', 'MathJax', 'Main', 'UA', + 'Draggable', 'Config', 'Parser', 'ThreadUpdater', 'SettingsMenu', 'QR', 'FC', + 'grecaptcha', 'Recaptcha', 'ados_refresh', 'style_group', 'StickyNav', + 'PostMenu', 'StorageSync', 'OGVPlayer', 'TCaptcha' + ].collect { |v| [v, false] }] + + cfg_path = 'tmp_jshint.json' + + File.write(cfg_path, opts.to_json) + + puts "--> #{file}" + output, outerr, status = Open3.capture3('jshint', file, '--config', cfg_path) + puts output + + FileUtils.rm(cfg_path) +end + +namespace :concat do + desc 'Concatenate painter.js files' + task :painter do + puts 'Building painter.js' + + root = 'js' + out_file = "#{root}/painter.js" + js = [] + + ['tegaki.js', 'painter-strings.js'].each do |file| + js << File.binread("#{root}/#{file}") + end + + File.binwrite(out_file, js.join("\n")) + end +end diff --git a/admin-test.php b/admin-test.php new file mode 100644 index 0000000..10febc5 --- /dev/null +++ b/admin-test.php @@ -0,0 +1,4447 @@ +17", $high ); + + return mysql_result( $howmany, 0, 0 ) - $high; +} + +function append_ban( $board, $ip ) +{ + // run in background + $cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $board $ip >/dev/null 2>&1 &"; + print "User banned from /$board/"; +// print $cmd . "
"; //disabling this because it's ugly and leaks filepaths + exec( $cmd ); +} + +function https_self_url() +{ + return "/".BOARD_DIR."/admin"; +} + +// for lib/admin.php +function delete_uploaded_files() +{ + +} + +function make_post_json($row) +{ + $nrow = array(); + + foreach( $row as $key => $val ) { + if( ctype_digit( $val ) || is_int( $val ) ) { + $val = (int)$val; + } + + $nrow[ $key ] = $val; + } + + return json_encode( $nrow ); +} + +function get_board_list() { + //mysql_global_call("SET character_set_results = 'utf8'"); + + $query = "SELECT dir, name FROM boardlist ORDER BY dir ASC"; + + $res = mysql_global_call($query); + + if (!$res) { + return array(); + } + + $boards = array(); + + while ($dir = mysql_fetch_row($res)) { + $boards[$dir[0]] = $dir[1]; + } + + return $boards; +} + +function is_board_valid($board, $allow_hidden = false) { + if (!$allow_hidden && ($board === 'test' || $board === 'j')) { + return false; + } + + $query = "SELECT dir FROM boardlist WHERE dir = '%s' LIMIT 1"; + $res = mysql_global_call($query, $board); + + if (!$res) { + return false; + } + + if (mysql_num_rows($res) === 1) { + return true; + } + + return false; +} + +function admin_clear_reports($board, $post_id) { + $query = "UPDATE reports SET cleared = 1 WHERE board = '%s' AND no = %d"; + + mysql_global_call($query, $board, $post_id); + + $query = << $ban_len) { + $spent_len = $ban_len; + } + } + else { + $spent_len = $ban_len; + } + + $recent_duration += $spent_len; + ++$recent_ban_count; + } + } + + if ($recent_duration) { + $recent_duration = round($recent_duration / 86400.0); + } + + return array( + 'total' => $total_count, + 'recent_bans' => $recent_ban_count, + 'recent_warns' => $recent_warn_count, + 'recent_days' => $recent_duration, + 'recent_permas' => $recent_perma_count + ); +} + +// Counts recently made threads by IP +function admin_get_thread_history($ip) { + $long_ip = ip2long($ip); + + if (!$long_ip) { + return false; + } + + $sql = "SELECT COUNT(*) FROM user_actions WHERE action = 'new_thread' AND ip = $long_ip AND time >= DATE_SUB(NOW(), INTERVAL 60 MINUTE)"; + + $res = mysql_global_call($sql); + + if (!$res) { + return false; + } + + return (int)mysql_fetch_row($res)[0]; +} + +function admin_hash_4chan_pass($pass) { + $salt = file_get_contents(SALTFILE); + + if (!$salt || !$pass) { + return ''; + } + + return sha1($pass . $salt); +} + +function get_ban_history_html($ban_summary, $host = false) { + $ban_tip = array(); + + if ($ban_summary['recent_bans'] > 0) { + $ban_tip[] = $ban_summary['recent_bans'] . ' ban' . ($ban_summary['recent_bans'] > 1 ? 's' : ''); + } + + if ($ban_summary['recent_warns'] > 0) { + $ban_tip[] = $ban_summary['recent_warns'] . ' warning' . ($ban_summary['recent_warns'] > 1 ? 's' : ''); + } + + if ($ban_summary['recent_days'] > 0) { + $ban_tip[] = $ban_summary['recent_days'] . ' day' + . ($ban_summary['recent_days'] > 1 ? 's' : '') + . ' spent banned'; + } + + if ($ban_summary['recent_permas'] > 0) { + $ban_tip[] = $ban_summary['recent_permas'] . ' permaban' . ($ban_summary['recent_permas'] > 1 ? 's' : ''); + } + + $ban_tip = "Past 12 months history'; + + if ($host !== false) { + return "
$ban_tip
[ {$ban_summary['total']} ban" . + (($ban_summary['total'] > 1) ? 's' : '') . " for this IP ]"; + } + else { + return "
$ban_tip
[ {$ban_summary['total']} ban" . + (($ban_summary['total'] > 1) ? 's' : '') . " for this Pass ]"; + } +} + +function ban_post( $no, $globalban, $length, $reason, $is_threadban = 0 ) +{ + $query = mysql_board_call( "SELECT HIGH_PRIORITY * FROM `" . SQLLOG . "` WHERE no=" . intval( $no ) ); //FIXME use assoc + $row = mysql_fetch_assoc( $query ); + if( !$row ) return ""; + extract( $row, EXTR_OVERWRITE ); + + //list( $no, $sticky, $permasage, $closed, $now, $name, $email, $sub, $com, $host, $pwd, $filename, $ext, $w, $h, $tn_w, $tn_h, $tim, $time, $md5, $fsize, $root, $resto ) = $row; + $name = str_replace( ' !', ' #', $name ); + $name = preg_replace( '/<[^>]+>/', '', $name ); // remove all remaining html crap + + if( $host ) $reverse = gethostbyaddr( $host ); + $displayhost = ( $reverse && $reverse != $host ) ? "$reverse ($host)" : $host; + $xff = ''; + + //$xffresult = mysql_board_call("select host from xff where board='%s' and postno=%d", BOARD_DIR, $no); + //$xffresult = mysql_global_call( "SELECT xff from xff where board='%s' AND postno='%d'", BOARD_DIR, $no ); + //if( $xffrow = mysql_fetch_row( $xffresult ) ) { + // $xff = $xffrow[ 0 ]; + // $xff_reverse = gethostbyaddr($xffrow[0]); + // $xff = ($xff_reverse && $xff_reverse!=$xffrow[0])?"$xff_reverse ($xff)":$xff; + //} + $board = BOARD_DIR; + $zonly = 0; + + $bannedby = $_COOKIE[ '4chan_auser' ]; + $pass_id = $row[ '4pass_id' ]; + $post_json = make_post_json($row); + + $result = mysql_global_do( + "INSERT INTO " . SQLLOGBAN . " + (board,global,zonly,name,host,reverse,xff,reason,length,admin,md5,4pass_id,post_num,post_time,post_json,admin_ip) + VALUES + ( '%s', %d, %d, '%s', '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', %d, FROM_UNIXTIME(%d), '%s', '%s')", + $board, $globalban, $zonly, $name, $host, $reverse, $xff, $reason, $length, $bannedby, $md5, $pass_id, $no, $time, $post_json, $_SERVER['REMOTE_ADDR'] ); + + //('" . $board . "','" . $globalban . "','" . $zonly . "','" . mysql_escape_string( $name ) . "','" . $host . "','" . mysql_escape_string( $reverse ) . "','" . mysql_escape_string( $xff ) . "','" . mysql_escape_string( $reason ) . "','$length','" . mysql_escape_string( $bannedby ) . "','$md5','$pass_id',$no,FROM_UNIXTIME('$time'), '%s')", $post_json ) ) { + if( !$result ) { + echo S_SQLFAIL; + } + + /*if( $ext != '' ) { + $salt = file_get_contents( SALTFILE ); + $hash = sha1( BOARD_DIR . $no . $salt ); + @copy( THUMB_DIR . "{$tim}s.jpg", BANTHUMB_DIR . "{$hash}s.jpg" ); + }*/ + /* + $afsize = (int)( $fsize > 0 ); + validate_admin_cookies(); + if( $is_threadban ) mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,admin_ip) values('0',%d,%d,'%s','%s','%s','%s','%d','%s','%s','%s')", $no, $resto, SQLLOG, $name, $sub, $com, $afsize, $filename.$ext, $bannedby, $_SERVER['REMOTE_ADDR'] ); // FIXME do all this in one insert outside the write lock + */ + echo "$displayhost banned.
\n"; + + return $host; +} + +function cpban($no) { + $no = (int)$no; + + if (!$no) { + die('Invalid thread number.'); + } + + $op_reason = htmlspecialchars($_POST['op_reason']); + $rep_reason = htmlspecialchars($_POST['rep_reason']); + + if (!$op_reason || !$rep_reason) { + die('Ban reason cannot be empty.'); + } + + $op_days = (int)$_POST['op_days']; + $rep_days = (int)$_POST['rep_days']; + + if ($op_days < 0 || $rep_days < 0 || $op_days > 9999 || $rep_days > 9999) { + die('Invalid ban length.'); + } + + $op_ban_end = date('YmdHis', time() + $op_days * (24 * 60 * 60)); + $rep_ban_end = date('YmdHis', time() + $rep_days * (24 * 60 * 60)); + + $op_host = ban_post($no, 1, $op_ban_end, "$op_reason<>Thread Ban No.$no", 1); + + if (!$op_host) { + die("Thread $no doesn't exist."); + } + + $query = mysql_board_call("SELECT no, host FROM `" . SQLLOG . "` WHERE resto = $no AND host != '$op_host' GROUP BY host"); + + while ($row = mysql_fetch_assoc($query)) { + ban_post($row['no'], 1, $rep_ban_end, "$rep_reason<>Thread Ban No.$no", 1); + } + + delete_post($no, false, null, 'threadban'); + + echo 'Done.
'; +} + +function delete_post($no, $imgonly, $template_id = null, $tool = null) { + $url = "/".BOARD_DIR."/post"; + + $post = array( + 'mode' => 'usrdel', + 'onlyimgdel' => $imgonly ? 'on' : '', + $no => 'delete', + 'remote_addr' => $_SERVER['REMOTE_ADDR'] + ); + + if ($template_id) { + $post['template_id'] = $template_id; + } + + if ($tool) { + $post['tool'] = $tool; + } + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + // don't bother waiting to check for errors + + return true; +} + +function archive_thread($thread_id) { + $url = "/".BOARD_DIR."/post"; + + $post = array( + 'mode' => 'forcearchive', + 'id' => $thread_id + ); + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + // don't bother waiting to check for errors + + return true; +} + +function move_thread($thread_id, $board) { + $url = "/".BOARD_DIR."/post"; + + $post = array( + 'mode' => 'movethread', + 'id' => $thread_id, + 'board' => $board + ); + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + // don't bother waiting to check for errors + + return true; +} + +function rebuild_thread($no, &$error = '', $is_archived = false) { + $url = '/' . BOARD_DIR . '/post'; + + if (!$is_archived) { + $post = array( + 'mode' => 'rebuildadmin', + 'no' => $no + ); + } + else { + $post = array(); + $post['mode'] = 'rebuild_threads_by_id'; + $post['ids'] = array($no); + $post = http_build_query($post); + } + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + return true; +} + +function rebuild_all(&$error = '') { + $url = '/' . BOARD_DIR . '/post'; + + $post = array( + 'mode' => 'rebuildall' + ); + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + return true; +} + +function dir_contents( $dir ) +{ + $d = opendir( $dir ); + $a = array(); + if( !$d ) return $a; + + while( ( $f = readdir( $d ) ) !== false ) { + if( $f == "." || $f == ".." || $f == "" ) continue; + $a[ ] = $f; + } + + closedir( $d ); + + return $a; +} + +function clean() +{ + // Survive oversized boards. + set_time_limit(0); + ini_set("memory_limit", "-1"); + + $images = array(); + $respages = array(); + $indexpages = array(); + + if( PAGE_MAX > 0 ) { + print "Running cleanup...
Pruning orphaned posts...
"; + $result = mysql_board_call( "select no from `%s` where resto>0 and resto not in (select no from `%s` where resto=0)", SQLLOG, SQLLOG ); + $nos = mysql_column_array( $result ); + if( count( $nos ) ) { + mysql_board_call( "delete from `" . SQLLOG . "` where no in (%s)", implode( $nos, "," ) ); + foreach( $nos as $no ) { + print "$no pruned
"; + } + } + } + + //clearstatcache(); + + // get list of images that should exist + if (MOBILE_IMG_RESIZE) { + $cols = ',m_img'; // FIXME, only because not all boards have that column + } + $result = mysql_board_call( "select tim,filename,ext$cols from `" . SQLLOG . "` where ext != ''" ); + while( $row = mysql_fetch_array( $result ) ) { + if( $row[ 'ext' ] == '.swf' ) { + $images[ "{$row[ 'filename' ]}{$row[ 'ext' ]}" ] = 1; + } + else { + $images[ "{$row[ 'tim' ]}{$row[ 'ext' ]}" ] = 1; // picture + $images[ "{$row[ 'tim' ]}s.jpg" ] = 1; // thumb + + if (ENABLE_OEKAKI_REPLAYS) { + $images["{$row['tim']}.tgkr"] = 1; // oe animation + } + + if (MOBILE_IMG_RESIZE) { + $images["{$row['tim']}m.jpg"] = 1; // resized + } + } + } + + // get list of res pages that should exist + $result = mysql_board_call( "select no from `" . SQLLOG . "` where resto=0" ); + while( $row = mysql_fetch_array( $result ) ) { + if( USE_GZIP == 1 ) { + $respages[ "{$row[ 'no' ]}.html.gz" ] = 1; + + if (ENABLE_JSON) { + $respages[$row['no'] . '.json.gz'] = 1; + + if (JSON_TAIL_SIZE) { + $respages[$row['no'] . '-tail.json.gz'] = 1; + } + } + } + else { + $respages[ "{$row[ 'no' ]}.html" ] = 1; + + if (ENABLE_JSON) { + $respages[$row['no'] . '.json'] = 1; + + if (JSON_TAIL_SIZE) { + $respages[$row['no'] . '-tail.json'] = 1; + } + } + } + + if( JANITOR_BOARD ) $respages[ $row[ 'no' ] . '.html.php' ] = 1; + } + + print "Cleaning src dir...
"; + foreach( dir_contents( IMG_DIR ) as $filename ) { + if( $images[ $filename ] != 1 && !preg_match('/dmca_/', $filename) && $filename !== 'src') { + print "Deleted $filename
"; + //if (file_exists(IMG_DIR . "$filename")) { + unlink(IMG_DIR . "$filename") or print "Couldn't delete!
"; + //} + } + } + + print "Cleaning thumb dir...
"; + foreach( dir_contents( THUMB_DIR ) as $filename ) { + if( $images[ $filename ] != 1 && !preg_match('/dmca_/', $filename)) { + print "Deleted $filename
"; + //if (file_exists(THUMB_DIR . "$filename")) { + unlink(THUMB_DIR . "$filename") or print "Couldn't delete!
"; + //} + } + } + + print "Cleaning res dir...
"; + foreach( dir_contents( RES_DIR ) as $filename ) { + if( $respages[ $filename ] != 1 ) { + print "Deleted $filename
"; + unlink( RES_DIR . "$filename" ) or print "Couldn't delete!
"; + } + } + + print "Cleaning index pages...
"; + $result = mysql_board_call( "SELECT COUNT(*) from `" . SQLLOG . "` WHERE archived = 0 AND resto = 0" ); + $lastpage = PAGE_MAX + 1;//(mysql_result( $result, 0, 0 ) / DEF_PAGES) + 1; + if( USE_GZIP == 1 ) { + $indexpages[ SELF_PATH2_FILE . '.gz' ] = 1; + + if (USE_RSS) { + $indexpages[INDEX_DIR . 'index.rss.gz'] = 1; + } + if (ENABLE_CATALOG) { + $indexpages[INDEX_DIR . 'catalog.html.gz'] = 1; + } + if (ENABLE_JSON_CATALOG) { + $indexpages[INDEX_DIR . 'catalog.json.gz'] = 1; + } + if (ENABLE_JSON_THREADS) { + $indexpages[INDEX_DIR . 'threads.json.gz'] = 1; + $indexpages[INDEX_DIR . 'archive.json.gz'] = 1; + } + if (ENABLE_ARCHIVE) { + $indexpages[INDEX_DIR . 'archive.html.gz'] = 1; + } + } + + $indexpages[ SELF_PATH2_FILE ] = 1; + + if (USE_RSS) { + $indexpages[INDEX_DIR . 'index.rss'] = 1; + } + if (ENABLE_CATALOG) { + $indexpages[INDEX_DIR . 'catalog.html'] = 1; + } + if (ENABLE_JSON_CATALOG) { + $indexpages[INDEX_DIR . 'catalog.json'] = 1; + } + if (ENABLE_JSON_THREADS) { + $indexpages[INDEX_DIR . 'threads.json'] = 1; + $indexpages[INDEX_DIR . 'archive.json'] = 1; + } + if (ENABLE_ARCHIVE) { + $indexpages[INDEX_DIR . 'archive.html'] = 1; + } + + for( $page = 1; $page < $lastpage; $page++ ) { + if( USE_GZIP == 1 ) { + $indexpages[ INDEX_DIR . $page . PHP_EXT . '.gz' ] = 1; + if (ENABLE_JSON_INDEXES) { + $indexpages[INDEX_DIR . $page . '.json.gz'] = 1; + } + } + $indexpages[ INDEX_DIR . $page . PHP_EXT ] = 1; + if (ENABLE_JSON_INDEXES) { + $indexpages[INDEX_DIR . $page . '.json'] = 1; + } + } + + foreach( glob( INDEX_DIR . '*.{html,gz}', GLOB_BRACE ) as $filename ) { + $bfilename = basename( $filename ); + if( $indexpages[ $filename ] != 1 ) { + print "Deleted $bfilename
"; + unlink( $filename ) or print "Couldn't delete!
"; + } + } + + print "Cleaning tmp uploads...
"; + $phptmp = ini_get( "upload_tmp_dir" ); + exec( "find $phptmp/ -mtime +2h -name php*", $tmpfiles ); + exec( "find -E " . INDEX_DIR . " -regex '.*/(gz)?tmp.*$' -mtime +2h", $indextmp ); + exec( "find -E " . RES_DIR . " -regex '.*/(gz)?tmp.*$' -mtime +2h", $restmp ); + + $tmpfiles = array_merge( $tmpfiles, $indextmp ); + $tmpfiles = array_merge( $tmpfiles, $restmp ); + + foreach( $tmpfiles as $filename ) { + $safename = explode( '/' . BOARD_DIR . '/', $filename ); + $safename = end( $safename ); + + print "Deleted $safename
"; + unlink( $filename ) or print "Couldn't delete!
"; + } + /*print "Cleaning /var/tmp
"; + exec("find /var/tmp/ -mtime +2h -type f", $tmpfiles); + foreach($tmpfiles as $filename) { + print "Delete $filename
"; unlink($filename) or print "Couldn't delete!
"; + }*/ + print "Cleaning up side tables...
"; + + mysql_global_call( "DELETE FROM user_actions WHERE time < DATE_SUB(NOW(), INTERVAL 7 DAY)" ); + mysql_global_call( "DELETE FROM event_log WHERE created_on < DATE_SUB(NOW(), INTERVAL 7 DAY)" ); + mysql_global_call( "DELETE FROM xff WHERE tim < (unix_timestamp(DATE_SUB(NOW(), INTERVAL 7 DAY))*1000)" ); + mysql_board_call( "DELETE FROM f_md5 WHERE now < DATE_SUB(NOW(), INTERVAL 2 DAY)" ); + mysql_board_call("DELETE FROM r9k_posts WHERE created_on < DATE_SUB(NOW(), INTERVAL 2 YEAR)"); + + print "Cleanup complete!"; +} + +// Changes relative board urls to absolute //sys.4chan.org admin urls +function fix_board_nav($nav) { + return preg_replace('/href="\/([a-z0-9]+)\/"/', "href=\"//sys." . L::d(BOARD_DIR) . "/$1/admin\"", $nav); +} + +/* head */ +function head( &$dat, $is_logged_in = false ) +{ + global $admin, $access_allow, $access_deny; + + $allowed_modes = array('ban', 'delall', 'unban', 'opt', 'banreq', 'editop'); + + if( !is_user() || ( is_user() && ( $admin != "ban" ) && ( $admin != "delall" ) && ( $admin != "unban" ) && ( $admin != "opt" ) && ( $admin != 'banreq' ) && ( $admin != 'editop' ) ) ) { + $navinc = fix_board_nav(file_get_contents( NAV_TXT )) . '
'; + $navinc = str_replace( '[Settings] ', '', $navinc ); + } + + if (DEFAULT_BURICHAN) { + $style_cookie = 'ws_style'; + $ws = 'ws'; + } + else { + $style_cookie = 'nws_style'; + $ws = ''; + } + + $preferred_style = $_COOKIE[$style_cookie]; + + switch ($preferred_style) { + case 'Yotsuba New': + $style = 'yotsubanew'; + break; + case 'Yotsuba B New': + $style = 'yotsubluenew'; + break; + //case 'Futaba New': + // $style = 'futabanew'; + // break; + //case 'Burichan New': + // $style = 'burichannew'; + // break; + case 'Tomorrow': + $style = 'tomorrow'; + break; + case 'Photon': + $style = 'photon'; + break; + default: + $style = DEFAULT_BURICHAN ? 'yotsubluenew' : 'yotsubanew'; + break; + } + + if (!in_array($admin, $allowed_modes)) { + $admin = ''; + } + + if ($admin == 'ban') { + $page_title = 'Ban No.' . (int)$_GET['id'] . ' on /' . BOARD_DIR . '/'; + $no_header = true; + } + else if ($admin == 'banreq') { + $page_title = 'Ban request No.' . (int)$_GET['id'] . ' on /' . BOARD_DIR . '/'; + $no_header = true; + } + else { + $page_title = TITLE; + $no_header = isset($_GET['noheader']); + } + + $fb_js = <<'; + + document.body.insertBefore(el, document.body.firstElementChild); + }, + + hideMessage: function() { + var el = document.getElementById('feedback'); + + if (el) { + document.body.removeChild(el); + } + }, + + checkTemplate: function(id) { + var tpl; + + Feedback.hideMessage(); + + if (id < 0) { + return; + } + + tpl = window.templates[id]; + + if (tpl.no == '1') { + Feedback.showMessage('Only use this ban template for images depicting apparent child pornography. For links and non-pornographic images, please use the appropriate template(s).'); + } + else if (tpl.no == '123' || tpl.no == '126') { + Feedback.showMessage('Images depicting apparent child pornography should be banned using the "Child Pornography (Explicit Image)" template.'); + } + } +}; +JS; + +$tooltip_js = << document.documentElement.clientWidth) { + left = rect.left - el.offsetWidth + t.offsetWidth + 2; + el.className += '-left'; + } + + top = rect.top - el.offsetHeight - 5; + + style = el.style; + style.display = 'none'; + style.top = (top + window.pageYOffset) + 'px'; + style.left = left + window.pageXOffset + 'px'; + style.display = ''; + + Tip.node = el; + }, + + hide: function() { + if (Tip.node) { + document.body.removeChild(Tip.node); + Tip.node = null; + } + } +} + +Tip.init(); +JS; + + $dat .= ' + + + + + + + + +' . $page_title . ' + + +'; + + if (!$no_header) { + $dat .= ' +' . str_replace( "12pt", "10pt", $navinc ) . ' +
' . TITLE . '
+
'; + } +} + +/* Footer */ +function foot( &$dat ) +{ + $dat .= ' +
+' . S_FOOT . ' + +
wtf? +' . str_replace( "12pt", "10pt", $navinc2 ) . ' +'; +} + +function error( $mes, $dest = '' ) +{ + global $upfile_name; + if ($dest && file_exists($dest)) { + unlink($dest); + } + head( $dat ); + echo $dat; + echo "

+
$mes

[" . S_RELOAD . "]
"; + die( "" ); +} + +/* text plastic surgery */ +function sanitize_text( $str ) +{ + global $admin; + $str = trim( $str ); //blankspace removal + if( get_magic_quotes_gpc() ) { //magic quotes is deleted (?) + $str = stripslashes( $str ); + } + if( $admin != $adminpass ) { //admins can use tags + $str = htmlspecialchars( $str ); //remove html special chars + $str = str_replace( "&", "&", $str ); //remove ampersands + } + + return str_replace( ",", ",", $str ); //remove commas +} + +//check for table existance +function table_exist( $table ) +{ + $result = mysql_global_call( "show tables like '$table'" ); + if( !$result ) { + return 0; + } + $a = mysql_fetch_row( $result ); + + return $a; +} + +function is_local() +{ + if (!isset($_SERVER['REMOTE_ADDR'])) { + return true; + } + + // local rpc can do anything + $longip = ip2long( $_SERVER['REMOTE_ADDR'] ); + + if( + cidrtest( $longip, "10.0.0.0/24" ) || + cidrtest( $longip, "204.152.204.0/24" ) || + cidrtest( $longip, "127.0.0.0/24" ) + ) { + return true; + } + + return false; +} + +// FIXME hack +function valid( $action = 'moderator', $no = 0 ) +{ + return false; +} + +/*password validation */ +function adminvalid( $title = 'Manager Mode' ) +{ + global $user, $pass, $access_allow, $access_deny, $admin; + + $level = 0; + $levelarr = array( 'janitor' => 1, 'mod' => 2, 'manager' => 3, 'admin' => 4 ); + + ob_start(); + + // 1 = janitor, 2 = mod, 3 = manager, 4 = admin + + if( is_local() ) { + echo head( $dat ); + + return; + } + + $user = $_COOKIE[ '4chan_auser' ]; + $pass = $_COOKIE[ '4chan_apass' ]; + + $valid = auth_user(); + + if( $valid !== true ) { + error( 'You do not have permission to access this page.' ); + } + + //if( !$valid ) admin_login_fail(); + + + // Do we have permission for this board? + if( $valid && ( $title !== 'Ban Request' && !access_board( BOARD_DIR ) ) ) { + error( 'You do not have permission to access this board.' ); + + } + + if ($title !== 'Ban Request' && !has_level('mod')) { + die(); + } + + if ($title === 'Board Cleanup' && !has_level('manager') && !has_flag('developer')) { + error( 'You do not have permission to access this board.' ); + } + + if( $valid && has_level() && $_GET[ 'admin' ] == 'adminext' ) { + return true; + } + + + head( $dat, $valid ); + echo $dat; + + $SELF_PATH2_ABS = SELF_PATH2_ABS; + $S_RETURNS = S_RETURNS; + $SELF_PATH = SELF_PATH; + $S_LOGUPD = S_LOGUPD; + $S_LOGUPDALL = S_LOGUPDALL; + if (!isset($_GET['noheader']) && $title == 'Manager Mode') { + + if( $valid && has_level( 'mod' ) ) { + echo '
'; + echo '[' . S_RETURNS . '] [' . S_LOGUPD . '] [' . S_LOGUPDALL . ']'; + + if( $_GET[ 'admin' ] == 'cleanup' ) { + echo ' [Admin]'; + } + else if (has_level('manager') || has_flag('developer')) { + echo ' [Cleanup]'; + } + + if( $_GET[ 'admin' ] == 'ban' ) { + echo ' [Rules]'; + } + + echo ''; + } + elseif( $valid ) { + //echo ' [Rules] '; + } + + if( ( !isset( $_GET[ 'admin' ] ) || $_GET[ 'admin' ] == 'cleanup' ) && has_level( 'mod' ) ) { + echo ""; + } + } + + $no_header = isset($_GET['noheader']) || $_GET[ 'admin' ] == 'ban' || $_GET[ 'admin' ] == 'banreq'; + + if( $valid && !has_level( 'mod' ) ) { + if ($no_header) { + return; + } + echo "

+
You are logged in as a janitor.

[Return]
"; + die( '' ); + } + + //if( $valid && (has_level('manager') || has_flag('developer')) ) { + $GLOBALS[ 'b_sticky' ] = 1; + //} + + if( !$valid ) $title = 'Manager Mode'; + if( !$valid ) echo ''; + if( !$no_header ) echo '
' . $title . '
'; + // Mana login form + if( !$valid ) { + echo "
\n"; + + echo "
"; + echo "\n"; + echo ""; + // echo ""; + echo ""; + echo ""; + echo "
ID(s):
Username
Password
\n"; + //echo file_get_contents( NAV2_TXT ); + echo ''; + + + die(); + } +} + +// FIXME +function adminreportclear() { + if (!has_level('mod')) { + die('404'); + } + + $pid = (int)$_GET['pid']; + + $board = $_GET['board']; + + if (!$pid || !$board) { + die('404'); + } + + $query = "UPDATE reports SET cleared = 1, cleared_by = '%s' WHERE board = '%s' AND no = $pid"; + + $res = mysql_global_call($query, $_COOKIE['4chan_auser'], $board); + + if (!$res) { + die("DB error (2-1)"); + } + + $query = <<'; + if ($post['fsize'] > 0) { + echo ''; + } + + $tid = $post['resto'] ? $post['resto'] : $report['no']; + + echo "/{$report['board']}/{$report['no']} ({$report['total_weight']}) — "; + + echo '[CLEAR]'; + + echo "

{$post['com']}

"; + + echo "
"; + } +} + +/* Admin deletion */ +// This might not be used anymore +function admin_delete() +{ + if( !has_level('mod') ) return true; + + global $admin, $onlyimgdel, $res, $thread, $ip, $user, $pass; + + if( ( $admin != "ban" ) && ( $admin != "delall" ) && ( $admin != "unban" ) ) { + $navinc = ''; //file_get_contents( NAV_TXT ); + } + if( !isset( $_POST[ 'p' ] ) ) { + $p = 1; + } + else { + $p = $_POST[ 'p' ]; + } + $max_results = 30; + $from = ( ( $p * $max_results ) - $max_results ); + + $board = explode( "/", $_SERVER[ 'SCRIPT_NAME' ] ); + $board = $board[ 1 ]; + + $threadmode = $_REQUEST[ 'threadmode' ]; + if( !$threadmode ) { // threadmode uses table aliases, so don't bother locking + if( $delflag ) mysql_board_call( "LOCK TABLES `" . SQLLOG . "` WRITE" ); + } + + $delno = array(); + $delflag = false; + reset( $_POST ); + while( $item = each( $_POST ) ) { + if( $item[ 1 ] == 'delete' ) { + array_push( $delno, intval( $item[ 0 ] ) ); + $delflag = true; + } + } + if( $delflag ) { + $resultstr = "(" . implode( ",", $delno ) . ")"; + if( $threadmode ) mysql_board_call( "LOCK TABLES `" . SQLLOG . "` WRITE" ); // can finally lock it now + if( !$result = mysql_board_call( "select * from `" . SQLLOG . "` where no in %s or resto in %s", $resultstr, $resultstr ) ) { + echo S_SQLFAIL; + } //FIXME use assoc + $find = false; + while( $row = mysql_fetch_assoc( $result ) ) { + //list( $no, $sticky, $permasage, $closed, $now, $name, $email, $sub, $com, $host, $pwd, $filename, $ext, $w, $h, $tn_w, $tn_h, $tim, $time, $md5, $fsize, $root, $resto ) = $row; + extract( $row, EXTR_OVERWRITE ); + + if( $onlyimgdel == 'on' ) { + if( array_search( $no, $delno ) ) { //only a picture is deleted + if( $board == "f" ) { + $delfile = IMG_DIR . $filename . $ext; + } + else { + $delfile = IMG_DIR . $tim . $ext; //only a picture is deleted + } + unlink( $delfile ); //delete + unlink( THUMB_DIR . $tim . 's.jpg' ); //delete + } + } + else { + if( array_search( $no, $delno ) || array_search( $resto, $delno ) ) { //It is empty when deleting + $find = true; + $auser = $_COOKIE[ '4chan_auser' ]; + $apass = $_COOKIE[ '4chan_apass' ]; + if( !mysql_board_call( "delete from `" . SQLLOG . "` where no=" . $no . " or resto=" . $no ) ) { + echo S_SQLFAIL; + } // FIXME can't this be atomic? (one statement) + if( $board == "f" ) { + $delfile = IMG_DIR . $filename . $ext; + } + else { + $delfile = IMG_DIR . $tim . $ext; + } + unlink( $delfile ); //Delete + unlink( THUMB_DIR . $tim . 's.jpg' ); //Delete + if( $fsize > 0 ) $adfsize = 1; + $adname = str_replace( '
!', '#', $name ); + if( $onlyimgdel == "on" ) { + $imgonly = 1; + } + else { + $imgonly = 0; + } + validate_admin_cookies(); + mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,admin_ip) values('$imgonly','$no',$resto,'" . SQLLOG . "','$adname','$sub','$com','$adfsize','$filename$ext','$auser', '" . mysql_real_escape_string($_SERVER['REMOTE_ADDR']) . "')" ); // FIXME do all this in one insert outside the write lock + } + } + } + } + + if( $delflag ) mysql_board_call( "UNLOCK TABLES" ); + + // Deletion screen display + echo "
\n"; + echo "\n"; + echo "Go to ID(s):  "; + if( $threadmode ) { + echo ""; + } + else { + echo ""; + } + + echo "
"; + + echo "

"; + echo ""; + echo " [" . S_MDONLYPIC . "]"; + + echo ""; + if( $threadmode ) { + echo ""; + } + else { + echo ""; + } + echo ''; + echo "\n"; + + $resq = "`"; + if( $res ) { + $resq = ""; + $splitres = explode( ",", $res ); + foreach( $splitres as $line ) { + $resq .= " no='" . mysql_escape_string( $line ) . "' OR"; + } + $resq = rtrim( $resq, " OR" ); + $resq = "` WHERE" . $resq; + + } + elseif( $thread && $ip ) { + $max_results = 5000; + $thread = (int)$thread; + $resq = "` WHERE (no='$thread' OR resto='$thread') AND host='" . sprintf( "%s", long2ip( -( 4294967296 - $ip ) ) ) . "'"; + } + elseif( $ip ) { + $max_results = 5000; + $resq = "` WHERE host='" . sprintf( "%s", long2ip( -( 4294967296 - $ip ) ) ) . "'"; + } + elseif( $thread ) { + $max_results = 5000; + $thread = (int)$thread; + $resq = "` WHERE no='$thread' OR resto='$thread'"; + } + + if( $threadmode ) { + if( !$result = mysql_board_call( "(SELECT child.*,parent.root proot from `" . SQLLOG . "` child LEFT OUTER JOIN `" . SQLLOG . "` parent ON child.resto=parent.no) UNION (SELECT *,root proot from `" . SQLLOG . "` parent WHERE resto=0) ORDER BY proot DESC, no ASC LIMIT " . $from . ", " . $max_results ) ) { + echo S_SQLFAIL; + } + } + else { + if( !$result = mysql_board_call( "select * FROM `" . SQLLOG . $resq . " order BY no DESC LIMIT " . $from . ", " . $max_results ) ) { + echo S_SQLFAIL; + } + } + + $j = 0; + while( $row = mysql_fetch_assoc( $result ) ) { //FIXME use assoc + $j++; + $img_flag = false; + extract( $row, EXTR_OVERWRITE ); + // Format + //$now=preg_replace('@^(../..)/..@','$1',$now); + //$now=preg_replace('/\(.*\)/',' ',$now); + $fullname = str_replace( '!', ' #', $name ); + $name = strip_tags( $name ); + $fullname = strip_tags( $name ); //for capcode cleaning + + if( strpos( $sub, 'SPOILER<>' ) !== false ) $sub = substr( $sub, 9 ); + + $fullsub = $sub; + if( strlen( $name ) > 14 ) $name = substr( $name, 0, 15 ) . "..."; + if( strlen( $sub ) > 14 ) $sub = substr( $sub, 0, 15 ) . "..."; + //if( $email ) $name = "$name"; + $shortcom = html_entity_decode( preg_replace( "/<[^>]+>/", " ", $com ), ENT_QUOTES, "UTF-8" ); + if( strlen( $shortcom ) > 35 ) $shortcom = substr( $shortcom, 0, 36 ) . "..."; + // Link to the picture + if( $ext ) { + $img_flag = true; + if( !$filedeleted ) { + if( SQLLOG == "f" ) { + $filelink = $filename . $ext; + } + else { + $filelink = $tim . $ext; + } + $clip = "" . $filelink . ""; + } + else { + $clip = "" . $filelink . ""; + } + $size = $fsize / 1024; + $size = round( $size, 2 ) . " KB"; + $all = $all + $fsize; //total calculation + $md5 = substr( $md5, 0, 10 ); + } + else { + $clip = ""; + $size = 0; + $md5 = ""; + } + $bg = ( $j % 2 ) ? "d0d0f0" : "f6f6f6"; //BG color + if( $threadmode ) { + $bg = ( !$resto ) ? "d0f0d0" : "eeffee"; + } + + $displayhost = $host; + + $cboard = explode( "/", $_SERVER[ 'SCRIPT_NAME' ] ); + $cboard = $cboard[ 1 ]; + + $bantrue = 0; + if( !$banned = mysql_global_call( "SELECT host,board,global,zonly,DATE_FORMAT(length, 'Until %W, %M %D, %Y.') AS buntil FROM " . SQLLOGBAN . " WHERE host='" . $host . "' AND active=1" ) ) { + echo S_SQLFAIL; + } + $bannedrows = mysql_num_rows( $banned ); + if( $bannedrows > 0 ) { + $row = mysql_fetch_array( $banned ); + $buntil = $row[ 'buntil' ]; + if( $row[ 'board' ] == $cboard ) { + $bg = "f0d0d0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + $bantrue = 1; + } + if( $row[ 'global' ] == 1 ) { + $bg = "f0a0a0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + //$globally = " (Globally)"; + $bantrue = 1; + } + elseif( $row[ 'zonly' ] == 1 ) { + $bg = "a0f0a0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + $bantrue = 0; + //$globally = " (".$board.")"; + } + else { + //$globally = " (".$board.")"; + } + } + + echo ""; + if( $resto == 0 ) { + $spec = ""; + if( $sticky == 1 ) $spec = " color: #800080;"; + if( $permasage == 1 ) $spec = " text-decoration: underline;"; + if( $closed == 1 ) $spec = " color: #FF0000;"; + if( $sticky == 1 && $closed == 1 ) $spec = " color: #808080;"; + + echo ""; + } + else { + $parentline = ( $threadmode ? "└" : "" ); + echo ""; + } + echo ""; + echo ""; + echo "\n"; + echo "\n"; + echo ""; + // echo ""; + + + if( $size != 0 ) { + echo ""; + echo ""; + echo ""; + echo ""; + // echo ""; + echo ""; + } + elseif( $resto == 0 ) { + //echo ""; + //echo "'; + echo ''; + } + else { + echo ""; + } + + echo ""; + echo ""; + $span = 8; + } + else { + echo ""; + echo ""; + echo ""; + echo ""; + //echo ""; + echo ""; + $span = 5; + } + echo ""; + } else {*/ + // if (!$bantrue) { + echo "    "; + // } + //} + if( $resto == 0 ) { + echo "    "; + if( !$thread ) { + echo "    "; + } + } + echo ""; + } + + echo "
 No.TimeNameSubjectCommentHost 
$no$parentline$no$now$name$sub$shortcom$displayhost
 File 
 $clip ($size)Text-only thread"; + if( $bannedrows > 0 ) { + echo 'Text-only threadBan lengthText-only thread
Reply to thread 
 $resto    "; + /*if ($bantrue) { + echo "    

"; + echo ""; + echo " [" . S_MDONLYPIC . "]


"; + + //$all = (int)($all / 1024); + + //page stuff + $total_results = mysql_result( mysql_board_call( "SELECT COUNT(*) as Num FROM `" . SQLLOG . "`" ), 0 ); + $total_pages = ceil( $total_results / $max_results ); + + echo "
\n"; + echo "\n"; + + echo '
"; + if( $p < $total_pages ) { + $next = ( $p + 1 ); + echo "
\n"; + echo "\n"; + echo "
"; + } + else { + echo "Next"; + } + echo "
"; + echo "
"; + echo str_replace( "12pt", "10pt", $navinc ); + + die( "" ); +} + +// return the images/thumbnails for a single post +// $row is an assoc. array representation of the post +function image_files_for( $row ) +{ + $del_files = array(); + // we always need to delete the image + $del_files[ IMG_DIR . $row[ 'tim' ] . $row[ 'ext' ] ] = 1; + // and the thumbnail + $del_files[ THUMB_DIR . $row[ 'tim' ] . 's.jpg' ] = 1; + // and the oekaki replay + if (ENABLE_OEKAKI_REPLAYS) { + $del_files[IMG_DIR . $row['tim'] . '.tgkr'] = 1; + } + // and the resized mobile images + if (MOBILE_IMG_RESIZE && $row['m_img']) { + $images["{$row['tim']}m.jpg"] = 1; + } + + return $del_files; +} + +// delete all posts from an IP, maintaining the consistency of the files and db +function delallbyip($ip, $imgonly, $replies_only = false) +{ + $ip = mysql_real_escape_string( $ip ); + + if ($ip === '') { + error('Invalid IP'); + } + + if ($replies_only) { + $_rep_sql = ' AND resto > 0'; + } + else { + $_rep_sql = ''; + } + + mysql_board_call( "LOCK TABLES `" . SQLLOG . "` WRITE" ); + $query = mysql_board_call("SELECT * FROM `" . SQLLOG . "` WHERE archived = 0 AND host='$ip'" . $_rep_sql); + $del_files = array(); // keys = delete these files + $update_threads = array(); // keys = update these threads' HTML + $del_threads = array(); // keys = delete replies to these thread numbers from db + $del_all = array(); // keys = these are being deleted from the db (used to clean up reports etc.) + + while( $row = mysql_fetch_assoc( $query ) ) { + // we always need to delete the image files + $del_files += image_files_for( $row ); + + if( !$imgonly ) // deleting this post from the db + { + $del_all[ $row[ 'no' ] ] = 1; + } + + if( $row[ 'resto' ] ) { // it's a reply, need to update parent + $update_threads[ $row[ 'resto' ] ] = 1; + } + elseif( !$imgonly ) { // it's a thread parent and it's getting deleted from db + // need to delete thread html + if( USE_GZIP == 1 ) { + // HTML + $del_files[ RES_DIR . $row[ 'no' ] . PHP_EXT ] = 1; + $del_files[ RES_DIR . $row[ 'no' ] . PHP_EXT . '.gz' ] = 1; + // JSON + $del_files[ RES_DIR . $row[ 'no' ] . '.json' ] = 1; + $del_files[ RES_DIR . $row[ 'no' ] . '.json.gz' ] = 1; + } + else { + // HTML + $del_files [ RES_DIR . $row[ 'no' ] . PHP_EXT ] = 1; + // JSON + $del_files[ RES_DIR . $row[ 'no' ] . '.json' ] = 1; + } + + $del_threads[ $row[ 'no' ] ] = 1; + $replyquery = mysql_board_call( "SELECT * FROM `" . SQLLOG . "` WHERE resto='{$row[ 'no' ]}'" ); + while( $replyrow = mysql_fetch_assoc( $replyquery ) ) { + $del_files += image_files_for( $replyrow ); + $del_all[ $replyrow[ 'no' ] ] = 1; + } + mysql_free_result( $replyquery ); + } + + { + $auser = $_COOKIE[ '4chan_auser' ]; + $adfsize = ( $row[ 'fsize' ] > 0 ) ? 1 : 0; + $adname = str_replace( '
!', '#', $row[ 'name' ] ); + if( $imgonly ) { + $imgonly = 1; + } + else { + $imgonly = 0; + } + //$row['sub'] = mysql_escape_string( $row['sub'] ); + //$row['com'] = mysql_escape_string( $row['com'] ); + //$row['filename'] = mysql_escape_string( $row['filename'] ); + validate_admin_cookies(); + mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,email,admin_ip,tool) values('%s',%d,%d,'%s','%s','%s','%s','%s','%s','%s','%s','%s', 'del-all-by-ip')", $imgonly, $row[ 'no' ], $row[ 'resto' ], SQLLOG, $adname, $row[ "sub" ], $row[ "com" ], $adfsize, $row[ "filename" ].$row['ext'], $auser, $row[ 'email' ], $_SERVER['REMOTE_ADDR']); + } + } + mysql_free_result( $query ); + + // delete IP's posts + if( !$imgonly ) { + mysql_board_call( "DELETE FROM `" . SQLLOG . "` WHERE host='$ip'" . $_rep_sql ); + // delete replies to IP's parent posts + foreach( $del_threads as $parent => $unused ) { + mysql_board_call( "DELETE FROM `" . SQLLOG . "` WHERE resto='$parent'" ); + } + } + else { + mysql_board_call( "UPDATE `" . SQLLOG . "` SET filedeleted=1,root=root WHERE host='$ip'" . $_rep_sql ); + } + mysql_board_call( "UNLOCK TABLES" ); + + // delete all necessary files (images and HTML) + foreach( $del_files as $file => $unused ) { + @unlink( $file ); + + if( CLOUDFLARE_PURGE_ON_DEL && strpos( $file, IMG_DIR ) !== false ) { + $filename = basename( $file ); + cloudflare_purge_by_basename(BOARD_DIR, $filename); + } + } + + // delete reports for deleted posts + foreach( $del_all as $no => $unused ) { + mysql_global_do( "DELETE FROM reports WHERE board='" . SQLLOG . "' AND no='$no'" ); + mysql_global_do( "DELETE FROM reports_for_posts WHERE board='" . SQLLOG . "' AND postid='$no'" ); + } + + echo "
Deleting posts...
"; + if( $imgonly ) { + echo "All images deleted.
Deletion successful!
"; + } + else { + echo "All posts deleted.
Deletion successful!
"; + } + + // rebuild html + if( count( $update_threads ) > 25 ) { + echo "Rebuilding all pages..."; + echo ( rebuild_all( $error ) ? " OK!" : $error ); // at some number of threads, this must be faster... + } + else { + foreach( $update_threads as $parent => $unused ) { + if( $del_threads[ $parent ] ) continue; // this thread was deleted, forget it + + echo "Rebuilding No.$parent..."; + echo ( rebuild_thread( $parent, $error ) ? " OK!" : $error ); + echo "
"; + } + } + + die( "
Rebuild successful!Done." ); +} + +function admindelall() +{ + global $onlyimgdel, $onlyrepdel, $id, $user, $pass; + $delno = array(); + $delflag = false; + reset( $_POST ); + while( $item = each( $_POST ) ) { + if( $item[ 1 ] == 'delete' ) { + array_push( $delno, intval( $item[ 0 ] ) ); + $delflag = true; + } + } + if( $delflag ) { + if( !$result = mysql_board_call( "SELECT host FROM `" . SQLLOG . "` WHERE archived = 0 AND no=" . mysql_real_escape_string( $id ) ) ) { + echo S_SQLFAIL; + } + $row = mysql_fetch_row( $result ); + list( $host ) = $row; + delallbyip($host, $onlyimgdel, $onlyrepdel === true); + } + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "
\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "
\n"; + die( "" ); +} + +function ban_template_js($post_has_file = true, $is_thread = false) { + $templates = array(); + + $level_map = get_level_map(); + + $query = "SELECT * FROM ban_templates ORDER BY LENGTH(rule), rule ASC"; + $q = mysql_global_call($query); + + while ($r = mysql_fetch_assoc($q)) { + if (!preg_match('#^(global|' . BOARD_DIR . ')[0-9]+$#', $r[ 'rule' ])) { + continue; + } + + if (($r['no'] == 1 || $r['no'] == 123 || $r['no'] == 213) && !$post_has_file) { + continue; + } + + if ($r['no'] == 6 && !DEFAULT_BURICHAN) { + continue; + } + + if ($r['no'] == 17 && (BOARD_DIR === 'mlp' || BOARD_DIR === 'trash')) { + continue; + } + + if ($r['no'] == 223 && BOARD_DIR === 'pol') { + continue; + } + + // Global 3 - Troll posts + if ($r['no'] == 222 && BOARD_DIR === 's4s') { + continue; + } + + // Global 3 + if ((BOARD_DIR === 'b' || BOARD_DIR === 'bant') && strpos($r['rule'], 'global3') !== false) { + continue; + } + + // Skip OP-only templates + if ($r['postban'] === 'move' && !$is_thread) { + continue; + } + + if ($level_map[$r['level']] !== true) { + continue; + } + + unset($r['special_action']); + + $templates[] = $r; + } + + return ' + + + '; +} + +function do_post_quarantine( $board, $post ) +{ + /* + Gathers - + Current post, current post image + All images of posts in the same thread + */ + + mysql_board_lock( true ); + $host = $post[ "host" ]; + + $post_json = make_post_json($post); + $postno = $post["no"]; + + $xffres = mysql_global_do("select xff from xff where board='%s' and postno=%d", $board, $postno); + if (mysql_num_rows($xffres)) $xff = mysql_fetch_assoc($xffres)["xff"]; + $res = mysql_global_do("insert into ncmec_reports (board,post_num,post_json,xff) value ('%s',%d,'%s','%s')", $board, $postno, $post_json, $xff); + $reportid = mysql_global_insert_id(); + + $path = "/www/quarantine/$reportid"; + mkdir( $path ); + + $image = $post[ "tim" ] . $post[ "ext" ]; + $dst_path = "$path/$image"; + $tmp_path = $dst_path.".tmp"; + @copy( IMG_DIR . "/$image", $tmp_path ); + @rename($tmp_path, $dst_path); + + if (!file_exists($dst_path)) { + // guess we can't quarantine it after all + mysql_global_do("delete from ncmec_reports where id=%d", $reportid); + } else { + $resto = $post["resto"]; + $respred = $resto ? "no=$resto or resto=$resto" : "resto=$postno"; + $q = mysql_board_call( "select * from `%s` where host='%s' and no!=%d and ($respred)", SQLLOG, $host, $postno ); + while( $p = mysql_fetch_assoc( $q ) ) { + $i = $p[ "tim" ] . $p[ "ext" ]; + mkdir( "$path/images" ); + @copy( IMG_DIR . "/$i", "$path/images/$i" ); + } + } + mysql_board_unlock(); +} + +function do_template_special_action($template, $board, $row, $is_manager = false) { + if ($template['special_action'] === 'quarantine') { + do_post_quarantine($board, $row); + } + + if ($is_manager) { + if( $template['special_action'] === 'quarantine' || $template['special_action'] === 'revokepass_spam' || $template['special_action'] === 'revokepass_illegal') { + $pass = $row['4pass_id']; + $status = $template['special_action'] === 'revokepass_spam' ? 4 : 5; + mysql_global_do("UPDATE pass_users SET status = %d WHERE user_hash = '%s' AND status = 0 LIMIT 1", $status, $pass); + } + } +} + +/** + * Auto-rangeban log entries + * $tpl_id: ban or BR template id + * $source: 1 = ban, 0 = ban request + */ +function process_auto_rangeban($ip, $browser_id, $thread_id, $post_id, $tpl_id, $source) { + $thread_id = (int)$thread_id; + + if (!$browser_id) { + return false; + } + + // Prune stale entries + $sql = "DELETE FROM event_log WHERE type = 'rangeban_hint' AND created_on < DATE_SUB(NOW(), INTERVAL 1 HOUR)"; + + $res = mysql_global_call($sql); + + if (!$res) { + return false; + } + + $need_rangeban = false; + + // Check if should apply auto rangeban (2 strikes for BRs, immediate for Bans) + $range_sql = explode('.', $ip); + + $range_sql = "{$range_sql[0]}.{$range_sql[1]}.%"; + + // Ban + if ($source === 1) { + $need_rangeban = true; + } + // Ban Request + else { + $sql =<< 0) { + $need_rangeban = true; + } + } + + if ($need_rangeban) { + // Skip if a rangeban already exists + $sql =<< DATE_SUB(NOW(), INTERVAL 1 HOUR) +SQL; + + $res = mysql_global_call($sql, BOARD_DIR, $browser_id, $range_sql); + + if (!$res) { + return false; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count > 0) { + return true; + } + + return add_auto_rangeban_log($ip, $browser_id, $thread_id, $post_id, true, $tpl_id, $source); + } + + // Add hint entry + return add_auto_rangeban_log($ip, $browser_id, $thread_id, $post_id, false, $tpl_id, $source); +} + +function add_auto_rangeban_log($ip, $browser_id, $thread_id, $post_id, $is_ban = false, $tpl_id = 0, $source = 0) { + if ($is_ban) { + $type = 'rangeban'; + } + else { + $type = 'rangeban_hint'; + } + + return write_to_event_log($type, $ip, [ + 'board' => BOARD_DIR, + 'thread_id' => $thread_id, + 'post_id' => $post_id, + 'ua_sig' => $browser_id, + 'arg_num' => $tpl_id, + 'arg_str' => (int)$source + ]); +} + +/** + * Collects posts related to the provided Password. + * This is used for banning people who hop between multiple IPs. + * Only posts made from non-mobile devices are collected. + */ +function admin_collect_related($ip, $pwd) { + if (!$pwd || !$ip) { + return null; + } + + $range_sql = explode('.', $ip); + + $range_sql[0] = (int)$range_sql[0]; + $range_sql[1] = (int)$range_sql[1]; + + $range_sql = "{$range_sql[0]}.{$range_sql[1]}."; + + $sql = <<setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); + $m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms + $m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms + $m->addServer(MEMCACHED_HOST, MEMCACHED_PORT); + + if (!$m) { + return false; + } + + $key = "def.$board.$thread_id"; + + if ($flag) { + if ($duration > 43200) { // 12h + $duration = 43200; + } + + $now = $_SERVER['REQUEST_TIME']; + + return $m->set($key, 1, $now + $duration); + } + else { + return $m->delete($key); + } +} + +function admin_get_template_by_id($tpl_id) { + $tpl_id = (int)$tpl_id; + $sql = "SELECT * FROM ban_templates WHERE no = $tpl_id LIMIT 1"; + $res = mysql_global_call($sql); + if (!$res) { + return false; + } + return mysql_fetch_assoc($res); +} + +function admin_is_ip_rangebanned($ip) { + require_once 'lib/geoip2.php'; + + $_asninfo = GeoIP2::get_asn($ip); + + if ($_asninfo) { + $asn = (int)$_asninfo['asn']; + } + else { + $asn = 0; + } + + if ($asn > 0) { + $query =<< 0) { + return true; + } + } + + $long_ip = ip2long($ip); + + if (!$long_ip) { + $this->error('Invalid IP.'); + } + + $query =<<= $long_ip +AND active = 1 AND boards = '' AND expires_on = 0 AND report_only = 0 +LIMIT 1 +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + return mysql_num_rows($res) > 0; +} + +function admin_protect_thread() { + if (!isset($_GET['thread_id'])) { + error('Bad Request.'); + } + + $thread_id = (int)$_GET['thread_id']; + + if ($thread_id <= 0) { + error('Bad Request.'); + } + + if (isset($_GET['remove']) && $_GET['remove']) { + $flag = false; + } + else { + $flag = true; + } + + $ret = admin_toggle_protected_thread($flag, BOARD_DIR, $thread_id); + + if (!$ret) { + error('Something went wrong.'); + } + else { + echo "Done."; + } +} + +// Signs the ip + timestamp for authenticating reverse dns requests below +// FIXME: This is to avoid delaying ban panels +function admin_get_rev_ip_sig($ip, $t) { + if (!$ip || !$t) { + return false; + } + + $secret = 'BusEFdduVhgVKIMAx1ndhvzrgMyA5uCcfRnvIKq4+0X2vL8elzf6wHZCpWS9fsTsNG/XdlwiIBV68hzlGm6sGQ=='; + $secret = base64_decode($secret); + + if (!$secret) { + return false; + } + + $msg = "$ip $t"; + + return hash_hmac('sha256', $msg, $secret); +} + +// Prints a JSON response with the hostname of the IP +// FIXME: This is to avoid delaying ban panels +// The IP needs to be in the long int format +function admin_reverse_ip() { + if (!isset($_GET['ip']) || !isset($_GET['t']) || !isset($_GET['s'])) { + die('N/A'); + } + + if (!$_GET['t'] || !$_GET['s']) { + die('N/A'); + } + + $ip = long2ip($_GET['ip']); + + if (!$ip) { + die('N/A'); + } + + if ($_SERVER['REQUEST_TIME'] - (int)$_GET['t'] > 3) { + die('N/A'); + } + + $sig = admin_get_rev_ip_sig($_GET['ip'], $_GET['t']); + + if (!$sig) { + die('N/A'); + } + + if (hash_equals($sig, $_GET['s']) !== true) { + die('N/A'); + } + + $rev = gethostbyaddr($ip); + + if ($rev && $rev == $ip) { + $rev = ''; + } + + header('Content-Type: application/json'); + echo json_encode(['rev' => $rev]); +} + +/* Admin banning */ +function adminban() +{ + if (BOARD_DIR == 'j' && !has_level('manager')) { + die(); + } + + global $id, $user, $pass; + + $by_tpl_mode = false; + + // for async calls from reports.4chan.org + if (isset($_POST['by_tpl']) && $_POST['by_tpl']) { + $template = admin_get_template_by_id($_POST['by_tpl']); + + if (!$template) { + die('No such template'); + } + + $by_tpl_mode = true; + + $_POST['submit'] = 1; + $_POST['pubreason'] = $template['publicreason']; + $_POST['pvtreason'] = $template['privatereason']; + $_POST['days'] = $template['days']; + $_POST['warn'] = (int)($template['days'] == 0 && $template['banlen'] == ''); + $_POST['indefinite'] = (int)($template['banlen'] === 'indefinite'); + $_POST['banmsg'] = 0; + $_POST['bantype'] = $template['bantype']; + + // This will be amended later for delall -> delallrep + $_POST['postban'] = $template['postban']; + + if ($template['postban'] === 'move' && $template['postban_arg']) { + $_POST['move-board'] = $template['postban_arg']; + } + } + else { + $template = null; + } + + $submit = $_POST[ 'submit' ]; + $start_time = microtime( true ); + $xff = htmlspecialchars($_POST[ 'xff' ], ENT_QUOTES); + $pubreason = nl2br( htmlspecialchars( $_POST[ 'pubreason' ] ), false ); + $pvtreason = nl2br( htmlspecialchars( $_POST[ 'pvtreason' ] ), false ); + $reason = "$pubreason<>$pvtreason"; + $bannedby = $_COOKIE['4chan_auser']; + $days = $_POST[ 'days' ]; + $warn = $_POST[ 'warn' ]; + $indefinite = $_POST[ 'indefinite' ]; + $banmsg = $_POST[ 'banmsg' ] == 1; + $globalban = $_POST[ 'bantype' ] == 'global'; + $zonly = isset($_POST['zonly']) && $_POST['zonly'] === '1'; + //$pass_id = htmlspecialchars($_POST[ 'pass_id' ], ENT_QUOTES); + $board = BOARD_DIR; + $postid = (int)$id; + + if ($by_tpl_mode) { + $template_used = (int)$_POST['by_tpl']; + } + else { + $template_used = (int)$_POST['templateno']; + } + + if( !$result = mysql_board_call( "SELECT HIGH_PRIORITY * FROM `" . SQLLOG . "` WHERE no=" . $postid ) ) { + die( 'Post no longer exists.' ); + } + $row = mysql_fetch_assoc( $result ); + if( $row === false ) die( 'Post no longer exists.' ); + + if ($row['archived']) { + die('This post is archived.'); + } + + $post_has_file = $row['ext'] && !$row['file_deleted']; + + //list( $no, $sticky, $permasage, $closed, $now, $name, $email, $sub, $com, $host, $pwd, $filename, $ext, $w, $h, $tn_w, $tn_h, $tim, $time, $md5, $fsize, $root, $resto ) = $row; + extract( $row, EXTR_OVERWRITE ); + + $password = $pwd; + + // insert tripcode (trip or !sectrip) if not warning + $tripcode = ''; + + if ($warn != 1) { + $name_bits = explode('
!', $name); + + if ($name_bits[1]) { + $tripcode = preg_replace('/<[^>]+>/', '', $name_bits[1]); // fixme: why do we do that? + } + } + + $name = str_replace( ' !', ' #', $name ); + $name = preg_replace( '/<[^>]+>/', '', $name ); // remove all remaining html crap + + if( !$result = mysql_board_call( "select COUNT(*) FROM `" . SQLLOG . "` WHERE host='$host' AND no=$resto" ) ) { + echo S_SQLFAIL; + } + + if (mysql_result($result, 0, 0) || $resto == 0) { + $poster_is_op = true; + } + else { + $poster_is_op = false; + } + +if( $submit != "" ) { // pressed submit + if (!$host) { + error('You cannot ban this post'); + } + + if ($host) { + $reverse = gethostbyaddr($host); + } + else { + $reverse = ''; + } + + $displayhost = ( $reverse && $reverse != $host ) ? "$reverse ($host)" : $host; + + if ($template_used > -1) { + if (!$template) { + $template = mysql_global_row("ban_templates", "no", $template_used); + } + + if (!$template) { + error('Invalid template'); + } + + if (!has_level($template['level'])) { + error('You cannot use this template'); + } + + if (($template['no'] == 1 || $template['no'] == 123 || $template['no'] == 213) && !$post_has_file) { + error('This template requires a post with a file'); + } + } + + if( !$template_used ) { + $rule = ''; + } + else { + $rule = $template[ 'rule' ]; + } + + if( !$row ) { + echo "This post doesn't exist anymore.
"; + die( "[Back]" ); + } + if( $pubreason == "" ) { + echo "Public reason not specified.
"; + die( "[Back]" ); + } + elseif( $bannedby == "" ) { + echo "Admin name not specified.
"; + die( "[Back]" ); + } + elseif( !is_numeric( $days ) && ( $indefinite != 1 ) && ( $warn != 1 ) ) { + echo "Length of ban not specified.
"; + die( "[Back]" ); + } + else { + if( $warn != 1 ) { + $ubd_ts = date( "Y-m-d H:i:s", time() + $days * ( 24 * 60 * 60 ) ); + } + else { + $ubd_ts = date( "Y-m-d H:i:s", time() ); + } + if( $indefinite == 1 ) { + $length = "00000000000000"; + } + else { + $length = $ubd_ts; + } + } + + $is_manager = has_level('manager'); + + if (!$is_manager) { + $zonly = 0; + } + + $nrow = array(); + + foreach( $row as $key => $val ) { + if( ctype_digit( $val ) || is_int( $val ) ) { + $val = (int)$val; + } + $nrow[ $key ] = $val; + } + + if ($row['resto']) { + $sub_query = mysql_board_call("SELECT sub FROM `%s` WHERE no = %d", $board, $row['resto']); + $sub_res = mysql_fetch_assoc($sub_query); + if ($sub_res) { + $rel_sub = $sub_res['sub']; + + if (strpos($rel_sub, 'SPOILER<>') === 0) { + $rel_sub = substr($rel_sub, 9); + } + + if ($rel_sub !== '') { + $nrow['rel_sub'] = $rel_sub; + } + } + } + + // FIXME: email field + if (isset($row['email'])) { + $nrow['ua'] = $row['email']; + unset($nrow['email']); + } + + $post_json = json_encode($nrow); + $no_thumb = false; + + if ($template && $template['save_post'] !== 'everything') { + $no_thumb = true; + } + + $result = mysql_global_do( "INSERT INTO " . SQLLOGBAN . " (board,global,zonly,name,host,reverse,xff,reason,length,admin,md5,4pass_id,post_num,rule,post_time,post_json,template_id,admin_ip,tripcode,password) VALUES ('%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, '%s', FROM_UNIXTIME(%d), '%s', %d, '%s', '%s', '%s')", $board, $globalban, $zonly, $name, $host, $reverse, $xff, $reason, $length, $bannedby, $md5, $row[ '4pass_id' ], $no, $rule, $time, $post_json, $template_used, $_SERVER['REMOTE_ADDR'], $tripcode, $password ); + + if( !$result ) { + echo S_SQLFAIL; + } + + if( $ext != '' && $template_used && !$no_thumb ) { + $salt = file_get_contents( SALTFILE ); + $hash = sha1( BOARD_DIR . $no . $salt ); + @copy( THUMB_DIR . "{$tim}s.jpg", BANTHUMB_DIR . "{$hash}s.jpg" ); + } + + if( $banmsg ) { + if( $warn ) { + $samessage = S_USERWARNEDFORPOST; + } + else { + $samessage = S_USERBANNEDFORPOST; + } + + //if( isset( $_GET[ 'santa' ] ) && $_GET[ 'santa' ] == 'hohoho' ) $samessage = 'USER WAS GIVEN COAL FOR THIS POST'; + + if (($is_manager || has_flag('banmsg')) && isset($_POST['custombanmsg']) && $_POST['custombanmsg'] != '') { + $samessage = mysql_real_escape_string(htmlspecialchars($_POST['custombanmsg'], ENT_QUOTES)); + } + + if( !$result = mysql_board_call( "UPDATE `" . SQLLOG . "` SET root=root,com=CONCAT(com,'

($samessage)') WHERE no=%d", $postid ) ) { + echo S_SQLFAIL; + } + mysql_board_call("UPDATE `%s` SET root=root,last_modified=%d where no=%d", SQLLOG, (int)$_SERVER['REQUEST_TIME'], ($resto ? $resto : $postid)); + + if ($_POST['postban'] !== 'delpost' && $_POST['postban'] !== 'move' && $_POST['postban'] !== 'archive') { + admin_clear_reports(BOARD_DIR, $postid); + } + } + + //print "\n
insert: " . (time() - $start_time)."\n
"; //disabling this because again nobody needs to see it/leaks filepaths + echo "Banning " . $displayhost . " from "; + + if( $globalban == 1 ) { + echo "the entirety of 4chan...
"; + //append_ban( "global", $host ); + } + else { + echo "/" . $board . "/...
"; + //append_ban( $board, $host ); + } + + if( $length == "00000000000000" ) { + echo " for an indefinite amount of time."; + } + else { + echo " until " . date( 'l, F jS, Y', time() + $days * ( $warn ? 0 : 24 * 60 * 60 ) ) . ".
Ban successful!"; + } + //print "\n
rebuild : " . (microtime(1) - $start_time); //disabling because no point in showing it/leaks file paths + if( $template ) { + global $gcon; + $inserted_ban_id = mysql_insert_id($gcon); + do_template_special_action( $template, $board, $row, $is_manager ); + if( ( $template[ "blacklist" ] == "image" || $template[ 'blacklist' ] == 'rejectimage' ) && $md5 ) { + $blban = (int)( $template[ 'blacklist' ] === 'image' ); + $len = $blban ? $template['days'] : '0'; + mysql_global_do( "insert into blacklist (field,contents,description,addedby,ban,banlength,banreason)" . + "values ('md5','%s','%s','%s','$blban','$len','%s')", + $md5, $template[ "name" ] . " (via ban template, ban ID: $inserted_ban_id)", $bannedby, $template[ "publicreason" ] ); + } + } + + // Auto-rangebans processing (bans) + // FIXME: email field + $_post_meta = decode_user_meta($row['email']); + + if (!$warn && $_post_meta && $_post_meta['is_mobile']) { // mobile devices only + // global rules only + if ($template && strpos($template['rule'], 'global') !== false) { + process_auto_rangeban($host, $_post_meta['browser_id'], $row['resto'], $row['no'], $template['no'], 1); + } + + // Collect and ban other IPs based on the password + /* + $related_posts = admin_collect_related($host, $password); + + if ($related_posts) { + write_to_event_log('rel_posts', $host, [ + 'board' => BOARD_DIR, + 'thread_id' => $row['resto'], + 'post_id' => $no, + 'pwd' => $password, + 'arg_str' => $rule, + 'meta' => json_encode($related_posts) + ]); + } + */ + } + + $should_delete = $_POST[ 'postban' ] == 'delpost' || $_POST[ 'postban' ] == 'delfile'; + + $skip_rebuild = false; + + if( $should_delete ) { + echo "
"; + if (delete_post($no, $_POST[ 'postban' ] == 'delfile' ? 1 : 0, $template ? $template['no'] : false, 'ban')) { + echo ( ( $_POST[ 'postban' ] == 'delfile' ) ? "File deleted." : "Post deleted." ); + } + // Fixme, this is for the temporary is2/is3 cache purging api + if ($ext != '' && $template_used && $template['rule'] == 'global1' && !UPLOAD_BOARD) { + purge_cache_internal_temp(BOARD_DIR, "$tim$ext"); + } + //print "\n
delete post: " . (microtime(true) - $start_time); //disabling, no point and leaks dirs + } + else if ($resto == 0) { + if ($_POST['postban'] == 'move') { + if (!isset($_POST['move-board']) || !is_board_valid($_POST['move-board'])) { + echo ('Invalid destination board. The thread was not moved.'); + } + else { + move_thread($no, $_POST['move-board']); + + echo ('
Thread moved to /' . htmlspecialchars($_POST['move-board']) . '/.'); + + $skip_rebuild = true; + } + } + else if ($_POST['postban'] === 'archive') { + archive_thread($no); + + echo ('
Thread archived.'); + + $skip_rebuild = true; + } + else if ($_POST['postban'] === 'close') { + if (mysql_board_call('UPDATE `%s` SET closed = 1 WHERE no = %d LIMIT 1', BOARD_DIR, $no)) { + log_thread_opts_action($row, $row['sticky'], $row['permasage'], 1, $row['permaage'], $row['undead']); + echo ('
Thread closed.'); + } + else { + echo ('
Could not close thread.'); + } + } + else if ($_POST['postban'] === 'permasage') { + if (mysql_board_call('UPDATE `%s` SET permasage = 1 WHERE no = %d LIMIT 1', BOARD_DIR, $no)) { + log_thread_opts_action($row, $row['sticky'], 1, $row['closed'], $row['permaage'], $row['undead']); + echo ('
Thread perma-saged.'); + } + else { + echo ('
Could not perma-sage thread.'); + } + } + } + + echo ''; + if( $banmsg && !$should_delete && !$skip_rebuild) { //need to update log because of the ban message + rebuild_thread( ( $resto ) ? $resto : $no ); + } + + // Delete all posts by IP, including threads + if ($_POST['postban'] === 'delall') { + delallbyip($host, false); + } + // Delete only replies by IP + else if ($_POST['postban'] === 'delallrep') { + // Delete the thread if the target post is an OP + if (!$resto) { + delete_post($no, 0, $template ? $template['no'] : false, 'ban'); + } + + delallbyip($host, false, true); + } + + //print "\n
total time: " . (microtime(1) - $start_time); //dont need to display this +} else { + // Banning screen display + $adminuser = mysql_real_escape_string( $_COOKIE[ '4chan_auser' ] ); + // see if user is banned + $ban_summary = get_bans_summary($host); + + if( $ban_summary['total'] > 0 ) { // don't bother checking the active ban if there weren't ever any bans on this IP... + if( !$banned = mysql_global_call( "SELECT host,board,global,zonly, DATE_FORMAT(length, 'Until %W, %M %D, %Y.') AS buntil FROM " . SQLLOGBAN . " WHERE host='" . $host . "' AND active=1" ) ) { + echo S_SQLFAIL; + } + $bannedrows = mysql_num_rows( $banned ); + if( $bannedrows > 0 ) { + while ($ban_row = mysql_fetch_array($banned)) { + $buntil = $ban_row[ 'buntil' ]; + $gban = $ban_row[ 'global' ]; + $bannedboard = $ban_row[ 'board' ]; + $bannedzonly = $ban_row[ 'zonly' ]; + if( $bannedboard == BOARD_DIR ) { + $bg = "f0d0d0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + $bantrue = 1; + } + if( $gban == 1 ) { + $bg = "f0a0a0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + $globally = " (Globally)"; + $bantrue = 1; + break; + } + else { + $globally = " (" . $board . ")"; + } + } + } + if( $bantrue ) { + echo ""; + } + } + + $note = array(); + + if ($poster_is_op) { + $note[] = 'This poster is the OP'; + + if ($resto == 0) { + $_count = admin_get_thread_history($host); + + if ($_count > 1) { + $note[0] .= ' ' . ($_count - 1) . ''; + } + } + } + + if ($row['4pass_id'] != '') { + $has_4chan_pass = $row['4pass_id']; + + $note[] = 'This user is using a 4chan Pass'; + + $ban_summary_pass = get_bans_summary($has_4chan_pass, true); + } + else { + $has_4chan_pass = false; + $ban_summary_pass = null; + } + + if (!preg_match('/Android|iPhone|iPad/', $_SERVER['HTTP_USER_AGENT'])) { + $autofocus_html = ' autofocus="autofocus"'; + } + else { + $autofocus_html = ''; + } + + if ($host) { + $geoinfo = GeoIP2::get_country($host); + $asninfo = GeoIP2::get_asn($host); + } + else { + $geoinfo = $asninfo = false; + } + + if ($asninfo && isset($asninfo['aso'])) { + $aso_formatted = ' (' . htmlspecialchars($asninfo['aso'], ENT_QUOTES) . ')'; + } + else { + $aso_formatted = ''; + } + + echo '
'; + echo csrf_tag(); + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo '' . "\n"; + echo "\n"; + echo "" . "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + + if ($geoinfo && isset($geoinfo['country_code'])) { + $geo_loc = array(); + + if (isset($geoinfo['city_name'])) { + $geo_loc[] = $geoinfo['city_name']; + } + + if (isset($geoinfo['state_code'])) { + $geo_loc[] = $geoinfo['state_code']; + } + + $geo_loc[] = $geoinfo['country_name']; + + $loc = htmlspecialchars(implode(', ', $geo_loc), ENT_QUOTES); + + echo ' + + + + + '; + } + + $ban_history_row = array(); + + if ($ban_summary['total'] > 0) { + $ban_history_row[] = get_ban_history_html($ban_summary, $host); + + } + + if ($ban_summary_pass['total'] > 0) { + $ban_history_row[] = get_ban_history_html($ban_summary_pass); + } + + if (!empty($ban_history_row)) { + echo "\n"; + } + + // Browser ID + $_post_meta = decode_user_meta($row['email']); + + $query = "SELECT warn_req, ban_templates.name FROM ban_requests LEFT JOIN ban_templates ON ban_template = ban_templates.no WHERE host='%s'"; + $result = mysql_global_call($query, $host); + $brpending = array(); + while ($row = mysql_fetch_assoc($result)) { + $brpending[] = $row['name'] . ($row['warn_req'] ? ' [Warn]' : ''); + } + $brtooltip = join("\n", $brpending); + $pending = ''; + $brcount = count($brpending); + if ($brcount > 0) { + $plural = ($brcount > 1) ? 's' : ''; + $pending = << + + + +HTML; + } + echo $pending; + + echo "\n"; + + echo "\n"; + + echo "\n"; + + echo ""; + + if (!empty($note)) { + $note = implode('
', $note); + + echo ""; + } + + if (has_level('manager')/* || has_flag('developer')*/) { + $toz = ' [Unappealable]'; + } + else { + $toz = ''; + } + + if (has_level('manager') || has_flag('banmsg')) { + $ban_msg_row = " +JS; + + $pub_ban_evt = ' onchange="toggleBanMsg(this)"'; + } + else { + $ban_msg_row = $pub_ban_evt = ''; + } + + if ($resto == 0 && ENABLE_ARCHIVE) { + $_opt_archive = ''; + } + else { + $_opt_archive = ''; + } + + if (!$host) { + $btn_disabled = ' disabled'; + } + else { + $btn_disabled = ''; + } + + echo "$ban_msg_row"; + + echo ""; + } + else { + echo ""; + } + + $can_thread_ban = false; + + if ($resto == 0 && (has_level('manager') || has_flag('threadban'))) { + echo ""; + $can_thread_ban = true; + } + + echo "
Autocomplete
Template
Name
IP
Host
Location
Ban History" . + implode(' ', $ban_history_row) . "
Ban Requests + [$brcount pending ban request$plural] +
Public Ban Reason
Private Info
Unban In days [Warn] [Perma]
More Info[View Info] [Search]" . ($_post_meta['is_mobile'] ? ' ' : '') . "
Note$note
Message"; + + $ban_msg_row .= << + function toggleBanMsg(cb) { + var el = document.getElementById('pub-ban-msg'); + if (!el) { return; } + if (cb.checked) { + el.style.display = ''; + } + else { + el.style.display = 'none'; + } + } +
Ban Scope
[Public Ban]$toz
Post-Ban
Ban Thread
\n"; + echo "
"; + + // Async reverse IP request + if ($host) { + $_rev_long_ip = ip2long($host); + $_rev_ts = $_SERVER['REQUEST_TIME']; + $_rev_sig = admin_get_rev_ip_sig($_rev_long_ip, $_rev_ts); + ?> + + + + + + var el; + + function submitRequest(e) { + var select, index; + + select = document.forms[0].template; + index = select.selectedIndex; + + if (index === 0) { + e.preventDefault(); + e.stopPropagation(); + alert("You forgot to select a template."); + } + else { + if (/ Child |\[Perm\]/.test(select.options[index].textContent)) { + if (!checkSubmitConfirm(this)) { + e.preventDefault(); + e.stopPropagation(); + return; + } + } + postBack("start-ban-$board-$no"); + } + } + + if (el = document.getElementById("submit-ban-btn")) { + el.addEventListener("click", submitRequest, false); + } + +HTML; + + echo $html; + + $is_manager = has_level('manager') || has_flag('developer'); + + echo ban_template_js($post_has_file, $resto == 0); + + echo "
"; + + if ($has_4chan_pass && has_level('mod')) { + if ($is_manager) { + echo ""; + } + else { + $hashed_4chan_pass = admin_hash_4chan_pass($has_4chan_pass); + echo ""; + } + } + + if ($md5) { + echo ""; + echo ""; + + echo ""; + } + + echo ""; + + if ($host && admin_is_ip_rangebanned($host)) { + echo ""; + } + + if ($_post_meta['req_sig']) { + echo ""; + } + + if ($_post_meta['browser_id']) { + echo ""; + } + + $_user_status = user_known_status_to_str($_post_meta['known_status']); + + if ($_post_meta['verified_level']) { + $_user_status .= ', Verified'; + } + + if ($_user_status) { + echo ""; + } + + echo ""; + echo "
4chan Pass
Hashed Pass
MD5
Filename
PHash
Password
Rangeban
Req. Sig.
Browser ID
User Status
[Close]
"; + + die( "" ); +} +} + +function adminToggleSpoiler($post, $new_spoiler) { + if (strpos($post['sub'], 'SPOILER<>') === 0) { + $old_subject = substr($post['sub'], 9); + $old_spoiler = true; + } + else { + $old_subject = $post['sub']; + $old_spoiler = false; + } + + if ($old_spoiler == $new_spoiler) { + return false; + } + + if ($new_spoiler) { + $subject = 'SPOILER<>' . $old_subject; + $actionType = 1; + } + else { + $subject = $old_subject; + $actionType = 2; + } + + $query = "UPDATE " . BOARD_DIR . " SET sub = '%s' WHERE no = %d LIMIT 1"; + $res = mysql_board_call($query, $subject, $post['no']); + + if (!$res) { + die('Database error (ats).'); + } + + $maskShift = 128; + $actionId = $maskShift + $actionType; + + $query =<< 60 ) ) { + echo "Sticky number must be between 0 and 59. Higher numbers appear above lower numbers.
"; + die( "[Back]" ); + } + else { + if( strlen( $post_sticknum ) == 1 ) $post_sticknum = "0" . $post_sticknum; + $post_sticknum = "202701010000" . $post_sticknum; + } + + echo ""; + $vars = ""; + echo "Thread flag status:
    "; + if( $post_sticky == 1 ) { + echo "
  • Sticky ✓
  • "; + $vars .= "sticky=1,root=" . $post_sticknum . ","; + } + else { + if( $sticky == 1 ) { + $sticktime = "now()"; + } + else { + $sticktime = "root"; + } + + echo '
  • Sticky ✗
  • '; + $vars .= "sticky=0,root=" . $sticktime . ","; + } + if( $post_permasage == 1 ) { + echo "
  • Perma-sage ✓
  • "; + $vars .= "permasage=1,"; + } + else { + echo '
  • Perma-sage ✗
  • '; + $vars .= "permasage=0,"; + } + if( $post_closed == 1 ) { + echo "
  • Closed ✓
  • "; + $vars .= "closed=1,"; + } + else { + echo '
  • Closed ✗
  • '; + $vars .= "closed=0,"; + } + if( $post_permaage ) { + if( $is_managerplus ) { + echo '
  • Perma-age ✓
  • '; + $vars .= "permaage=1,"; + } + } + else { + if( $is_managerplus ) { + echo '
  • Perma-age ✗
  • '; + $vars .= "permaage=0,"; + } + } + if( $post_undead ) { + //if( $is_managerplus ) { + echo '
  • Undead ✓
  • '; + $vars .= "undead=1,"; + //} + } + else { + //if( $is_managerplus ) { + echo '
  • Undead ✗
  • '; + $vars .= "undead=0,"; + //} + } + + // Clear the undead flag when a moderator modifies the sticky flag + // so the thread doesn't turn into a rolling sticky or get stuck as undead + /* + if ($undead && !$is_managerplus && $post_sticky != $sticky) { + echo '
  • Undead ✗
  • '; + $vars .= "undead=0,"; + } + */ + $vars .= "last_modified=".$_SERVER['REQUEST_TIME']; // FIXME consider checking if we only change hidden vars and don't update this + + echo '
'; + + if( !$result = mysql_board_call( "UPDATE `" . SQLLOG . "` SET %s WHERE no=%d", $vars, $post_id ) ) { + echo S_SQLFAIL; + } + + log_thread_opts_action($row, $post_sticky, $post_permasage, $post_closed, $post_permaage, $post_undead); + + if( $post_sticky != $sticky || $post_closed != $closed) rebuild_thread( $post_id ); + } + else { + echo '
'; + echo csrf_tag(); + echo "\n"; + echo "\n"; + + if( BOARD_DIR != 'b' || $GLOBALS[ 'b_sticky' ] ) { + echo "\n"; + } + echo "\n"; + echo "\n"; + + if ($is_managerplus) { + echo "\n"; + } + + //if ($is_managerplus) { + echo "\n"; + //} + + echo "\n"; + + echo "
Sticky     (Order: 0-59)
Perma-sage
Closed
Perma-age
Undead
\n"; + echo "
"; + + + /** + * Thread moving form + */ + if (BOARD_DIR !== 'b' && !UPLOAD_BOARD && !JANITOR_BOARD) { + echo move_thread_form($post_id); + } + + if (BOARD_DIR === 'test') { + echo admin_protect_thread_form($post_id); + } + + die( "" ); + } +} + +function log_thread_opts_action($post_data, $sticky, $permasage, $closed, $permaage, $undead) { + if (!isset($post_data['no']) || !$post_data['no']) { + die('Internal Server Error (ltoa)'); + } + + $new_mask = 0 + (($sticky) ? 1 : 0) + + (($permasage) ? 2 : 0) + + (($closed) ? 4 : 0) + + (($permaage) ? 8 : 0) + + ($undead ? 16 : 0); + + $old_mask = 0 + (($post_data['sticky']) ? 1 : 0) + + (($post_data['permasage']) ? 2 : 0) + + (($post_data['closed']) ? 4 : 0) + + (($post_data['permaage']) ? 8 : 0) + + ($post_data['undead'] ? 16 : 0); + + if ($new_mask == $old_mask) { + return false; + } + + $query = << Board'); + + foreach ($boardlist as $b_dir => $b_title) { + if ($b_dir === BOARD_DIR || $b_dir === 'f') { + continue; + } + $board_sel[] = ''; + } + + return implode("\n", $board_sel); +} + +function move_thread_form($post_id) { + $csrf_tag = csrf_tag(); + + $board_sel = get_board_options_html(); + + if (!ENABLE_ARCHIVE) { + $del_attrs = ' checked'; + } + else { + $del_attrs = ''; + } + + return << +
+$csrf_tag + + + + + + + + + + + + + + +
Move to
+ +
+
+HTML; +} + +function admin_protect_thread_form($thread_id) { + return << +
+ + +
+HTML; +} + +function adminExt() +{ + global $thread; + $where = ''; + + if( isset( $_GET[ 'from' ] ) && ctype_digit( $_GET[ 'from' ] ) ) { + $from = intval( $_GET[ 'from' ] ); + $where = " AND no >= $from"; + } + + if( !$thread ) return false; + + $thread = (int)$thread; + if( !$result = mysql_board_call( "SELECT `host`, `no` FROM `%s` WHERE (no=%d OR resto=%d)$where", SQLLOG, $thread, $thread ) ) { + echo S_SQLFAIL; + + return false; + } + $json = array(); + + $salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$salt) { + die('Internal Server Error'); + } + + while ($row = mysql_fetch_assoc($result)) { + $hash = substr(base64_encode(pack( "H*", sha1($row['host'] . $salt))), 0, 8); + + $json[$row['no']] = $hash; + } + + echo json_encode( $json, JSON_NUMERIC_CHECK ); + + die(); +} + +function adminBanReq() +{ + $no = (int)$_GET['id']; + $board = BOARD_DIR; + $janitor = $_COOKIE['4chan_auser']; + + if ($board === 'j') { + die(); + } + + $result = mysql_board_call( "SELECT * FROM `%s` WHERE no=%d", $board, $no ); + + if (!mysql_num_rows($result)) { + echo ''; + error("This post doesn't exist anymore"); + } + + $post = mysql_fetch_assoc($result); + + if ($post['archived']) { + echo ''; + error("This post is archived"); + } + + if ($post['host'] === '') { + error('You cannot request a ban for this post.'); + } + + $post_has_file = $post['ext'] && !$post['file_deleted']; + + if (!access_board(BOARD_DIR)) { + // Check if the report is unlocked, weight threshold is 1500 + $query = << 0, weight, weight * 1.25))) as total_weight +FROM reports +WHERE board = '%s' AND no = %d +SQL; + + $result = mysql_global_call($query, $board, $no); + + if (!$result) { + error('Database Error (abru1'); + } + + $total_weight = (int)mysql_fetch_row($result)[0]; + + if (!$total_weight || $total_weight < 1500) { + error('You do not have permission to access this board.'); + } + } + + // for async calls from reports.4chan.org + if (isset($_POST['by_tpl']) && $_POST['by_tpl']) { + $_POST['template'] = $_POST['by_tpl']; + unset($_POST['warn_req']); + } + + if( $_POST[ 'template' ] ) { + $template = (int)$_POST['template']; + + if ($template < 1) { + error('You forgot to select a template.'); + } + + $bquery = mysql_global_call( "SELECT * FROM ban_templates WHERE no=%d", $template ); + $bres = mysql_fetch_assoc( $bquery ); + + if (!has_level($bres['level'])) { + error('You cannot use this template'); + } + + if (($bres['no'] == 1 || $bres['no'] == 123 || $bres['no'] == 213) && !$post_has_file) { + error('This template requires a post with a file'); + } + + $reason = $bres['publicreason']; + + $xffquery = mysql_global_call( "SELECT xff FROM xff WHERE board = '%s' AND postno = %d", $board, $no ); + $reverse = gethostbyaddr( $post['host'] ); + + if( $xffresult = mysql_fetch_row( $xffquery ) ) { + if( !( $xff = gethostbyaddr( $xffresult[0] ) ) ) $xff = $xffresult[0]; + } + + if (isset($_POST['warn_req']) && $_POST['warn_req']) { + if (!$bres['can_warn']) { + error('You cannot issue warn requests using this template.'); + } + $warn_req = 1; + } + else if ($bres['days'] === '0') { + $warn_req = 1; + } + else { + $warn_req = 0; + } + + // Fixme: for the cache purger below + if ($post['ext'] != '') { + $post_filename = "{$post['tim']}{$post['ext']}"; + } + else { + $post_filename = null; + } + + // Make sure we don't have any illegal reports (stop illegal images being stored) + $illegal = mysql_global_call( "SELECT COUNT(*) FROM reports WHERE board='%s' AND no=%d AND cat=2", $board, $_POST['no'] ); + if ((mysql_result($illegal, 0, 0) == 0) && $bres['save_post'] === 'everything') { + $salt = file_get_contents( SALTFILE ); + $hash = sha1($board . $post['no'] . $salt); + + @copy( + IMG_DIR . "{$post['tim']}{$post['ext']}", + BANIMG_ROOT . "$board/$hash{$post['ext']}" + ); + + @copy( + THUMB_DIR . "{$post['tim']}s.jpg", + BANTHUMB_DIR . "{$hash}s.jpg" + ); + } + else { + //unset($post['ext']); + $post['raw_md5'] = $post['md5']; + } + + if ($post['resto']) { + $sub_query = mysql_board_call("SELECT sub FROM `%s` WHERE no = %d", $board, $post['resto']); + $sub_res = mysql_fetch_assoc($sub_query); + if ($sub_res) { + $rel_sub = $sub_res['sub']; + + if (strpos($rel_sub, 'SPOILER<>') === 0) { + $rel_sub = substr($rel_sub, 9); + } + + if ($rel_sub !== '') { + $post['rel_sub'] = $rel_sub; + } + } + } + + $tpl_name = $bres['name']; + $tpl_global = $bres['bantype'] !== 'local' ? 1 : 0; + + delete_post($no, false, $template, 'ban-req'); + + $res = mysql_global_call("INSERT INTO ban_requests SET host='%s', reverse='%s', pwd='%s', xff='%s', reason='', global = $tpl_global, tpl_name = '%s', ban_template='%s', board='%s', janitor='%s', spost='%s', post_json='%s', warn_req = %d", $post['host'], $reverse, $post['pwd'], $xff, $tpl_name, $template, $board, $janitor, serialize( $post ), json_for_post($board, $post), $warn_req); + + if (!$res) { + error('Database error.'); + } + + // Auto-rangebans processing (ban requests) + // FIXME: email field + $_post_meta = decode_user_meta($row['email']); + + if (!$warn_req && $_post_meta && $_post_meta['is_mobile']) { // mobile devices only + // global rules only + if ($bres && strpos($bres['rule'], 'global') !== false) { + process_auto_rangeban($post['host'], $_post_meta['browser_id'], $post['resto'], $post['no'], $bres['no'], 0); + } + } + + // Fixme, this is for the temporary is2/is3 cache purging api + if ($post_filename && $bres && $bres['rule'] == 'global1') { + purge_cache_internal_temp(BOARD_DIR, $post_filename); + } + echo ''; + die( ($warn_req ? 'Warn' : 'Ban') . ' request submitted! Window will now close...' ); + } + + $name = str_replace( '
!', ' !', $post[ 'name' ] ); + + $query = "SELECT warn_req, ban_templates.name FROM ban_requests LEFT JOIN ban_templates ON ban_template = ban_templates.no WHERE host='%s'"; + $result = mysql_global_call($query, $post['host']); + $brpending = array(); + while ($row = mysql_fetch_assoc($result)) { + $brpending[] = $row['name'] . ($row['warn_req'] ? ' [Warn]' : ''); + } + $brtooltip = join("\n", $brpending); + $pending = ''; + $brcount = count($brpending); + if ($brcount > 0) { + $plural = ($brcount > 1) ? 's' : ''; + $pending = << + Note + + [$brcount pending ban request$plural] + + +HTML; + } + + $csrf_tag = csrf_tag(); + + if (!preg_match('/Android|iPhone|iPad/', $_SERVER['HTTP_USER_AGENT'])) { + $autofocus_html = ' autofocus="autofocus"'; + } + else { + $autofocus_html = ''; + } + + $html = <<$csrf_tag + + + + + + + + + + + +$pending + + + + + + + + + + + + + + + + + + + + + +
Autocomplete + +
Template + +
Name + +
Reason + +
Warn? + +
Requested By + + + + +
+ +HTML; + + echo $html; + + $templates = array(); + $level_map = get_level_map(); + + $q = mysql_global_do("SELECT * FROM ban_templates ORDER BY length(rule), rule asc"); + + while( $r = mysql_fetch_assoc( $q ) ) { + if (!preg_match('#^(global|' . BOARD_DIR . ')[0-9]+$#', $r['rule'])) { + continue; + } + + if (($r['no'] == 1 || $r['no'] == 123 || $r['no'] == 213) && !$post_has_file) { + continue; + } + + if ($r['no'] == 6 && !DEFAULT_BURICHAN) { // NWS on Worksafe Board + continue; + } + + if ($r['no'] == 17 && (BOARD_DIR === 'mlp' || BOARD_DIR === 'trash')) { // Pony/Ponies Outside of /mlp/ + continue; + } + + if ($r['no'] == 222 && (BOARD_DIR === 's4s' || BOARD_DIR === 'bant')) { // Global 3 - Troll posts + continue; + } + + if ($r['no'] == 223 && BOARD_DIR === 'pol') { // Global 3 - Racism + continue; + } + + // Global 3 + if ((BOARD_DIR === 'b' || BOARD_DIR === 'bant') && strpos($r['rule'], 'global3') !== false) { + continue; + } + + if ($r['no'] == 59 && $post['resto']) { // Request Thread Outside of /r/ + continue; + } + + if ($level_map[$r['level']] !== true) { + continue; + } + + unset($r[ 'special_action' ], $r[ 'blacklist' ], $r[ 'bantype' ], $r[ 'postban' ], $r[ 'privatereason' ]); + + $templates[] = $r; + } + + $encTemp = json_encode( $templates ); + + $v = << + +HTML; + + echo $v; +} + +/* FIXME: this is for the temporary is2/is3 cache purge api */ +function purge_cache_internal_temp($board, $file) { + $url = "http://g0ch4.brazil.jp:24502"; + + $post = array(); + $post['rmpath'] = "/$board/$file"; + $post['key'] = '6a310437e13935b64beefcf10da8dba3'; + $post = http_build_query($post); + + rpc_start_request($url, $post, null, false); +} + +/** + * Sets or usnets the spoiler flag for images + * Does its own access validation. + * Accessible to janitors + */ +function admin_toggle_spoiler() { + header('Content-Type: text/plain'); + + if (!SPOILERS) { + echo '0'; die(); + } + + auth_user(); + + if (!has_level() && (!has_level('janitor') || !access_board(BOARD_DIR))) { + echo '-1'; die(); + } + + if (!isset($_GET['pid']) || !isset($_GET['flag'])) { + echo '0'; die(); + } + + $query = "SELECT * FROM `" . SQLLOG . "` WHERE no = %d"; + + $res = mysql_board_call($query, $_GET['pid']); + + if (!$res) { + echo '0'; die(); + } + + $post = mysql_fetch_assoc($res); + + if (!$post) { + echo '0'; die(); + } + + $spoiler_updated = adminToggleSpoiler($post, (bool)$_GET['flag']); + + if ($spoiler_updated) { + if ($post['resto']) { + $thread_id = (int)$post['resto']; + } + else { + $thread_id = (int)$post['no']; + } + + rebuild_thread($thread_id, $error, (bool)$post['archived']); + } + + echo '1'; die(); +} + +function validate_csrf($ref_only = false) { + if ($_SERVER['REQUEST_METHOD'] == 'POST' && !$ref_only) { + if (!isset($_COOKIE['_tkn']) || !isset($_POST['_tkn']) + || $_COOKIE['_tkn'] == '' || $_POST['_tkn'] == '' + || $_COOKIE['_tkn'] !== $_POST['_tkn']) { + + if (!is_local()) { + error('Bad Request.'); + } + } + } + else { + if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] != '' + && !preg_match('/^https?:\/\/([_a-z0-9]+)\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) { + error('Bad Request.'); + } + } +} + +/*-----------Main-------------*/ + +// Can't check for csrf token for this. Only check the referer. +validate_csrf($admin === 'delall'); + +switch($admin) { + case 'adminext': + adminvalid(); + adminExt(); + break; + + case 'banreq': + adminvalid( 'Ban Request' ); + adminBanReq(); + break; + case 'del': + adminvalid(); + //admin_delete(); + break; + case 'delall': + adminvalid(); + admindelall(); + break; + case 'delallbyip': + adminvalid(); + delallbyip( $_POST[ 'ip' ], $_POST[ 'imgonly' ] ); + break; + case 'ban': + adminvalid( 'Ban User' ); + adminban(); + break; + case 'opt': + adminvalid( 'Thread Options' ); + adminopt(); + break; + case 'spoiler': + admin_toggle_spoiler(); + break; + case 'cleanup': + adminvalid( 'Board Cleanup' ); + clean(); + break; + case 'cpban': + adminvalid(); + cpban((int)$_POST['no']); + break; + case 'protectthread': + adminvalid(); + admin_protect_thread(); + break; + case 'rev': + admin_reverse_ip(); + break; + /* + case 'reportqueue': + adminvalid(); + adminreportqueue(); + break; + case 'reportclear': + adminvalid(); + adminreportclear(); + break; + */ + default: + adminvalid(); + //admin_delete(); +} diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..ea04a68 --- /dev/null +++ b/admin.php @@ -0,0 +1,4388 @@ +17", $high ); + + return mysql_result( $howmany, 0, 0 ) - $high; +} + +function append_ban( $board, $ip ) +{ + // run in background + $cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $board $ip >/dev/null 2>&1 &"; + print "User banned from /$board/"; +// print $cmd . "
"; //disabling this because it's ugly and leaks filepaths + exec( $cmd ); +} + +function https_self_url() +{ + return "/".BOARD_DIR."/admin"; +} + +// for lib/admin.php +function delete_uploaded_files() +{ + +} + +function make_post_json($row) +{ + $nrow = array(); + + foreach( $row as $key => $val ) { + if( ctype_digit( $val ) || is_int( $val ) ) { + $val = (int)$val; + } + + $nrow[ $key ] = $val; + } + + return json_encode( $nrow ); +} + +function get_board_list() { + //mysql_global_call("SET character_set_results = 'utf8'"); + + $query = "SELECT dir, name FROM boardlist ORDER BY dir ASC"; + + $res = mysql_global_call($query); + + if (!$res) { + return array(); + } + + $boards = array(); + + while ($dir = mysql_fetch_row($res)) { + $boards[$dir[0]] = $dir[1]; + } + + return $boards; +} + +function is_board_valid($board, $allow_hidden = false) { + if (!$allow_hidden && ($board === 'test' || $board === 'j')) { + return false; + } + + $query = "SELECT dir FROM boardlist WHERE dir = '%s' LIMIT 1"; + $res = mysql_global_call($query, $board); + + if (!$res) { + return false; + } + + if (mysql_num_rows($res) === 1) { + return true; + } + + return false; +} + +function admin_clear_reports($board, $post_id) { + $query = "UPDATE reports SET cleared = 1 WHERE board = '%s' AND no = %d"; + + mysql_global_call($query, $board, $post_id); + + $query = << $ban_len) { + $spent_len = $ban_len; + } + } + else { + $spent_len = $ban_len; + } + + $recent_duration += $spent_len; + ++$recent_ban_count; + } + } + + if ($recent_duration) { + $recent_duration = round($recent_duration / 86400.0); + } + + return array( + 'total' => $total_count, + 'recent_bans' => $recent_ban_count, + 'recent_warns' => $recent_warn_count, + 'recent_days' => $recent_duration, + 'recent_permas' => $recent_perma_count + ); +} + +// Counts recently made threads by IP +function admin_get_thread_history($ip) { + $long_ip = ip2long($ip); + + if (!$long_ip) { + return false; + } + + $sql = "SELECT COUNT(*) FROM user_actions WHERE action = 'new_thread' AND ip = $long_ip AND time >= DATE_SUB(NOW(), INTERVAL 60 MINUTE)"; + + $res = mysql_global_call($sql); + + if (!$res) { + return false; + } + + return (int)mysql_fetch_row($res)[0]; +} + +function admin_hash_4chan_pass($pass) { + $salt = file_get_contents(SALTFILE); + + if (!$salt || !$pass) { + return ''; + } + + return sha1($pass . $salt); +} + +function get_ban_history_html($ban_summary, $host = false) { + $ban_tip = array(); + + if ($ban_summary['recent_bans'] > 0) { + $ban_tip[] = $ban_summary['recent_bans'] . ' ban' . ($ban_summary['recent_bans'] > 1 ? 's' : ''); + } + + if ($ban_summary['recent_warns'] > 0) { + $ban_tip[] = $ban_summary['recent_warns'] . ' warning' . ($ban_summary['recent_warns'] > 1 ? 's' : ''); + } + + if ($ban_summary['recent_days'] > 0) { + $ban_tip[] = $ban_summary['recent_days'] . ' day' + . ($ban_summary['recent_days'] > 1 ? 's' : '') + . ' spent banned'; + } + + if ($ban_summary['recent_permas'] > 0) { + $ban_tip[] = $ban_summary['recent_permas'] . ' permaban' . ($ban_summary['recent_permas'] > 1 ? 's' : ''); + } + + $ban_tip = "Past 12 months history
  • " . implode('
  • ', $ban_tip) . '
'; + + if ($host !== false) { + return "
$ban_tip
[ {$ban_summary['total']} ban" . + (($ban_summary['total'] > 1) ? 's' : '') . " for this IP ]"; + } + else { + return "
$ban_tip
[ {$ban_summary['total']} ban" . + (($ban_summary['total'] > 1) ? 's' : '') . " for this Pass ]"; + } +} + +function ban_post( $no, $globalban, $length, $reason, $is_threadban = 0 ) +{ + $query = mysql_board_call( "SELECT HIGH_PRIORITY * FROM `" . SQLLOG . "` WHERE no=" . intval( $no ) ); //FIXME use assoc + $row = mysql_fetch_assoc( $query ); + if( !$row ) return ""; + extract( $row, EXTR_OVERWRITE ); + + //list( $no, $sticky, $permasage, $closed, $now, $name, $email, $sub, $com, $host, $pwd, $filename, $ext, $w, $h, $tn_w, $tn_h, $tim, $time, $md5, $fsize, $root, $resto ) = $row; + $name = str_replace( '
!', ' #', $name ); + $name = preg_replace( '/<[^>]+>/', '', $name ); // remove all remaining html crap + + if( $host ) $reverse = gethostbyaddr( $host ); + $displayhost = ( $reverse && $reverse != $host ) ? "$reverse ($host)" : $host; + $xff = ''; + + //$xffresult = mysql_board_call("select host from xff where board='%s' and postno=%d", BOARD_DIR, $no); + //$xffresult = mysql_global_call( "SELECT xff from xff where board='%s' AND postno='%d'", BOARD_DIR, $no ); + //if( $xffrow = mysql_fetch_row( $xffresult ) ) { + // $xff = $xffrow[ 0 ]; + // $xff_reverse = gethostbyaddr($xffrow[0]); + // $xff = ($xff_reverse && $xff_reverse!=$xffrow[0])?"$xff_reverse ($xff)":$xff; + //} + $board = BOARD_DIR; + $zonly = 0; + + $bannedby = $_COOKIE[ '4chan_auser' ]; + $pass_id = $row[ '4pass_id' ]; + $post_json = make_post_json($row); + + $result = mysql_global_do( + "INSERT INTO " . SQLLOGBAN . " + (board,global,zonly,name,host,reverse,xff,reason,length,admin,md5,4pass_id,post_num,post_time,post_json,admin_ip) + VALUES + ( '%s', %d, %d, '%s', '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', %d, FROM_UNIXTIME(%d), '%s', '%s')", + $board, $globalban, $zonly, $name, $host, $reverse, $xff, $reason, $length, $bannedby, $md5, $pass_id, $no, $time, $post_json, $_SERVER['REMOTE_ADDR'] ); + + //('" . $board . "','" . $globalban . "','" . $zonly . "','" . mysql_escape_string( $name ) . "','" . $host . "','" . mysql_escape_string( $reverse ) . "','" . mysql_escape_string( $xff ) . "','" . mysql_escape_string( $reason ) . "','$length','" . mysql_escape_string( $bannedby ) . "','$md5','$pass_id',$no,FROM_UNIXTIME('$time'), '%s')", $post_json ) ) { + if( !$result ) { + echo S_SQLFAIL; + } + + /*if( $ext != '' ) { + $salt = file_get_contents( SALTFILE ); + $hash = sha1( BOARD_DIR . $no . $salt ); + @copy( THUMB_DIR . "{$tim}s.jpg", BANTHUMB_DIR . "{$hash}s.jpg" ); + }*/ + /* + $afsize = (int)( $fsize > 0 ); + validate_admin_cookies(); + if( $is_threadban ) mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,admin_ip) values('0',%d,%d,'%s','%s','%s','%s','%d','%s','%s','%s')", $no, $resto, SQLLOG, $name, $sub, $com, $afsize, $filename.$ext, $bannedby, $_SERVER['REMOTE_ADDR'] ); // FIXME do all this in one insert outside the write lock + */ + echo "$displayhost banned.
\n"; + + return $host; +} + +function cpban($no) { + $no = (int)$no; + + if (!$no) { + die('Invalid thread number.'); + } + + $op_reason = htmlspecialchars($_POST['op_reason']); + $rep_reason = htmlspecialchars($_POST['rep_reason']); + + if (!$op_reason || !$rep_reason) { + die('Ban reason cannot be empty.'); + } + + $op_days = (int)$_POST['op_days']; + $rep_days = (int)$_POST['rep_days']; + + if ($op_days < 0 || $rep_days < 0 || $op_days > 9999 || $rep_days > 9999) { + die('Invalid ban length.'); + } + + $op_ban_end = date('YmdHis', time() + $op_days * (24 * 60 * 60)); + $rep_ban_end = date('YmdHis', time() + $rep_days * (24 * 60 * 60)); + + $op_host = ban_post($no, 1, $op_ban_end, "$op_reason<>Thread Ban No.$no", 1); + + if (!$op_host) { + die("Thread $no doesn't exist."); + } + + $query = mysql_board_call("SELECT no, host FROM `" . SQLLOG . "` WHERE resto = $no AND host != '$op_host' GROUP BY host"); + + while ($row = mysql_fetch_assoc($query)) { + ban_post($row['no'], 1, $rep_ban_end, "$rep_reason<>Thread Ban No.$no", 1); + } + + delete_post($no, false, null, 'threadban'); + + echo 'Done.
'; +} + +function delete_post($no, $imgonly, $template_id = null, $tool = null) { + $url = "/".BOARD_DIR."/post"; + + $post = array( + 'mode' => 'usrdel', + 'onlyimgdel' => $imgonly ? 'on' : '', + $no => 'delete', + 'remote_addr' => $_SERVER['REMOTE_ADDR'] + ); + + if ($template_id) { + $post['template_id'] = $template_id; + } + + if ($tool) { + $post['tool'] = $tool; + } + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + // don't bother waiting to check for errors + + return true; +} + +function archive_thread($thread_id) { + $url = "/".BOARD_DIR."/post"; + + $post = array( + 'mode' => 'forcearchive', + 'id' => $thread_id + ); + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + // don't bother waiting to check for errors + + return true; +} + +function move_thread($thread_id, $board) { + $url = "/".BOARD_DIR."/post"; + + $post = array( + 'mode' => 'movethread', + 'id' => $thread_id, + 'board' => $board + ); + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + // don't bother waiting to check for errors + + return true; +} + +function rebuild_thread($no, &$error = '', $is_archived = false) { + $url = '/' . BOARD_DIR . '/post'; + + if (!$is_archived) { + $post = array( + 'mode' => 'rebuildadmin', + 'no' => $no + ); + } + else { + $post = array(); + $post['mode'] = 'rebuild_threads_by_id'; + $post['ids'] = array($no); + $post = http_build_query($post); + } + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + return true; +} + +function rebuild_all(&$error = '') { + $url = '/' . BOARD_DIR . '/post'; + + $post = array( + 'mode' => 'rebuildall' + ); + + rpc_start_request("https://sys.int$url", $post, $_COOKIE, true); + + return true; +} + +function dir_contents( $dir ) +{ + $d = opendir( $dir ); + $a = array(); + if( !$d ) return $a; + + while( ( $f = readdir( $d ) ) !== false ) { + if( $f == "." || $f == ".." || $f == "" ) continue; + $a[ ] = $f; + } + + closedir( $d ); + + return $a; +} + +function clean() +{ + // Survive oversized boards. + set_time_limit(0); + ini_set("memory_limit", "-1"); + + $images = array(); + $respages = array(); + $indexpages = array(); + + if( PAGE_MAX > 0 ) { + print "Running cleanup...
Pruning orphaned posts...
"; + $result = mysql_board_call( "select no from `%s` where resto>0 and resto not in (select no from `%s` where resto=0)", SQLLOG, SQLLOG ); + $nos = mysql_column_array( $result ); + if( count( $nos ) ) { + mysql_board_call( "delete from `" . SQLLOG . "` where no in (%s)", implode( $nos, "," ) ); + foreach( $nos as $no ) { + print "$no pruned
"; + } + } + } + + //clearstatcache(); + + // get list of images that should exist + if (MOBILE_IMG_RESIZE) { + $cols = ',m_img'; // FIXME, only because not all boards have that column + } + $result = mysql_board_call( "select tim,filename,ext$cols from `" . SQLLOG . "` where ext != ''" ); + while( $row = mysql_fetch_array( $result ) ) { + if( $row[ 'ext' ] == '.swf' ) { + $images[ "{$row[ 'filename' ]}{$row[ 'ext' ]}" ] = 1; + } + else { + $images[ "{$row[ 'tim' ]}{$row[ 'ext' ]}" ] = 1; // picture + $images[ "{$row[ 'tim' ]}s.jpg" ] = 1; // thumb + + if (ENABLE_OEKAKI_REPLAYS) { + $images["{$row['tim']}.tgkr"] = 1; // oe animation + } + + if (MOBILE_IMG_RESIZE) { + $images["{$row['tim']}m.jpg"] = 1; // resized + } + } + } + + // get list of res pages that should exist + $result = mysql_board_call( "select no from `" . SQLLOG . "` where resto=0" ); + while( $row = mysql_fetch_array( $result ) ) { + if( USE_GZIP == 1 ) { + $respages[ "{$row[ 'no' ]}.html.gz" ] = 1; + + if (ENABLE_JSON) { + $respages[$row['no'] . '.json.gz'] = 1; + + if (JSON_TAIL_SIZE) { + $respages[$row['no'] . '-tail.json.gz'] = 1; + } + } + } + else { + $respages[ "{$row[ 'no' ]}.html" ] = 1; + + if (ENABLE_JSON) { + $respages[$row['no'] . '.json'] = 1; + + if (JSON_TAIL_SIZE) { + $respages[$row['no'] . '-tail.json'] = 1; + } + } + } + + if( JANITOR_BOARD ) $respages[ $row[ 'no' ] . '.html.php' ] = 1; + } + + print "Cleaning src dir...
"; + foreach( dir_contents( IMG_DIR ) as $filename ) { + if( $images[ $filename ] != 1 && !preg_match('/dmca_/', $filename) && $filename !== 'src') { + print "Deleted $filename
"; + //if (file_exists(IMG_DIR . "$filename")) { + unlink(IMG_DIR . "$filename") or print "Couldn't delete!
"; + //} + } + } + + print "Cleaning thumb dir...
"; + foreach( dir_contents( THUMB_DIR ) as $filename ) { + if( $images[ $filename ] != 1 && !preg_match('/dmca_/', $filename)) { + print "Deleted $filename
"; + //if (file_exists(THUMB_DIR . "$filename")) { + unlink(THUMB_DIR . "$filename") or print "Couldn't delete!
"; + //} + } + } + + print "Cleaning res dir...
"; + foreach( dir_contents( RES_DIR ) as $filename ) { + if( $respages[ $filename ] != 1 ) { + print "Deleted $filename
"; + unlink( RES_DIR . "$filename" ) or print "Couldn't delete!
"; + } + } + + print "Cleaning index pages...
"; + $result = mysql_board_call( "SELECT COUNT(*) from `" . SQLLOG . "` WHERE archived = 0 AND resto = 0" ); + $lastpage = PAGE_MAX + 1;//(mysql_result( $result, 0, 0 ) / DEF_PAGES) + 1; + if( USE_GZIP == 1 ) { + $indexpages[ SELF_PATH2_FILE . '.gz' ] = 1; + + if (USE_RSS) { + $indexpages[INDEX_DIR . 'index.rss.gz'] = 1; + } + if (ENABLE_CATALOG) { + $indexpages[INDEX_DIR . 'catalog.html.gz'] = 1; + } + if (ENABLE_JSON_CATALOG) { + $indexpages[INDEX_DIR . 'catalog.json.gz'] = 1; + } + if (ENABLE_JSON_THREADS) { + $indexpages[INDEX_DIR . 'threads.json.gz'] = 1; + $indexpages[INDEX_DIR . 'archive.json.gz'] = 1; + } + if (ENABLE_ARCHIVE) { + $indexpages[INDEX_DIR . 'archive.html.gz'] = 1; + } + } + + $indexpages[ SELF_PATH2_FILE ] = 1; + + if (USE_RSS) { + $indexpages[INDEX_DIR . 'index.rss'] = 1; + } + if (ENABLE_CATALOG) { + $indexpages[INDEX_DIR . 'catalog.html'] = 1; + } + if (ENABLE_JSON_CATALOG) { + $indexpages[INDEX_DIR . 'catalog.json'] = 1; + } + if (ENABLE_JSON_THREADS) { + $indexpages[INDEX_DIR . 'threads.json'] = 1; + $indexpages[INDEX_DIR . 'archive.json'] = 1; + } + if (ENABLE_ARCHIVE) { + $indexpages[INDEX_DIR . 'archive.html'] = 1; + } + + for( $page = 1; $page < $lastpage; $page++ ) { + if( USE_GZIP == 1 ) { + $indexpages[ INDEX_DIR . $page . PHP_EXT . '.gz' ] = 1; + if (ENABLE_JSON_INDEXES) { + $indexpages[INDEX_DIR . $page . '.json.gz'] = 1; + } + } + $indexpages[ INDEX_DIR . $page . PHP_EXT ] = 1; + if (ENABLE_JSON_INDEXES) { + $indexpages[INDEX_DIR . $page . '.json'] = 1; + } + } + + foreach( glob( INDEX_DIR . '*.{html,gz}', GLOB_BRACE ) as $filename ) { + $bfilename = basename( $filename ); + if( $indexpages[ $filename ] != 1 ) { + print "Deleted $bfilename
"; + unlink( $filename ) or print "Couldn't delete!
"; + } + } + + print "Cleaning tmp uploads...
"; + $phptmp = ini_get( "upload_tmp_dir" ); + exec( "find $phptmp/ -mtime +2h -name php*", $tmpfiles ); + exec( "find -E " . INDEX_DIR . " -regex '.*/(gz)?tmp.*$' -mtime +2h", $indextmp ); + exec( "find -E " . RES_DIR . " -regex '.*/(gz)?tmp.*$' -mtime +2h", $restmp ); + + $tmpfiles = array_merge( $tmpfiles, $indextmp ); + $tmpfiles = array_merge( $tmpfiles, $restmp ); + + foreach( $tmpfiles as $filename ) { + $safename = explode( '/' . BOARD_DIR . '/', $filename ); + $safename = end( $safename ); + + print "Deleted $safename
"; + unlink( $filename ) or print "Couldn't delete!
"; + } + /*print "Cleaning /var/tmp
"; + exec("find /var/tmp/ -mtime +2h -type f", $tmpfiles); + foreach($tmpfiles as $filename) { + print "Delete $filename
"; unlink($filename) or print "Couldn't delete!
"; + }*/ + print "Cleaning up side tables...
"; + + mysql_global_call( "DELETE FROM user_actions WHERE time < DATE_SUB(NOW(), INTERVAL 7 DAY)" ); + mysql_global_call( "DELETE FROM event_log WHERE created_on < DATE_SUB(NOW(), INTERVAL 7 DAY)" ); + mysql_global_call( "DELETE FROM xff WHERE tim < (unix_timestamp(DATE_SUB(NOW(), INTERVAL 7 DAY))*1000)" ); + mysql_board_call( "DELETE FROM f_md5 WHERE now < DATE_SUB(NOW(), INTERVAL 2 DAY)" ); + mysql_board_call("DELETE FROM r9k_posts WHERE created_on < DATE_SUB(NOW(), INTERVAL 2 YEAR)"); + + print "Cleanup complete!"; +} + +// Changes relative board urls to absolute //sys.4chan.org admin urls +function fix_board_nav($nav) { + return preg_replace('/href="\/([a-z0-9]+)\/"/', "href=\"//sys." . L::d(BOARD_DIR) . "/$1/admin\"", $nav); +} + +/* head */ +function head( &$dat, $is_logged_in = false ) +{ + global $admin, $access_allow, $access_deny; + + $allowed_modes = array('ban', 'delall', 'unban', 'opt', 'banreq', 'editop'); + + if( !is_user() || ( is_user() && ( $admin != "ban" ) && ( $admin != "delall" ) && ( $admin != "unban" ) && ( $admin != "opt" ) && ( $admin != 'banreq' ) && ( $admin != 'editop' ) ) ) { + $navinc = fix_board_nav(file_get_contents( NAV_TXT )) . '
'; + $navinc = str_replace( '[Settings] ', '', $navinc ); + } + + if (DEFAULT_BURICHAN) { + $style_cookie = 'ws_style'; + $ws = 'ws'; + } + else { + $style_cookie = 'nws_style'; + $ws = ''; + } + + $preferred_style = $_COOKIE[$style_cookie]; + + switch ($preferred_style) { + case 'Yotsuba New': + $style = 'yotsubanew'; + break; + case 'Yotsuba B New': + $style = 'yotsubluenew'; + break; + //case 'Futaba New': + // $style = 'futabanew'; + // break; + //case 'Burichan New': + // $style = 'burichannew'; + // break; + case 'Tomorrow': + $style = 'tomorrow'; + break; + case 'Photon': + $style = 'photon'; + break; + default: + $style = DEFAULT_BURICHAN ? 'yotsubluenew' : 'yotsubanew'; + break; + } + + if (!in_array($admin, $allowed_modes)) { + $admin = ''; + } + + if ($admin == 'ban') { + $page_title = 'Ban No.' . (int)$_GET['id'] . ' on /' . BOARD_DIR . '/'; + $no_header = true; + } + else if ($admin == 'banreq') { + $page_title = 'Ban request No.' . (int)$_GET['id'] . ' on /' . BOARD_DIR . '/'; + $no_header = true; + } + else { + $page_title = TITLE; + $no_header = isset($_GET['noheader']); + } + + $fb_js = <<'; + + document.body.insertBefore(el, document.body.firstElementChild); + }, + + hideMessage: function() { + var el = document.getElementById('feedback'); + + if (el) { + document.body.removeChild(el); + } + }, + + checkTemplate: function(id) { + var tpl; + + Feedback.hideMessage(); + + if (id < 0) { + return; + } + + tpl = window.templates[id]; + + if (tpl.no == '1') { + Feedback.showMessage('Only use this ban template for images depicting apparent child pornography. For links and non-pornographic images, please use the appropriate template(s).'); + } + else if (tpl.no == '123' || tpl.no == '126') { + Feedback.showMessage('Images depicting apparent child pornography should be banned using the "Child Pornography (Explicit Image)" template.'); + } + } +}; +JS; + +$tooltip_js = << document.documentElement.clientWidth) { + left = rect.left - el.offsetWidth + t.offsetWidth + 2; + el.className += '-left'; + } + + top = rect.top - el.offsetHeight - 5; + + style = el.style; + style.display = 'none'; + style.top = (top + window.pageYOffset) + 'px'; + style.left = left + window.pageXOffset + 'px'; + style.display = ''; + + Tip.node = el; + }, + + hide: function() { + if (Tip.node) { + document.body.removeChild(Tip.node); + Tip.node = null; + } + } +} + +Tip.init(); +JS; + + $dat .= ' + + + + + + + + +' . $page_title . ' + + +'; + + if (!$no_header) { + $dat .= ' +' . str_replace( "12pt", "10pt", $navinc ) . ' +
' . TITLE . '
+
'; + } +} + +/* Footer */ +function foot( &$dat ) +{ + $dat .= ' +
+' . S_FOOT . ' + +
wtf? +' . str_replace( "12pt", "10pt", $navinc2 ) . ' +'; +} + +function error( $mes, $dest = '' ) +{ + global $upfile_name; + if ($dest && file_exists($dest)) { + unlink($dest); + } + head( $dat ); + echo $dat; + echo "

+
$mes

[" . S_RELOAD . "]
"; + die( "" ); +} + +/* text plastic surgery */ +function sanitize_text( $str ) +{ + global $admin; + $str = trim( $str ); //blankspace removal + if( get_magic_quotes_gpc() ) { //magic quotes is deleted (?) + $str = stripslashes( $str ); + } + if( $admin != $adminpass ) { //admins can use tags + $str = htmlspecialchars( $str ); //remove html special chars + $str = str_replace( "&", "&", $str ); //remove ampersands + } + + return str_replace( ",", ",", $str ); //remove commas +} + +//check for table existance +function table_exist( $table ) +{ + $result = mysql_global_call( "show tables like '$table'" ); + if( !$result ) { + return 0; + } + $a = mysql_fetch_row( $result ); + + return $a; +} + +function is_local() +{ + if (!isset($_SERVER['REMOTE_ADDR'])) { + return true; + } + + // local rpc can do anything + $longip = ip2long( $_SERVER['REMOTE_ADDR'] ); + + if( + cidrtest( $longip, "10.0.0.0/24" ) || + cidrtest( $longip, "204.152.204.0/24" ) || + cidrtest( $longip, "127.0.0.0/24" ) + ) { + return true; + } + + return false; +} + +// FIXME hack +function valid( $action = 'moderator', $no = 0 ) +{ + return false; +} + +/*password validation */ +function adminvalid( $title = 'Manager Mode' ) +{ + global $user, $pass, $access_allow, $access_deny, $admin; + + $level = 0; + $levelarr = array( 'janitor' => 1, 'mod' => 2, 'manager' => 3, 'admin' => 4 ); + + ob_start(); + + // 1 = janitor, 2 = mod, 3 = manager, 4 = admin + + if( is_local() ) { + echo head( $dat ); + + return; + } + + $user = $_COOKIE[ '4chan_auser' ]; + $pass = $_COOKIE[ '4chan_apass' ]; + + $valid = auth_user(); + + if( $valid !== true ) { + error( 'You do not have permission to access this page.' ); + } + + //if( !$valid ) admin_login_fail(); + + + // Do we have permission for this board? + if( $valid && ( $title !== 'Ban Request' && !access_board( BOARD_DIR ) ) ) { + error( 'You do not have permission to access this board.' ); + + } + + if ($title !== 'Ban Request' && !has_level('mod')) { + die(); + } + + if ($title === 'Board Cleanup' && !has_level('manager') && !has_flag('developer')) { + error( 'You do not have permission to access this board.' ); + } + + if( $valid && has_level() && $_GET[ 'admin' ] == 'adminext' ) { + return true; + } + + + head( $dat, $valid ); + echo $dat; + + $SELF_PATH2_ABS = SELF_PATH2_ABS; + $S_RETURNS = S_RETURNS; + $SELF_PATH = SELF_PATH; + $S_LOGUPD = S_LOGUPD; + $S_LOGUPDALL = S_LOGUPDALL; + if (!isset($_GET['noheader']) && $title == 'Manager Mode') { + + if( $valid && has_level( 'mod' ) ) { + echo '
'; + echo '[' . S_RETURNS . '] [' . S_LOGUPD . '] [' . S_LOGUPDALL . ']'; + + if( $_GET[ 'admin' ] == 'cleanup' ) { + echo ' [Admin]'; + } + else if (has_level('manager') || has_flag('developer')) { + echo ' [Cleanup]'; + } + + if( $_GET[ 'admin' ] == 'ban' ) { + echo ' [Rules]'; + } + + echo ''; + } + elseif( $valid ) { + //echo ' [Rules] '; + } + + if( ( !isset( $_GET[ 'admin' ] ) || $_GET[ 'admin' ] == 'cleanup' ) && has_level( 'mod' ) ) { + echo ""; + } + } + + $no_header = isset($_GET['noheader']) || $_GET[ 'admin' ] == 'ban' || $_GET[ 'admin' ] == 'banreq'; + + if( $valid && !has_level( 'mod' ) ) { + if ($no_header) { + return; + } + echo "

+
You are logged in as a janitor.

[Return]
"; + die( '' ); + } + + //if( $valid && (has_level('manager') || has_flag('developer')) ) { + $GLOBALS[ 'b_sticky' ] = 1; + //} + + if( !$valid ) $title = 'Manager Mode'; + if( !$valid ) echo ''; + if( !$no_header ) echo '
' . $title . '
'; + // Mana login form + if( !$valid ) { + echo "
\n"; + + echo "
"; + echo "\n"; + echo ""; + // echo ""; + echo ""; + echo ""; + echo "
ID(s):
Username
Password
\n"; + //echo file_get_contents( NAV2_TXT ); + echo ''; + + + die(); + } +} + +// FIXME +function adminreportclear() { + if (!has_level('mod')) { + die('404'); + } + + $pid = (int)$_GET['pid']; + + $board = $_GET['board']; + + if (!$pid || !$board) { + die('404'); + } + + $query = "UPDATE reports SET cleared = 1, cleared_by = '%s' WHERE board = '%s' AND no = $pid"; + + $res = mysql_global_call($query, $_COOKIE['4chan_auser'], $board); + + if (!$res) { + die("DB error (2-1)"); + } + + $query = <<'; + if ($post['fsize'] > 0) { + echo ''; + } + + $tid = $post['resto'] ? $post['resto'] : $report['no']; + + echo "/{$report['board']}/{$report['no']} ({$report['total_weight']}) — "; + + echo '[CLEAR]'; + + echo "

{$post['com']}

"; + + echo "
"; + } +} + +/* Admin deletion */ +// This might not be used anymore +function admin_delete() +{ + if( !has_level('mod') ) return true; + + global $admin, $onlyimgdel, $res, $thread, $ip, $user, $pass; + + if( ( $admin != "ban" ) && ( $admin != "delall" ) && ( $admin != "unban" ) ) { + $navinc = ''; //file_get_contents( NAV_TXT ); + } + if( !isset( $_POST[ 'p' ] ) ) { + $p = 1; + } + else { + $p = $_POST[ 'p' ]; + } + $max_results = 30; + $from = ( ( $p * $max_results ) - $max_results ); + + $board = explode( "/", $_SERVER[ 'SCRIPT_NAME' ] ); + $board = $board[ 1 ]; + + $threadmode = $_REQUEST[ 'threadmode' ]; + if( !$threadmode ) { // threadmode uses table aliases, so don't bother locking + if( $delflag ) mysql_board_call( "LOCK TABLES `" . SQLLOG . "` WRITE" ); + } + + $delno = array(); + $delflag = false; + reset( $_POST ); + while( $item = each( $_POST ) ) { + if( $item[ 1 ] == 'delete' ) { + array_push( $delno, intval( $item[ 0 ] ) ); + $delflag = true; + } + } + if( $delflag ) { + $resultstr = "(" . implode( ",", $delno ) . ")"; + if( $threadmode ) mysql_board_call( "LOCK TABLES `" . SQLLOG . "` WRITE" ); // can finally lock it now + if( !$result = mysql_board_call( "select * from `" . SQLLOG . "` where no in %s or resto in %s", $resultstr, $resultstr ) ) { + echo S_SQLFAIL; + } //FIXME use assoc + $find = false; + while( $row = mysql_fetch_assoc( $result ) ) { + //list( $no, $sticky, $permasage, $closed, $now, $name, $email, $sub, $com, $host, $pwd, $filename, $ext, $w, $h, $tn_w, $tn_h, $tim, $time, $md5, $fsize, $root, $resto ) = $row; + extract( $row, EXTR_OVERWRITE ); + + if( $onlyimgdel == 'on' ) { + if( array_search( $no, $delno ) ) { //only a picture is deleted + if( $board == "f" ) { + $delfile = IMG_DIR . $filename . $ext; + } + else { + $delfile = IMG_DIR . $tim . $ext; //only a picture is deleted + } + unlink( $delfile ); //delete + unlink( THUMB_DIR . $tim . 's.jpg' ); //delete + } + } + else { + if( array_search( $no, $delno ) || array_search( $resto, $delno ) ) { //It is empty when deleting + $find = true; + $auser = $_COOKIE[ '4chan_auser' ]; + $apass = $_COOKIE[ '4chan_apass' ]; + if( !mysql_board_call( "delete from `" . SQLLOG . "` where no=" . $no . " or resto=" . $no ) ) { + echo S_SQLFAIL; + } // FIXME can't this be atomic? (one statement) + if( $board == "f" ) { + $delfile = IMG_DIR . $filename . $ext; + } + else { + $delfile = IMG_DIR . $tim . $ext; + } + unlink( $delfile ); //Delete + unlink( THUMB_DIR . $tim . 's.jpg' ); //Delete + if( $fsize > 0 ) $adfsize = 1; + $adname = str_replace( '
!', '#', $name ); + if( $onlyimgdel == "on" ) { + $imgonly = 1; + } + else { + $imgonly = 0; + } + validate_admin_cookies(); + mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,admin_ip) values('$imgonly','$no',$resto,'" . SQLLOG . "','$adname','$sub','$com','$adfsize','$filename$ext','$auser', '" . mysql_real_escape_string($_SERVER['REMOTE_ADDR']) . "')" ); // FIXME do all this in one insert outside the write lock + } + } + } + } + + if( $delflag ) mysql_board_call( "UNLOCK TABLES" ); + + // Deletion screen display + echo "
\n"; + echo "\n"; + echo "Go to ID(s):  "; + if( $threadmode ) { + echo ""; + } + else { + echo ""; + } + + echo "
"; + + echo "

"; + echo ""; + echo " [" . S_MDONLYPIC . "]"; + + echo ""; + if( $threadmode ) { + echo ""; + } + else { + echo ""; + } + echo ''; + echo "\n"; + + $resq = "`"; + if( $res ) { + $resq = ""; + $splitres = explode( ",", $res ); + foreach( $splitres as $line ) { + $resq .= " no='" . mysql_escape_string( $line ) . "' OR"; + } + $resq = rtrim( $resq, " OR" ); + $resq = "` WHERE" . $resq; + + } + elseif( $thread && $ip ) { + $max_results = 5000; + $thread = (int)$thread; + $resq = "` WHERE (no='$thread' OR resto='$thread') AND host='" . sprintf( "%s", long2ip( -( 4294967296 - $ip ) ) ) . "'"; + } + elseif( $ip ) { + $max_results = 5000; + $resq = "` WHERE host='" . sprintf( "%s", long2ip( -( 4294967296 - $ip ) ) ) . "'"; + } + elseif( $thread ) { + $max_results = 5000; + $thread = (int)$thread; + $resq = "` WHERE no='$thread' OR resto='$thread'"; + } + + if( $threadmode ) { + if( !$result = mysql_board_call( "(SELECT child.*,parent.root proot from `" . SQLLOG . "` child LEFT OUTER JOIN `" . SQLLOG . "` parent ON child.resto=parent.no) UNION (SELECT *,root proot from `" . SQLLOG . "` parent WHERE resto=0) ORDER BY proot DESC, no ASC LIMIT " . $from . ", " . $max_results ) ) { + echo S_SQLFAIL; + } + } + else { + if( !$result = mysql_board_call( "select * FROM `" . SQLLOG . $resq . " order BY no DESC LIMIT " . $from . ", " . $max_results ) ) { + echo S_SQLFAIL; + } + } + + $j = 0; + while( $row = mysql_fetch_assoc( $result ) ) { //FIXME use assoc + $j++; + $img_flag = false; + extract( $row, EXTR_OVERWRITE ); + // Format + //$now=preg_replace('@^(../..)/..@','$1',$now); + //$now=preg_replace('/\(.*\)/',' ',$now); + $fullname = str_replace( '!', ' #', $name ); + $name = strip_tags( $name ); + $fullname = strip_tags( $name ); //for capcode cleaning + + if( strpos( $sub, 'SPOILER<>' ) !== false ) $sub = substr( $sub, 9 ); + + $fullsub = $sub; + if( strlen( $name ) > 14 ) $name = substr( $name, 0, 15 ) . "..."; + if( strlen( $sub ) > 14 ) $sub = substr( $sub, 0, 15 ) . "..."; + //if( $email ) $name = "$name"; + $shortcom = html_entity_decode( preg_replace( "/<[^>]+>/", " ", $com ), ENT_QUOTES, "UTF-8" ); + if( strlen( $shortcom ) > 35 ) $shortcom = substr( $shortcom, 0, 36 ) . "..."; + // Link to the picture + if( $ext ) { + $img_flag = true; + if( !$filedeleted ) { + if( SQLLOG == "f" ) { + $filelink = $filename . $ext; + } + else { + $filelink = $tim . $ext; + } + $clip = "" . $filelink . ""; + } + else { + $clip = "" . $filelink . ""; + } + $size = $fsize / 1024; + $size = round( $size, 2 ) . " KB"; + $all = $all + $fsize; //total calculation + $md5 = substr( $md5, 0, 10 ); + } + else { + $clip = ""; + $size = 0; + $md5 = ""; + } + $bg = ( $j % 2 ) ? "d0d0f0" : "f6f6f6"; //BG color + if( $threadmode ) { + $bg = ( !$resto ) ? "d0f0d0" : "eeffee"; + } + + $displayhost = $host; + + $cboard = explode( "/", $_SERVER[ 'SCRIPT_NAME' ] ); + $cboard = $cboard[ 1 ]; + + $bantrue = 0; + if( !$banned = mysql_global_call( "SELECT host,board,global,zonly,DATE_FORMAT(length, 'Until %W, %M %D, %Y.') AS buntil FROM " . SQLLOGBAN . " WHERE host='" . $host . "' AND active=1" ) ) { + echo S_SQLFAIL; + } + $bannedrows = mysql_num_rows( $banned ); + if( $bannedrows > 0 ) { + $row = mysql_fetch_array( $banned ); + $buntil = $row[ 'buntil' ]; + if( $row[ 'board' ] == $cboard ) { + $bg = "f0d0d0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + $bantrue = 1; + } + if( $row[ 'global' ] == 1 ) { + $bg = "f0a0a0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + //$globally = " (Globally)"; + $bantrue = 1; + } + elseif( $row[ 'zonly' ] == 1 ) { + $bg = "a0f0a0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + $bantrue = 0; + //$globally = " (".$board.")"; + } + else { + //$globally = " (".$board.")"; + } + } + + echo ""; + if( $resto == 0 ) { + $spec = ""; + if( $sticky == 1 ) $spec = " color: #800080;"; + if( $permasage == 1 ) $spec = " text-decoration: underline;"; + if( $closed == 1 ) $spec = " color: #FF0000;"; + if( $sticky == 1 && $closed == 1 ) $spec = " color: #808080;"; + + echo ""; + } + else { + $parentline = ( $threadmode ? "└" : "" ); + echo ""; + } + echo ""; + echo ""; + echo "\n"; + echo "\n"; + echo ""; + // echo ""; + + + if( $size != 0 ) { + echo ""; + echo ""; + echo ""; + echo ""; + // echo ""; + echo ""; + } + elseif( $resto == 0 ) { + //echo ""; + //echo "'; + echo ''; + } + else { + echo ""; + } + + echo ""; + echo ""; + $span = 8; + } + else { + echo ""; + echo ""; + echo ""; + echo ""; + //echo ""; + echo ""; + $span = 5; + } + echo ""; + } else {*/ + // if (!$bantrue) { + echo "    "; + // } + //} + if( $resto == 0 ) { + echo "    "; + if( !$thread ) { + echo "    "; + } + } + echo ""; + } + + echo "
 No.TimeNameSubjectCommentHost 
$no$parentline$no$now$name$sub$shortcom$displayhost
 File 
 $clip ($size)Text-only thread"; + if( $bannedrows > 0 ) { + echo 'Text-only threadBan lengthText-only thread
Reply to thread 
 $resto    "; + /*if ($bantrue) { + echo "    

"; + echo ""; + echo " [" . S_MDONLYPIC . "]


"; + + //$all = (int)($all / 1024); + + //page stuff + $total_results = mysql_result( mysql_board_call( "SELECT COUNT(*) as Num FROM `" . SQLLOG . "`" ), 0 ); + $total_pages = ceil( $total_results / $max_results ); + + echo "
\n"; + echo "\n"; + + echo '
"; + if( $p < $total_pages ) { + $next = ( $p + 1 ); + echo "
\n"; + echo "\n"; + echo "
"; + } + else { + echo "Next"; + } + echo "
"; + echo "
"; + echo str_replace( "12pt", "10pt", $navinc ); + + die( "" ); +} + +// return the images/thumbnails for a single post +// $row is an assoc. array representation of the post +function image_files_for( $row ) +{ + $del_files = array(); + // we always need to delete the image + $del_files[ IMG_DIR . $row[ 'tim' ] . $row[ 'ext' ] ] = 1; + // and the thumbnail + $del_files[ THUMB_DIR . $row[ 'tim' ] . 's.jpg' ] = 1; + // and the oekaki replay + if (ENABLE_OEKAKI_REPLAYS) { + $del_files[IMG_DIR . $row['tim'] . '.tgkr'] = 1; + } + // and the resized mobile images + if (MOBILE_IMG_RESIZE && $row['m_img']) { + $images["{$row['tim']}m.jpg"] = 1; + } + + return $del_files; +} + +// delete all posts from an IP, maintaining the consistency of the files and db +function delallbyip($ip, $imgonly, $replies_only = false) +{ + $ip = mysql_real_escape_string( $ip ); + + if ($ip === '') { + error('Invalid IP'); + } + + if ($replies_only) { + $_rep_sql = ' AND resto > 0'; + } + else { + $_rep_sql = ''; + } + + mysql_board_call( "LOCK TABLES `" . SQLLOG . "` WRITE" ); + $query = mysql_board_call("SELECT * FROM `" . SQLLOG . "` WHERE archived = 0 AND host='$ip'" . $_rep_sql); + $del_files = array(); // keys = delete these files + $update_threads = array(); // keys = update these threads' HTML + $del_threads = array(); // keys = delete replies to these thread numbers from db + $del_all = array(); // keys = these are being deleted from the db (used to clean up reports etc.) + + while( $row = mysql_fetch_assoc( $query ) ) { + // we always need to delete the image files + $del_files += image_files_for( $row ); + + if( !$imgonly ) // deleting this post from the db + { + $del_all[ $row[ 'no' ] ] = 1; + } + + if( $row[ 'resto' ] ) { // it's a reply, need to update parent + $update_threads[ $row[ 'resto' ] ] = 1; + } + elseif( !$imgonly ) { // it's a thread parent and it's getting deleted from db + // need to delete thread html + if( USE_GZIP == 1 ) { + // HTML + $del_files[ RES_DIR . $row[ 'no' ] . PHP_EXT ] = 1; + $del_files[ RES_DIR . $row[ 'no' ] . PHP_EXT . '.gz' ] = 1; + // JSON + $del_files[ RES_DIR . $row[ 'no' ] . '.json' ] = 1; + $del_files[ RES_DIR . $row[ 'no' ] . '.json.gz' ] = 1; + } + else { + // HTML + $del_files [ RES_DIR . $row[ 'no' ] . PHP_EXT ] = 1; + // JSON + $del_files[ RES_DIR . $row[ 'no' ] . '.json' ] = 1; + } + + $del_threads[ $row[ 'no' ] ] = 1; + $replyquery = mysql_board_call( "SELECT * FROM `" . SQLLOG . "` WHERE resto='{$row[ 'no' ]}'" ); + while( $replyrow = mysql_fetch_assoc( $replyquery ) ) { + $del_files += image_files_for( $replyrow ); + $del_all[ $replyrow[ 'no' ] ] = 1; + } + mysql_free_result( $replyquery ); + } + + { + $auser = $_COOKIE[ '4chan_auser' ]; + $adfsize = ( $row[ 'fsize' ] > 0 ) ? 1 : 0; + $adname = str_replace( '
!', '#', $row[ 'name' ] ); + if( $imgonly ) { + $imgonly = 1; + } + else { + $imgonly = 0; + } + //$row['sub'] = mysql_escape_string( $row['sub'] ); + //$row['com'] = mysql_escape_string( $row['com'] ); + //$row['filename'] = mysql_escape_string( $row['filename'] ); + validate_admin_cookies(); + mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,email,admin_ip,tool) values('%s',%d,%d,'%s','%s','%s','%s','%s','%s','%s','%s','%s', 'del-all-by-ip')", $imgonly, $row[ 'no' ], $row[ 'resto' ], SQLLOG, $adname, $row[ "sub" ], $row[ "com" ], $adfsize, $row[ "filename" ].$row['ext'], $auser, $row[ 'email' ], $_SERVER['REMOTE_ADDR']); + } + } + mysql_free_result( $query ); + + // delete IP's posts + if( !$imgonly ) { + mysql_board_call( "DELETE FROM `" . SQLLOG . "` WHERE host='$ip'" . $_rep_sql ); + // delete replies to IP's parent posts + foreach( $del_threads as $parent => $unused ) { + mysql_board_call( "DELETE FROM `" . SQLLOG . "` WHERE resto='$parent'" ); + } + } + else { + mysql_board_call( "UPDATE `" . SQLLOG . "` SET filedeleted=1,root=root WHERE host='$ip'" . $_rep_sql ); + } + mysql_board_call( "UNLOCK TABLES" ); + + // delete all necessary files (images and HTML) + foreach( $del_files as $file => $unused ) { + @unlink( $file ); + + if( CLOUDFLARE_PURGE_ON_DEL && strpos( $file, IMG_DIR ) !== false ) { + $filename = basename( $file ); + cloudflare_purge_by_basename(BOARD_DIR, $filename); + } + } + + // delete reports for deleted posts + foreach( $del_all as $no => $unused ) { + mysql_global_do( "DELETE FROM reports WHERE board='" . SQLLOG . "' AND no='$no'" ); + mysql_global_do( "DELETE FROM reports_for_posts WHERE board='" . SQLLOG . "' AND postid='$no'" ); + } + + echo "
Deleting posts...
"; + if( $imgonly ) { + echo "All images deleted.
Deletion successful!
"; + } + else { + echo "All posts deleted.
Deletion successful!
"; + } + + // rebuild html + if( count( $update_threads ) > 25 ) { + echo "Rebuilding all pages..."; + echo ( rebuild_all( $error ) ? " OK!" : $error ); // at some number of threads, this must be faster... + } + else { + foreach( $update_threads as $parent => $unused ) { + if( $del_threads[ $parent ] ) continue; // this thread was deleted, forget it + + echo "Rebuilding No.$parent..."; + echo ( rebuild_thread( $parent, $error ) ? " OK!" : $error ); + echo "
"; + } + } + + die( "
Rebuild successful!Done." ); +} + +function admindelall() +{ + global $onlyimgdel, $onlyrepdel, $id, $user, $pass; + $delno = array(); + $delflag = false; + reset( $_POST ); + while( $item = each( $_POST ) ) { + if( $item[ 1 ] == 'delete' ) { + array_push( $delno, intval( $item[ 0 ] ) ); + $delflag = true; + } + } + if( $delflag ) { + if( !$result = mysql_board_call( "SELECT host FROM `" . SQLLOG . "` WHERE archived = 0 AND no=" . mysql_real_escape_string( $id ) ) ) { + echo S_SQLFAIL; + } + $row = mysql_fetch_row( $result ); + list( $host ) = $row; + delallbyip($host, $onlyimgdel, $onlyrepdel === true); + } + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "
\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "
\n"; + die( "" ); +} + +function ban_template_js($post_has_file = true, $is_thread = false) { + $templates = array(); + + $level_map = get_level_map(); + + $query = "SELECT * FROM ban_templates ORDER BY LENGTH(rule), rule ASC"; + $q = mysql_global_call($query); + + while ($r = mysql_fetch_assoc($q)) { + if (!preg_match('#^(global|' . BOARD_DIR . ')[0-9]+$#', $r[ 'rule' ])) { + continue; + } + + if (($r['no'] == 1 || $r['no'] == 123 || $r['no'] == 213) && !$post_has_file) { + continue; + } + + if ($r['no'] == 6 && !DEFAULT_BURICHAN) { + continue; + } + + if ($r['no'] == 17 && (BOARD_DIR === 'mlp' || BOARD_DIR === 'trash')) { + continue; + } + + if ($r['no'] == 223 && BOARD_DIR === 'pol') { + continue; + } + + // Global 3 - Troll posts + if ($r['no'] == 222 && BOARD_DIR === 's4s') { + continue; + } + + // Global 16 - Request Thread Outside of /r/ + if ($r['no'] == 59 && BOARD_DIR === 'po') { + continue; + } + + // Global 3 + if ((BOARD_DIR === 'b' || BOARD_DIR === 'bant') && strpos($r['rule'], 'global3') !== false) { + continue; + } + + // Skip OP-only templates + if ($r['postban'] === 'move' && !$is_thread) { + continue; + } + + if ($level_map[$r['level']] !== true) { + continue; + } + + unset($r['special_action']); + + $templates[] = $r; + } + + return ' + + + '; +} + +function do_post_quarantine( $board, $post ) +{ + /* + Gathers - + Current post, current post image + All images of posts in the same thread + */ + + mysql_board_lock( true ); + $host = $post[ "host" ]; + + $post_json = make_post_json($post); + $postno = $post["no"]; + + $xffres = mysql_global_do("select xff from xff where board='%s' and postno=%d", $board, $postno); + if (mysql_num_rows($xffres)) $xff = mysql_fetch_assoc($xffres)["xff"]; + $res = mysql_global_do("insert into ncmec_reports (board,post_num,post_json,xff) value ('%s',%d,'%s','%s')", $board, $postno, $post_json, $xff); + $reportid = mysql_global_insert_id(); + + $path = "/www/quarantine/$reportid"; + mkdir( $path ); + + $image = $post[ "tim" ] . $post[ "ext" ]; + $dst_path = "$path/$image"; + $tmp_path = $dst_path.".tmp"; + @copy( IMG_DIR . "/$image", $tmp_path ); + @rename($tmp_path, $dst_path); + + if (!file_exists($dst_path)) { + // guess we can't quarantine it after all + mysql_global_do("delete from ncmec_reports where id=%d", $reportid); + } else { + $resto = $post["resto"]; + $respred = $resto ? "no=$resto or resto=$resto" : "resto=$postno"; + $q = mysql_board_call( "select * from `%s` where host='%s' and no!=%d and ($respred)", SQLLOG, $host, $postno ); + while( $p = mysql_fetch_assoc( $q ) ) { + $i = $p[ "tim" ] . $p[ "ext" ]; + mkdir( "$path/images" ); + @copy( IMG_DIR . "/$i", "$path/images/$i" ); + } + } + mysql_board_unlock(); +} + +function do_template_special_action($template, $board, $row, $is_manager = false) { + if ($template['special_action'] === 'quarantine') { + do_post_quarantine($board, $row); + } + + if ($is_manager) { + if( $template['special_action'] === 'quarantine' || $template['special_action'] === 'revokepass_spam' || $template['special_action'] === 'revokepass_illegal') { + $pass = $row['4pass_id']; + $status = $template['special_action'] === 'revokepass_spam' ? 4 : 5; + mysql_global_do("UPDATE pass_users SET status = %d WHERE user_hash = '%s' AND status = 0 LIMIT 1", $status, $pass); + } + } +} + +/** + * Auto-rangeban log entries + * $tpl_id: ban or BR template id + * $source: 1 = ban, 0 = ban request + */ +function process_auto_rangeban($ip, $browser_id, $thread_id, $post_id, $tpl_id, $source) { + $thread_id = (int)$thread_id; + + if (!$browser_id) { + return false; + } + + // Prune stale entries + $sql = "DELETE FROM event_log WHERE type = 'rangeban_hint' AND created_on < DATE_SUB(NOW(), INTERVAL 1 HOUR)"; + + $res = mysql_global_call($sql); + + if (!$res) { + return false; + } + + $need_rangeban = false; + + // Check if should apply auto rangeban (2 strikes for BRs, immediate for Bans) + $range_sql = explode('.', $ip); + + $range_sql = "{$range_sql[0]}.{$range_sql[1]}.%"; + + // Ban + if ($source === 1) { + $need_rangeban = true; + } + // Ban Request + else { + $sql =<< 0) { + $need_rangeban = true; + } + } + + if ($need_rangeban) { + // Skip if a rangeban already exists + $sql =<< DATE_SUB(NOW(), INTERVAL 1 HOUR) +SQL; + + $res = mysql_global_call($sql, BOARD_DIR, $browser_id, $range_sql); + + if (!$res) { + return false; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count > 0) { + return true; + } + + return add_auto_rangeban_log($ip, $browser_id, $thread_id, $post_id, true, $tpl_id, $source); + } + + // Add hint entry + return add_auto_rangeban_log($ip, $browser_id, $thread_id, $post_id, false, $tpl_id, $source); +} + +function add_auto_rangeban_log($ip, $browser_id, $thread_id, $post_id, $is_ban = false, $tpl_id = 0, $source = 0) { + if ($is_ban) { + $type = 'rangeban'; + } + else { + $type = 'rangeban_hint'; + } + + return write_to_event_log($type, $ip, [ + 'board' => BOARD_DIR, + 'thread_id' => $thread_id, + 'post_id' => $post_id, + 'ua_sig' => $browser_id, + 'arg_num' => $tpl_id, + 'arg_str' => (int)$source + ]); +} + +/** + * Collects posts related to the provided Password. + * This is used for banning people who hop between multiple IPs. + * Only posts made from non-mobile devices are collected. + */ +function admin_collect_related($ip, $pwd) { + if (!$pwd || !$ip) { + return null; + } + + $range_sql = explode('.', $ip); + + $range_sql[0] = (int)$range_sql[0]; + $range_sql[1] = (int)$range_sql[1]; + + $range_sql = "{$range_sql[0]}.{$range_sql[1]}."; + + $sql = << 0) { + $query =<< 0) { + return true; + } + } + + $long_ip = ip2long($ip); + + if (!$long_ip) { + $this->error('Invalid IP.'); + } + + $query =<<= $long_ip +AND active = 1 AND boards = '' AND expires_on = 0 AND report_only = 0 +LIMIT 1 +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + return mysql_num_rows($res) > 0; +} + +// Signs the ip + timestamp for authenticating reverse dns requests below +// FIXME: This is to avoid delaying ban panels +function admin_get_rev_ip_sig($ip, $t) { + if (!$ip || !$t) { + return false; + } + + $secret = 'BusEFdduVhgVKIMAx1ndhvzrgMyA5uCcfRnvIKq4+0X2vL8elzf6wHZCpWS9fsTsNG/XdlwiIBV68hzlGm6sGQ=='; + $secret = base64_decode($secret); + + if (!$secret) { + return false; + } + + $msg = "$ip $t"; + + return hash_hmac('sha256', $msg, $secret); +} + +// Prints a JSON response with the hostname of the IP +// FIXME: This is to avoid delaying ban panels +// The IP needs to be in the long int format +function admin_reverse_ip() { + if (!isset($_GET['ip']) || !isset($_GET['t']) || !isset($_GET['s'])) { + die('N/A'); + } + + if (!$_GET['t'] || !$_GET['s']) { + die('N/A'); + } + + $ip = long2ip($_GET['ip']); + + if (!$ip) { + die('N/A'); + } + + if ($_SERVER['REQUEST_TIME'] - (int)$_GET['t'] > 3) { + die('N/A'); + } + + $sig = admin_get_rev_ip_sig($_GET['ip'], $_GET['t']); + + if (!$sig) { + die('N/A'); + } + + if (hash_equals($sig, $_GET['s']) !== true) { + die('N/A'); + } + + $rev = gethostbyaddr($ip); + + if ($rev && $rev == $ip) { + $rev = ''; + } + + header('Content-Type: application/json'); + echo json_encode(['rev' => $rev]); +} + +/* Admin banning */ +function adminban() +{ + if (BOARD_DIR == 'j' && !has_level('manager')) { + die(); + } + + global $id, $user, $pass; + + $by_tpl_mode = false; + + // for async calls from reports.4chan.org + if (isset($_POST['by_tpl']) && $_POST['by_tpl']) { + $template = admin_get_template_by_id($_POST['by_tpl']); + + if (!$template) { + die('No such template'); + } + + $by_tpl_mode = true; + + $_POST['submit'] = 1; + $_POST['pubreason'] = $template['publicreason']; + $_POST['pvtreason'] = $template['privatereason']; + $_POST['days'] = $template['days']; + $_POST['warn'] = (int)($template['days'] == 0 && $template['banlen'] == ''); + $_POST['indefinite'] = (int)($template['banlen'] === 'indefinite'); + $_POST['banmsg'] = 0; + $_POST['bantype'] = $template['bantype']; + + // This will be amended later for delall -> delallrep + $_POST['postban'] = $template['postban']; + + if ($template['postban'] === 'move' && $template['postban_arg']) { + $_POST['move-board'] = $template['postban_arg']; + } + } + else { + $template = null; + } + + $submit = $_POST[ 'submit' ]; + $start_time = microtime( true ); + $xff = htmlspecialchars($_POST[ 'xff' ], ENT_QUOTES); + $pubreason = nl2br( htmlspecialchars( $_POST[ 'pubreason' ] ), false ); + $pvtreason = nl2br( htmlspecialchars( $_POST[ 'pvtreason' ] ), false ); + $reason = "$pubreason<>$pvtreason"; + $bannedby = $_COOKIE['4chan_auser']; + $days = $_POST[ 'days' ]; + $warn = $_POST[ 'warn' ]; + $indefinite = $_POST[ 'indefinite' ]; + $banmsg = $_POST[ 'banmsg' ] == 1; + $globalban = $_POST[ 'bantype' ] == 'global'; + $zonly = isset($_POST['zonly']) && $_POST['zonly'] === '1'; + //$pass_id = htmlspecialchars($_POST[ 'pass_id' ], ENT_QUOTES); + $board = BOARD_DIR; + $postid = (int)$id; + + if ($by_tpl_mode) { + $template_used = (int)$_POST['by_tpl']; + } + else { + $template_used = (int)$_POST['templateno']; + } + + if( !$result = mysql_board_call( "SELECT HIGH_PRIORITY * FROM `" . SQLLOG . "` WHERE no=" . $postid ) ) { + die( 'Post no longer exists.' ); + } + $row = mysql_fetch_assoc( $result ); + if( $row === false ) die( 'Post no longer exists.' ); + + if ($row['archived']) { + die('This post is archived.'); + } + + $post_has_file = $row['ext'] && !$row['file_deleted']; + + //list( $no, $sticky, $permasage, $closed, $now, $name, $email, $sub, $com, $host, $pwd, $filename, $ext, $w, $h, $tn_w, $tn_h, $tim, $time, $md5, $fsize, $root, $resto ) = $row; + extract( $row, EXTR_OVERWRITE ); + + $password = $pwd; + + // insert tripcode (trip or !sectrip) if not warning + $tripcode = ''; + + if ($warn != 1) { + $name_bits = explode('
!', $name); + + if ($name_bits[1]) { + $tripcode = preg_replace('/<[^>]+>/', '', $name_bits[1]); // fixme: why do we do that? + } + } + + $name = str_replace( ' !', ' #', $name ); + $name = preg_replace( '/<[^>]+>/', '', $name ); // remove all remaining html crap + + if( !$result = mysql_board_call( "select COUNT(*) FROM `" . SQLLOG . "` WHERE host='$host' AND no=$resto" ) ) { + echo S_SQLFAIL; + } + + if (mysql_result($result, 0, 0) || $resto == 0) { + $poster_is_op = true; + } + else { + $poster_is_op = false; + } + +if( $submit != "" ) { // pressed submit + if (!$host) { + error('You cannot ban this post'); + } + + if ($host) { + $reverse = gethostbyaddr($host); + } + else { + $reverse = ''; + } + + $displayhost = ( $reverse && $reverse != $host ) ? "$reverse ($host)" : $host; + + if ($template_used > -1) { + if (!$template) { + $template = mysql_global_row("ban_templates", "no", $template_used); + } + + if (!$template) { + error('Invalid template'); + } + + if (!has_level($template['level'])) { + error('You cannot use this template'); + } + + if (($template['no'] == 1 || $template['no'] == 123 || $template['no'] == 213) && !$post_has_file) { + error('This template requires a post with a file'); + } + } + + if( !$template_used ) { + $rule = ''; + } + else { + $rule = $template[ 'rule' ]; + } + + if( !$row ) { + echo "This post doesn't exist anymore.
"; + die( "[Back]" ); + } + if( $pubreason == "" ) { + echo "Public reason not specified.
"; + die( "[Back]" ); + } + elseif( $bannedby == "" ) { + echo "Admin name not specified.
"; + die( "[Back]" ); + } + elseif( !is_numeric( $days ) && ( $indefinite != 1 ) && ( $warn != 1 ) ) { + echo "Length of ban not specified.
"; + die( "[Back]" ); + } + else { + if( $warn != 1 ) { + $ubd_ts = date( "Y-m-d H:i:s", time() + $days * ( 24 * 60 * 60 ) ); + } + else { + $ubd_ts = date( "Y-m-d H:i:s", time() ); + } + if( $indefinite == 1 ) { + $length = "00000000000000"; + } + else { + $length = $ubd_ts; + } + } + + $is_manager = has_level('manager'); + + if (!$is_manager) { + $zonly = 0; + } + + $nrow = array(); + + foreach( $row as $key => $val ) { + if( ctype_digit( $val ) || is_int( $val ) ) { + $val = (int)$val; + } + $nrow[ $key ] = $val; + } + + if ($row['resto']) { + $sub_query = mysql_board_call("SELECT sub FROM `%s` WHERE no = %d", $board, $row['resto']); + $sub_res = mysql_fetch_assoc($sub_query); + if ($sub_res) { + $rel_sub = $sub_res['sub']; + + if (strpos($rel_sub, 'SPOILER<>') === 0) { + $rel_sub = substr($rel_sub, 9); + } + + if ($rel_sub !== '') { + $nrow['rel_sub'] = $rel_sub; + } + } + } + + // FIXME: email field + if (isset($row['email'])) { + $nrow['ua'] = $row['email']; + unset($nrow['email']); + } + + $post_json = json_encode($nrow); + $no_thumb = false; + + if ($template && $template['save_post'] !== 'everything') { + $no_thumb = true; + } + + $result = mysql_global_do( "INSERT INTO " . SQLLOGBAN . " (board,global,zonly,name,host,reverse,xff,reason,length,admin,md5,4pass_id,post_num,rule,post_time,post_json,template_id,admin_ip,tripcode,password) VALUES ('%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, '%s', FROM_UNIXTIME(%d), '%s', %d, '%s', '%s', '%s')", $board, $globalban, $zonly, $name, $host, $reverse, $xff, $reason, $length, $bannedby, $md5, $row[ '4pass_id' ], $no, $rule, $time, $post_json, $template_used, $_SERVER['REMOTE_ADDR'], $tripcode, $password ); + + if( !$result ) { + echo S_SQLFAIL; + } + + if( $ext != '' && $template_used && !$no_thumb ) { + $salt = file_get_contents( SALTFILE ); + $hash = sha1( BOARD_DIR . $no . $salt ); + @copy( THUMB_DIR . "{$tim}s.jpg", BANTHUMB_DIR . "{$hash}s.jpg" ); + } + + if( $banmsg ) { + if( $warn ) { + $samessage = S_USERWARNEDFORPOST; + } + else { + $samessage = S_USERBANNEDFORPOST; + } + + //if( isset( $_GET[ 'santa' ] ) && $_GET[ 'santa' ] == 'hohoho' ) $samessage = 'USER WAS GIVEN COAL FOR THIS POST'; + + if (($is_manager || has_flag('banmsg')) && isset($_POST['custombanmsg']) && $_POST['custombanmsg'] != '') { + $samessage = mysql_real_escape_string(htmlspecialchars($_POST['custombanmsg'], ENT_QUOTES)); + } + + if( !$result = mysql_board_call( "UPDATE `" . SQLLOG . "` SET root=root,com=CONCAT(com,'

($samessage)') WHERE no=%d", $postid ) ) { + echo S_SQLFAIL; + } + mysql_board_call("UPDATE `%s` SET root=root,last_modified=%d where no=%d", SQLLOG, (int)$_SERVER['REQUEST_TIME'], ($resto ? $resto : $postid)); + + if ($_POST['postban'] !== 'delpost' && $_POST['postban'] !== 'move' && $_POST['postban'] !== 'archive') { + admin_clear_reports(BOARD_DIR, $postid); + } + } + + //print "\n
insert: " . (time() - $start_time)."\n
"; //disabling this because again nobody needs to see it/leaks filepaths + echo "Banning " . $displayhost . " from "; + + if( $globalban == 1 ) { + echo "the entirety of 4chan...
"; + //append_ban( "global", $host ); + } + else { + echo "/" . $board . "/...
"; + //append_ban( $board, $host ); + } + + if( $length == "00000000000000" ) { + echo " for an indefinite amount of time."; + } + else { + echo " until " . date( 'l, F jS, Y', time() + $days * ( $warn ? 0 : 24 * 60 * 60 ) ) . ".
Ban successful!"; + } + //print "\n
rebuild : " . (microtime(1) - $start_time); //disabling because no point in showing it/leaks file paths + if( $template ) { + global $gcon; + $inserted_ban_id = mysql_insert_id($gcon); + do_template_special_action( $template, $board, $row, $is_manager ); + if( ( $template[ "blacklist" ] == "image" || $template[ 'blacklist' ] == 'rejectimage' ) && $md5 ) { + $blban = (int)( $template[ 'blacklist' ] === 'image' ); + $len = $blban ? $template['days'] : '0'; + mysql_global_do( "insert into blacklist (field,contents,description,addedby,ban,banlength,banreason)" . + "values ('md5','%s','%s','%s','$blban','$len','%s')", + $md5, $template[ "name" ] . " (via ban template, ban ID: $inserted_ban_id)", $bannedby, $template[ "publicreason" ] ); + } + } + + // Auto-rangebans processing (bans) + // FIXME: email field + $_post_meta = decode_user_meta($row['email']); + + if (!$warn && $_post_meta && $_post_meta['is_mobile']) { // mobile devices only + // global rules only + if ($template && strpos($template['rule'], 'global') !== false) { + process_auto_rangeban($host, $_post_meta['browser_id'], $row['resto'], $row['no'], $template['no'], 1); + } + + // Collect and ban other IPs based on the password + /* + $related_posts = admin_collect_related($host, $password); + + if ($related_posts) { + write_to_event_log('rel_posts', $host, [ + 'board' => BOARD_DIR, + 'thread_id' => $row['resto'], + 'post_id' => $no, + 'pwd' => $password, + 'arg_str' => $rule, + 'meta' => json_encode($related_posts) + ]); + } + */ + } + + $should_delete = $_POST[ 'postban' ] == 'delpost' || $_POST[ 'postban' ] == 'delfile'; + + $skip_rebuild = false; + + if( $should_delete ) { + echo "
"; + if (delete_post($no, $_POST[ 'postban' ] == 'delfile' ? 1 : 0, $template ? $template['no'] : false, 'ban')) { + echo ( ( $_POST[ 'postban' ] == 'delfile' ) ? "File deleted." : "Post deleted." ); + } + // Fixme, this is for the temporary is2/is3 cache purging api + if ($ext != '' && $template_used && $template['rule'] == 'global1' && !UPLOAD_BOARD) { + //purge_cache_internal_temp(BOARD_DIR, "$tim$ext"); + } + //print "\n
delete post: " . (microtime(true) - $start_time); //disabling, no point and leaks dirs + } + else if ($resto == 0) { + if ($_POST['postban'] == 'move') { + if (!isset($_POST['move-board']) || !is_board_valid($_POST['move-board'])) { + echo ('Invalid destination board. The thread was not moved.'); + } + else { + move_thread($no, $_POST['move-board']); + + echo ('
Thread moved to /' . htmlspecialchars($_POST['move-board']) . '/.'); + + $skip_rebuild = true; + } + } + else if ($_POST['postban'] === 'archive') { + archive_thread($no); + + echo ('
Thread archived.'); + + $skip_rebuild = true; + } + else if ($_POST['postban'] === 'close') { + if (mysql_board_call('UPDATE `%s` SET closed = 1 WHERE no = %d LIMIT 1', BOARD_DIR, $no)) { + log_thread_opts_action($row, $row['sticky'], $row['permasage'], 1, $row['permaage'], $row['undead']); + echo ('
Thread closed.'); + } + else { + echo ('
Could not close thread.'); + } + } + else if ($_POST['postban'] === 'permasage') { + if (mysql_board_call('UPDATE `%s` SET permasage = 1 WHERE no = %d LIMIT 1', BOARD_DIR, $no)) { + log_thread_opts_action($row, $row['sticky'], 1, $row['closed'], $row['permaage'], $row['undead']); + echo ('
Thread perma-saged.'); + } + else { + echo ('
Could not perma-sage thread.'); + } + } + } + + echo ''; + if( $banmsg && !$should_delete && !$skip_rebuild) { //need to update log because of the ban message + rebuild_thread( ( $resto ) ? $resto : $no ); + } + + // Delete all posts by IP, including threads + if ($_POST['postban'] === 'delall') { + delallbyip($host, false); + } + // Delete only replies by IP + else if ($_POST['postban'] === 'delallrep') { + // Delete the thread if the target post is an OP + if (!$resto) { + delete_post($no, 0, $template ? $template['no'] : false, 'ban'); + } + + delallbyip($host, false, true); + } + + //print "\n
total time: " . (microtime(1) - $start_time); //dont need to display this +} else { + // Banning screen display + $adminuser = mysql_real_escape_string( $_COOKIE[ '4chan_auser' ] ); + // see if user is banned + $ban_summary = get_bans_summary($host); + + if( $ban_summary['total'] > 0 ) { // don't bother checking the active ban if there weren't ever any bans on this IP... + if( !$banned = mysql_global_call( "SELECT host,board,global,zonly, DATE_FORMAT(length, 'Until %W, %M %D, %Y.') AS buntil FROM " . SQLLOGBAN . " WHERE host='" . $host . "' AND active=1" ) ) { + echo S_SQLFAIL; + } + $bannedrows = mysql_num_rows( $banned ); + if( $bannedrows > 0 ) { + while ($ban_row = mysql_fetch_array($banned)) { + $buntil = $ban_row[ 'buntil' ]; + $gban = $ban_row[ 'global' ]; + $bannedboard = $ban_row[ 'board' ]; + $bannedzonly = $ban_row[ 'zonly' ]; + if( $bannedboard == BOARD_DIR ) { + $bg = "f0d0d0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + $bantrue = 1; + } + if( $gban == 1 ) { + $bg = "f0a0a0"; + if( $buntil == "" ) $buntil = "Indefinitely."; + $globally = " (Globally)"; + $bantrue = 1; + break; + } + else { + $globally = " (" . $board . ")"; + } + } + } + if( $bantrue ) { + echo ""; + } + } + + $note = array(); + + if ($poster_is_op) { + $note[] = 'This poster is the OP'; + + if ($resto == 0) { + $_count = admin_get_thread_history($host); + + if ($_count > 1) { + $note[0] .= ' ' . ($_count - 1) . ''; + } + } + } + + if ($row['4pass_id'] != '') { + $has_4chan_pass = $row['4pass_id']; + + $note[] = 'This user is using a 4chan Pass'; + + $ban_summary_pass = get_bans_summary($has_4chan_pass, true); + } + else { + $has_4chan_pass = false; + $ban_summary_pass = null; + } + + if (!preg_match('/Android|iPhone|iPad/', $_SERVER['HTTP_USER_AGENT'])) { + $autofocus_html = ' autofocus="autofocus"'; + } + else { + $autofocus_html = ''; + } + + if ($host) { + $geoinfo = GeoIP2::get_country($host); + $asninfo = GeoIP2::get_asn($host); + } + else { + $geoinfo = $asninfo = false; + } + + if ($asninfo && isset($asninfo['aso'])) { + $aso_formatted = ' (' . htmlspecialchars($asninfo['aso'], ENT_QUOTES) . ')'; + } + else { + $aso_formatted = ''; + } + + echo '
'; + echo csrf_tag(); + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo '' . "\n"; + echo "\n"; + echo "" . "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + + if ($geoinfo && isset($geoinfo['country_code'])) { + $geo_loc = array(); + + if (isset($geoinfo['city_name'])) { + $geo_loc[] = $geoinfo['city_name']; + } + + if (isset($geoinfo['state_code'])) { + $geo_loc[] = $geoinfo['state_code']; + } + + $geo_loc[] = $geoinfo['country_name']; + + $loc = htmlspecialchars(implode(', ', $geo_loc), ENT_QUOTES); + + echo ' + + + + + '; + } + + $ban_history_row = array(); + + if ($ban_summary['total'] > 0) { + $ban_history_row[] = get_ban_history_html($ban_summary, $host); + + } + + if ($ban_summary_pass['total'] > 0) { + $ban_history_row[] = get_ban_history_html($ban_summary_pass); + } + + if (!empty($ban_history_row)) { + echo "\n"; + } + + // Browser ID + $_post_meta = decode_user_meta($row['email']); + + $query = "SELECT warn_req, ban_templates.name FROM ban_requests LEFT JOIN ban_templates ON ban_template = ban_templates.no WHERE host='%s'"; + $result = mysql_global_call($query, $host); + $brpending = array(); + while ($row = mysql_fetch_assoc($result)) { + $brpending[] = $row['name'] . ($row['warn_req'] ? ' [Warn]' : ''); + } + $brtooltip = join("\n", $brpending); + $pending = ''; + $brcount = count($brpending); + if ($brcount > 0) { + $plural = ($brcount > 1) ? 's' : ''; + $pending = << + + + +HTML; + } + echo $pending; + + echo "\n"; + + echo "\n"; + + echo "\n"; + + echo ""; + + if (!empty($note)) { + $note = implode('
', $note); + + echo ""; + } + + if (has_level('manager')/* || has_flag('developer')*/) { + $toz = ' [Unappealable]'; + } + else { + $toz = ''; + } + + if (has_level('manager') || has_flag('banmsg')) { + $ban_msg_row = " +JS; + + $pub_ban_evt = ' onchange="toggleBanMsg(this)"'; + } + else { + $ban_msg_row = $pub_ban_evt = ''; + } + + if ($resto == 0 && ENABLE_ARCHIVE) { + $_opt_archive = ''; + } + else { + $_opt_archive = ''; + } + + if (!$host) { + $btn_disabled = ' disabled'; + } + else { + $btn_disabled = ''; + } + + echo "$ban_msg_row"; + + echo ""; + } + else { + echo ""; + } + + $can_thread_ban = false; + + if ($resto == 0 && (has_level('manager') || has_flag('threadban'))) { + echo ""; + $can_thread_ban = true; + } + + echo "
Autocomplete
Template
Name
IP
Host
Location
Ban History" . + implode(' ', $ban_history_row) . "
Ban Requests + [$brcount pending ban request$plural] +
Public Ban Reason
Private Info
Unban In days [Warn] [Perma]
More Info[View Info] [Search]" . ($_post_meta['is_mobile'] ? ' ' : '') . "
Note$note
Message"; + + $ban_msg_row .= << + function toggleBanMsg(cb) { + var el = document.getElementById('pub-ban-msg'); + if (!el) { return; } + if (cb.checked) { + el.style.display = ''; + } + else { + el.style.display = 'none'; + } + } +
Ban Scope
[Public Ban]$toz
Post-Ban
Ban Thread
\n"; + echo "
"; + + // Async reverse IP request + if ($host) { + $_rev_long_ip = ip2long($host); + $_rev_ts = $_SERVER['REQUEST_TIME']; + $_rev_sig = admin_get_rev_ip_sig($_rev_long_ip, $_rev_ts); + ?> + + + + + + var el; + + function submitRequest(e) { + var select, index; + + select = document.forms[0].template; + index = select.selectedIndex; + + if (index === 0) { + e.preventDefault(); + e.stopPropagation(); + alert("You forgot to select a template."); + } + else { + if (/ Child |\[Perm\]/.test(select.options[index].textContent)) { + if (!checkSubmitConfirm(this)) { + e.preventDefault(); + e.stopPropagation(); + return; + } + } + postBack("start-ban-$board-$no"); + } + } + + if (el = document.getElementById("submit-ban-btn")) { + el.addEventListener("click", submitRequest, false); + } + +HTML; + + echo $html; + + $is_manager = has_level('manager') || has_flag('developer'); + + echo ban_template_js($post_has_file, $resto == 0); + + echo "
"; + + if ($has_4chan_pass && has_level('mod')) { + if ($is_manager) { + echo ""; + } + else { + $hashed_4chan_pass = admin_hash_4chan_pass($has_4chan_pass); + echo ""; + } + } + + if ($md5) { + echo ""; + echo ""; + + echo ""; + } + + echo ""; + + if ($host && admin_is_ip_rangebanned($host)) { + echo ""; + } + + if ($_post_meta['req_sig']) { + echo ""; + } + + if ($_post_meta['browser_id']) { + echo ""; + } + + if ($_post_meta['is_new']) { + $_user_status = 'New'; + } + else if ($_post_meta['is_known']) { + $_user_status = 'Trusted'; + } + else { + $_user_status = 'Untrusted'; + } + + if ($_post_meta['verified_level']) { + $_user_status .= ', Verified'; + } + + if ($_user_status) { + echo ""; + } + + echo ""; + echo "
4chan Pass
Hashed Pass
MD5
Filename
PHash
Password
Rangeban
Req. Sig.
Browser ID
User Status
[Close]
"; + + die( "" ); +} +} + +function adminToggleSpoiler($post, $new_spoiler) { + if (strpos($post['sub'], 'SPOILER<>') === 0) { + $old_subject = substr($post['sub'], 9); + $old_spoiler = true; + } + else { + $old_subject = $post['sub']; + $old_spoiler = false; + } + + if ($old_spoiler == $new_spoiler) { + return false; + } + + if ($new_spoiler) { + $subject = 'SPOILER<>' . $old_subject; + $actionType = 1; + } + else { + $subject = $old_subject; + $actionType = 2; + } + + $query = "UPDATE " . BOARD_DIR . " SET sub = '%s' WHERE no = %d LIMIT 1"; + $res = mysql_board_call($query, $subject, $post['no']); + + if (!$res) { + die('Database error (ats).'); + } + + $maskShift = 128; + $actionId = $maskShift + $actionType; + + $query =<< 60 ) ) { + echo "Sticky number must be between 0 and 59. Higher numbers appear above lower numbers.
"; + die( "[Back]" ); + } + else { + if( strlen( $post_sticknum ) == 1 ) $post_sticknum = "0" . $post_sticknum; + $post_sticknum = "202701010000" . $post_sticknum; + } + + echo ""; + $vars = ""; + echo "Thread flag status:
    "; + if( $post_sticky == 1 ) { + echo "
  • Sticky ✓
  • "; + $vars .= "sticky=1,root=" . $post_sticknum . ","; + } + else { + if( $sticky == 1 ) { + $sticktime = "now()"; + } + else { + $sticktime = "root"; + } + + echo '
  • Sticky ✗
  • '; + $vars .= "sticky=0,root=" . $sticktime . ","; + } + if( $post_permasage == 1 ) { + echo "
  • Perma-sage ✓
  • "; + $vars .= "permasage=1,"; + } + else { + echo '
  • Perma-sage ✗
  • '; + $vars .= "permasage=0,"; + } + if( $post_closed == 1 ) { + echo "
  • Closed ✓
  • "; + $vars .= "closed=1,"; + } + else { + echo '
  • Closed ✗
  • '; + $vars .= "closed=0,"; + } + if( $post_permaage ) { + if( $is_managerplus ) { + echo '
  • Perma-age ✓
  • '; + $vars .= "permaage=1,"; + } + } + else { + if( $is_managerplus ) { + echo '
  • Perma-age ✗
  • '; + $vars .= "permaage=0,"; + } + } + if( $post_undead ) { + //if( $is_managerplus ) { + echo '
  • Undead ✓
  • '; + $vars .= "undead=1,"; + //} + } + else { + //if( $is_managerplus ) { + echo '
  • Undead ✗
  • '; + $vars .= "undead=0,"; + //} + } + + // Clear the undead flag when a moderator modifies the sticky flag + // so the thread doesn't turn into a rolling sticky or get stuck as undead + /* + if ($undead && !$is_managerplus && $post_sticky != $sticky) { + echo '
  • Undead ✗
  • '; + $vars .= "undead=0,"; + } + */ + $vars .= "last_modified=".$_SERVER['REQUEST_TIME']; // FIXME consider checking if we only change hidden vars and don't update this + + echo '
'; + + if( !$result = mysql_board_call( "UPDATE `" . SQLLOG . "` SET %s WHERE no=%d", $vars, $post_id ) ) { + echo S_SQLFAIL; + } + + log_thread_opts_action($row, $post_sticky, $post_permasage, $post_closed, $post_permaage, $post_undead); + + if( $post_sticky != $sticky || $post_closed != $closed) rebuild_thread( $post_id ); + } + else { + echo '
'; + echo csrf_tag(); + echo "\n"; + echo "\n"; + + if( BOARD_DIR != 'b' || $GLOBALS[ 'b_sticky' ] ) { + echo "\n"; + } + echo "\n"; + echo "\n"; + + if ($is_managerplus) { + echo "\n"; + } + + //if ($is_managerplus) { + echo "\n"; + //} + + echo "\n"; + + echo "
Sticky     (Order: 0-59)
Perma-sage
Closed
Perma-age
Undead
\n"; + echo "
"; + + + /** + * Thread moving form + */ + if (BOARD_DIR !== 'b' && !UPLOAD_BOARD && !JANITOR_BOARD) { + echo move_thread_form($post_id); + } + + die( "" ); + } +} + +function log_thread_opts_action($post_data, $sticky, $permasage, $closed, $permaage, $undead) { + if (!isset($post_data['no']) || !$post_data['no']) { + die('Internal Server Error (ltoa)'); + } + + $new_mask = 0 + (($sticky) ? 1 : 0) + + (($permasage) ? 2 : 0) + + (($closed) ? 4 : 0) + + (($permaage) ? 8 : 0) + + ($undead ? 16 : 0); + + $old_mask = 0 + (($post_data['sticky']) ? 1 : 0) + + (($post_data['permasage']) ? 2 : 0) + + (($post_data['closed']) ? 4 : 0) + + (($post_data['permaage']) ? 8 : 0) + + ($post_data['undead'] ? 16 : 0); + + if ($new_mask == $old_mask) { + return false; + } + + $query = << Board'); + + foreach ($boardlist as $b_dir => $b_title) { + if ($b_dir === BOARD_DIR || $b_dir === 'f') { + continue; + } + $board_sel[] = ''; + } + + return implode("\n", $board_sel); +} + +function move_thread_form($post_id) { + $csrf_tag = csrf_tag(); + + $board_sel = get_board_options_html(); + + if (!ENABLE_ARCHIVE) { + $del_attrs = ' checked'; + } + else { + $del_attrs = ''; + } + + return << +
+$csrf_tag + + + + + + + + + + + + + + +
Move to
+ +
+
+HTML; +} + +function adminExt() +{ + global $thread; + $where = ''; + + if( isset( $_GET[ 'from' ] ) && ctype_digit( $_GET[ 'from' ] ) ) { + $from = intval( $_GET[ 'from' ] ); + $where = " AND no >= $from"; + } + + if( !$thread ) return false; + + $thread = (int)$thread; + if( !$result = mysql_board_call( "SELECT `host`, `no` FROM `%s` WHERE (no=%d OR resto=%d)$where", SQLLOG, $thread, $thread ) ) { + echo S_SQLFAIL; + + return false; + } + $json = array(); + + $salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$salt) { + die('Internal Server Error'); + } + + while ($row = mysql_fetch_assoc($result)) { + $hash = substr(base64_encode(pack( "H*", sha1($row['host'] . $salt))), 0, 8); + + $json[$row['no']] = $hash; + } + + echo json_encode( $json, JSON_NUMERIC_CHECK ); + + die(); +} + +function adminBanReq() +{ + $no = (int)$_GET['id']; + $board = BOARD_DIR; + $janitor = $_COOKIE['4chan_auser']; + + if ($board === 'j') { + die(); + } + + $result = mysql_board_call( "SELECT * FROM `%s` WHERE no=%d", $board, $no ); + + if (!mysql_num_rows($result)) { + echo ''; + error("This post doesn't exist anymore"); + } + + $post = mysql_fetch_assoc($result); + + if ($post['archived']) { + echo ''; + error("This post is archived"); + } + + if ($post['host'] === '') { + error('You cannot request a ban for this post.'); + } + + $post_has_file = $post['ext'] && !$post['file_deleted']; + + if (!access_board(BOARD_DIR)) { + // Check if the report is unlocked, weight threshold is 1500 + $query = << 0, weight, weight * 1.25))) as total_weight +FROM reports +WHERE board = '%s' AND no = %d +SQL; + + $result = mysql_global_call($query, $board, $no); + + if (!$result) { + error('Database Error (abru1'); + } + + $total_weight = (int)mysql_fetch_row($result)[0]; + + if (!$total_weight || $total_weight < 1500) { + error('You do not have permission to access this board.'); + } + } + + // for async calls from reports.4chan.org + if (isset($_POST['by_tpl']) && $_POST['by_tpl']) { + $_POST['template'] = $_POST['by_tpl']; + unset($_POST['warn_req']); + } + + if( $_POST[ 'template' ] ) { + $template = (int)$_POST['template']; + + if ($template < 1) { + error('You forgot to select a template.'); + } + + $bquery = mysql_global_call( "SELECT * FROM ban_templates WHERE no=%d", $template ); + $bres = mysql_fetch_assoc( $bquery ); + + if (!has_level($bres['level'])) { + error('You cannot use this template'); + } + + if (($bres['no'] == 1 || $bres['no'] == 123 || $bres['no'] == 213) && !$post_has_file) { + error('This template requires a post with a file'); + } + + $reason = $bres['publicreason']; + + $xffquery = mysql_global_call( "SELECT xff FROM xff WHERE board = '%s' AND postno = %d", $board, $no ); + $reverse = gethostbyaddr( $post['host'] ); + + if( $xffresult = mysql_fetch_row( $xffquery ) ) { + if( !( $xff = gethostbyaddr( $xffresult[0] ) ) ) $xff = $xffresult[0]; + } + + if (isset($_POST['warn_req']) && $_POST['warn_req']) { + if (!$bres['can_warn']) { + error('You cannot issue warn requests using this template.'); + } + $warn_req = 1; + } + else if ($bres['days'] === '0') { + $warn_req = 1; + } + else { + $warn_req = 0; + } + + // Fixme: for the cache purger below + if ($post['ext'] != '') { + $post_filename = "{$post['tim']}{$post['ext']}"; + } + else { + $post_filename = null; + } + + // Make sure we don't have any illegal reports (stop illegal images being stored) + $illegal = mysql_global_call( "SELECT COUNT(*) FROM reports WHERE board='%s' AND no=%d AND cat=2", $board, $_POST['no'] ); + if ((mysql_result($illegal, 0, 0) == 0) && $bres['save_post'] === 'everything') { + $salt = file_get_contents( SALTFILE ); + $hash = sha1($board . $post['no'] . $salt); + + @copy( + IMG_DIR . "{$post['tim']}{$post['ext']}", + BANIMG_ROOT . "$board/$hash{$post['ext']}" + ); + + @copy( + THUMB_DIR . "{$post['tim']}s.jpg", + BANTHUMB_DIR . "{$hash}s.jpg" + ); + } + else { + //unset($post['ext']); + $post['raw_md5'] = $post['md5']; + } + + if ($post['resto']) { + $sub_query = mysql_board_call("SELECT sub FROM `%s` WHERE no = %d", $board, $post['resto']); + $sub_res = mysql_fetch_assoc($sub_query); + if ($sub_res) { + $rel_sub = $sub_res['sub']; + + if (strpos($rel_sub, 'SPOILER<>') === 0) { + $rel_sub = substr($rel_sub, 9); + } + + if ($rel_sub !== '') { + $post['rel_sub'] = $rel_sub; + } + } + } + + $tpl_name = $bres['name']; + $tpl_global = $bres['bantype'] !== 'local' ? 1 : 0; + + delete_post($no, false, $template, 'ban-req'); + + $res = mysql_global_call("INSERT INTO ban_requests SET host='%s', reverse='%s', pwd='%s', xff='%s', reason='', global = $tpl_global, tpl_name = '%s', ban_template='%s', board='%s', janitor='%s', spost='%s', post_json='%s', warn_req = %d", $post['host'], $reverse, $post['pwd'], $xff, $tpl_name, $template, $board, $janitor, serialize( $post ), json_for_post($board, $post), $warn_req); + + if (!$res) { + error('Database error.'); + } + + // Auto-rangebans processing (ban requests) + // FIXME: email field + $_post_meta = decode_user_meta($row['email']); + + if (!$warn_req && $_post_meta && $_post_meta['is_mobile']) { // mobile devices only + // global rules only + if ($bres && strpos($bres['rule'], 'global') !== false) { + process_auto_rangeban($post['host'], $_post_meta['browser_id'], $post['resto'], $post['no'], $bres['no'], 0); + } + } + + // Fixme, this is for the temporary is2/is3 cache purging api + if ($post_filename && $bres && $bres['rule'] == 'global1') { + //purge_cache_internal_temp(BOARD_DIR, $post_filename); + } + echo ''; + die( ($warn_req ? 'Warn' : 'Ban') . ' request submitted! Window will now close...' ); + } + + $name = str_replace( '
!', ' !', $post[ 'name' ] ); + + $query = "SELECT warn_req, ban_templates.name FROM ban_requests LEFT JOIN ban_templates ON ban_template = ban_templates.no WHERE host='%s'"; + $result = mysql_global_call($query, $post['host']); + $brpending = array(); + while ($row = mysql_fetch_assoc($result)) { + $brpending[] = $row['name'] . ($row['warn_req'] ? ' [Warn]' : ''); + } + $brtooltip = join("\n", $brpending); + $pending = ''; + $brcount = count($brpending); + if ($brcount > 0) { + $plural = ($brcount > 1) ? 's' : ''; + $pending = << + Note + + [$brcount pending ban request$plural] + + +HTML; + } + + $csrf_tag = csrf_tag(); + + if (!preg_match('/Android|iPhone|iPad/', $_SERVER['HTTP_USER_AGENT'])) { + $autofocus_html = ' autofocus="autofocus"'; + } + else { + $autofocus_html = ''; + } + + $html = <<$csrf_tag + + + + + + + + + + + +$pending + + + + + + + + + + + + + + + + + + + + + +
Autocomplete + +
Template + +
Name + +
Reason + +
Warn? + +
Requested By + + + + +
+ +HTML; + + echo $html; + + $templates = array(); + $level_map = get_level_map(); + + $q = mysql_global_do("SELECT * FROM ban_templates ORDER BY length(rule), rule asc"); + + while( $r = mysql_fetch_assoc( $q ) ) { + if (!preg_match('#^(global|' . BOARD_DIR . ')[0-9]+$#', $r['rule'])) { + continue; + } + + if (($r['no'] == 1 || $r['no'] == 123 || $r['no'] == 213) && !$post_has_file) { + continue; + } + + if ($r['no'] == 6 && !DEFAULT_BURICHAN) { // NWS on Worksafe Board + continue; + } + + if ($r['no'] == 17 && (BOARD_DIR === 'mlp' || BOARD_DIR === 'trash')) { // Pony/Ponies Outside of /mlp/ + continue; + } + + if ($r['no'] == 222 && (BOARD_DIR === 's4s' || BOARD_DIR === 'bant')) { // Global 3 - Troll posts + continue; + } + + if ($r['no'] == 59 && BOARD_DIR === 'po') { // Global 16 - Request Thread Outside of /r/ + continue; + } + + if ($r['no'] == 223 && BOARD_DIR === 'pol') { // Global 3 - Racism + continue; + } + + // Global 3 + if ((BOARD_DIR === 'b' || BOARD_DIR === 'bant') && strpos($r['rule'], 'global3') !== false) { + continue; + } + + if ($r['no'] == 59 && $post['resto']) { // Request Thread Outside of /r/ + continue; + } + + if ($level_map[$r['level']] !== true) { + continue; + } + + unset($r[ 'special_action' ], $r[ 'blacklist' ], $r[ 'bantype' ], $r[ 'postban' ], $r[ 'privatereason' ]); + + $templates[] = $r; + } + + $encTemp = json_encode( $templates ); + + $v = << + +HTML; + + echo $v; +} + +/* FIXME: this is for the temporary is2/is3 cache purge api */ +function purge_cache_internal_temp($board, $file) { + $url = "http://g0ch4.brazil.jp:24502"; + + $post = array(); + $post['rmpath'] = "/$board/$file"; + $post['key'] = '6a310437e13935b64beefcf10da8dba3'; + $post = http_build_query($post); + + rpc_start_request($url, $post, null, false); +} + +/** + * Sets or usnets the spoiler flag for images + * Does its own access validation. + * Accessible to janitors + */ +function admin_toggle_spoiler() { + header('Content-Type: text/plain'); + + if (!SPOILERS) { + echo '0'; die(); + } + + auth_user(); + + if (!has_level() && (!has_level('janitor') || !access_board(BOARD_DIR))) { + echo '-1'; die(); + } + + if (!isset($_GET['pid']) || !isset($_GET['flag'])) { + echo '0'; die(); + } + + $query = "SELECT * FROM `" . SQLLOG . "` WHERE no = %d"; + + $res = mysql_board_call($query, $_GET['pid']); + + if (!$res) { + echo '0'; die(); + } + + $post = mysql_fetch_assoc($res); + + if (!$post) { + echo '0'; die(); + } + + $spoiler_updated = adminToggleSpoiler($post, (bool)$_GET['flag']); + + if ($spoiler_updated) { + if ($post['resto']) { + $thread_id = (int)$post['resto']; + } + else { + $thread_id = (int)$post['no']; + } + + rebuild_thread($thread_id, $error, (bool)$post['archived']); + } + + echo '1'; die(); +} + +function validate_csrf($ref_only = false) { + if ($_SERVER['REQUEST_METHOD'] == 'POST' && !$ref_only) { + if (!isset($_COOKIE['_tkn']) || !isset($_POST['_tkn']) + || $_COOKIE['_tkn'] == '' || $_POST['_tkn'] == '' + || $_COOKIE['_tkn'] !== $_POST['_tkn']) { + + if (!is_local()) { + error('Bad Request.'); + } + } + } + else { + if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] != '' + && !preg_match('/^https?:\/\/([_a-z0-9]+)\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) { + error('Bad Request.'); + } + } +} + +/*-----------Main-------------*/ + +// Can't check for csrf token for this. Only check the referer. +validate_csrf($admin === 'delall'); + +switch($admin) { + case 'adminext': + adminvalid(); + adminExt(); + break; + + case 'banreq': + adminvalid( 'Ban Request' ); + adminBanReq(); + break; + case 'del': + adminvalid(); + //admin_delete(); + break; + case 'delall': + adminvalid(); + admindelall(); + break; + case 'delallbyip': + adminvalid(); + delallbyip( $_POST[ 'ip' ], $_POST[ 'imgonly' ] ); + break; + case 'ban': + adminvalid( 'Ban User' ); + adminban(); + break; + case 'opt': + adminvalid( 'Thread Options' ); + adminopt(); + break; + case 'spoiler': + admin_toggle_spoiler(); + break; + case 'cleanup': + adminvalid( 'Board Cleanup' ); + clean(); + break; + case 'cpban': + adminvalid(); + cpban((int)$_POST['no']); + break; + case 'rev': + admin_reverse_ip(); + break; + /* + case 'reportqueue': + adminvalid(); + adminreportqueue(); + break; + case 'reportclear': + adminvalid(); + adminreportclear(); + break; + */ + default: + adminvalid(); + //admin_delete(); +} diff --git a/auth-test.php b/auth-test.php new file mode 100644 index 0000000..d662f0a --- /dev/null +++ b/auth-test.php @@ -0,0 +1,422 @@ +this page to renew it.', // status 1 + ERR_REFUNDED = 'This Pass has been refunded and disabled. You cannot use it anymore.', // status 2 + ERR_DISPUTED = 'This Pass has a disputed payment. You cannot use it until the dispute is resolved.', // status 3 + ERR_REVOKED_SPAM = 'This Pass has been revoked due to spamming, which is a violation of the Terms of Use.', // status 4 + ERR_REVOKED_ILLEGAL = 'This Pass has been revoked due to illegal content being posted, which is a violaton of the Terms of Use.' // status 5 + ; + + private function error($msg) { + $this->renderResponse(self::AUTH_ERROR, $msg); + } + + private function renderResponse($status, $msg = null) { + if ($this->is_xhr) { + header('Content-type: application/json'); + echo json_encode(array('status' => $status, 'message' => $msg)); + } + else { + $this->auth_status = $status; + $this->message = $msg; + require_once(self::VIEW_TPL); + } + die(); + } + + private function pretty_duration($sec) { + $duration = ''; + + $hours = (int)($sec / 3600); + $minutes = (int)($sec / 60); + + if ($hours) { + $duration .= str_pad($hours, 2, '0', STR_PAD_LEFT) . ' hour'; + + if ($hours != 1) { + $duration .= 's'; + } + + $duration .= ' '; + } + + if ($minutes) { + $minutes = (int)(($sec / 60) % 60); + + $duration .= str_pad($minutes, 2, '0', STR_PAD_LEFT). ' minute'; + + if ($minutes != 1) { + $duration .= 's'; + } + } + + $seconds = intval($sec % 60); + + return $duration; + } + + private function get_csrf_token() { + return bin2hex(openssl_random_pseudo_bytes(16)); + } + + private function validate_referer() { + if (!isset($_SERVER['HTTP_REFERER']) || $_SERVER['HTTP_REFERER'] === '') { + return; + } + + if (!preg_match('/^https:\/\/sys\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) { + $this->error(self::ERR_BAD_REQUEST); + } + } + + private function validate_csrf() { + if (!isset($_COOKIE['csrf']) || !isset($_POST['csrf']) + || $_COOKIE['csrf'] === '' || $_POST['csrf'] === '') { + $this->error(self::ERR_BAD_REQUEST); + } + + if ($_COOKIE['csrf'] !== $_POST['csrf']) { + $this->error(self::ERR_BAD_REQUEST); + } + } + + private function validate_auth_flood($long_ip) { + if (!$long_ip) { + return; + } + + $query = "SELECT COUNT(ip) FROM user_actions WHERE ip = $long_ip AND action = 'fail_pass_auth' AND time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)"; + + $res = mysql_global_call($query); + + if (!$res) { + return; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count >= LOGIN_FAIL_HOURLY) { + $this->error(self::ERR_FLOOD); + } + } + + private function register_auth_failure($long_ip) { + if (!$long_ip) { + return; + } + + $query = "INSERT INTO user_actions (ip, board, action, time) VALUES(%d, '', 'fail_pass_auth', NOW())"; + $res = mysql_global_call($query, $long_ip); + } + + private function convert_new_pass_status($user_hash, $hashed_pin) { + $table = self::PASS_TABLE; + + $query = "UPDATE $table SET pin = '%s', status = 0 WHERE user_hash = '%s' AND status = 6 LIMIT 1"; + + mysql_global_call($query, $hashed_pin, $user_hash); + + $this->set_cookie('pass_email', '', -1); + } + + private function convert_delayed_pass_status($user_hash, $hashed_pin) { + $table = self::PASS_TABLE; + + $query = "UPDATE $table SET pin = '%s', status = 0, expiration_date = NOW() + INTERVAL 1 YEAR WHERE user_hash = '%s' AND status = 7 LIMIT 1"; + + mysql_global_call($query, $hashed_pin, $user_hash); + } + + private function set_cookie($name, $value, $expire, $secure = false, $http_only = false) { + setcookie($name, $value, $expire, '/', '.' . THIS_DOMAIN, $secure, $http_only); + } + + private function clear_cookies() { + $cookie_time = $_SERVER['REQUEST_TIME'] - 3600; + $this->set_cookie('pass_id', null, $cookie_time, true, true); + $this->set_cookie('pass_enabled', null, $cookie_time); + } + + private function get_random_base64bytes($length = 64) { + $data = openssl_random_pseudo_bytes($length); + + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + private function get_salt() { + $salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$salt) { + $this->error(sprintf(self::ERR_GENERIC, 'gs')); + } + + return $salt; + } + + /** + * Login + */ + private function authenticate() { + $this->validate_referer(); + + $table = self::PASS_TABLE; + + $time_now = time(); + + // Token + if (!isset($_POST['id']) || $_POST['id'] === '') { + $this->error(self::ERR_EMPTY_FIELD); + } + + if (strlen($_POST['id']) != 10) { + $this->error(self::ERR_TOKEN_LEN); + } + + $id = $_POST['id']; + + // Pin + if (!isset($_POST['pin']) || $_POST['pin'] === '') { + $this->error(self::ERR_EMPTY_FIELD); + } + + $pin = $_POST['pin']; + + // --- + + $ip = $_SERVER['REMOTE_ADDR']; + $long_ip = ip2long($ip); + + $this->validate_auth_flood($long_ip); + + // --- + + $plain_pin = $pin; + $pin = crypt($pin, substr($id, 4, 9)); + + $query = "SELECT * FROM $table WHERE user_hash = '%s' AND (pin = '%s' OR pin = '%s') LIMIT 1"; + + $res = mysql_global_call($query, $id, $pin, $plain_pin); + + if (!$res) { + $this->error(self::ERR_DB); + } + + if (mysql_num_rows($res) !== 1) { + $this->register_auth_failure($long_ip); + $this->error(self::ERR_BAD_AUTH); + } + + $pass = mysql_fetch_assoc($res); + + if (!$pass) { + $this->error(sprintf(self::ERR_GENERIC, 'mfa1')); + } + + $last_used = strtotime($pass['last_used']); + + $last_ip_mask = ip2long($pass['last_ip']) & (~65535); + + $ip_mask = $long_ip & (~65535); + + if ($last_ip_mask !== 0 && ($time_now - $last_used) < PASS_TIMEOUT && $last_ip_mask != $ip_mask) { + $remaining = $this->pretty_duration(PASS_TIMEOUT - ($time_now - $last_used)); + $this->error(sprintf(self::ERR_IN_USE, $remaining)); + } + + switch ($pass['status']){ + case 0: + break; + + case 1: + $this->clear_cookies(); + $this->error(sprintf(self::ERR_EXPIRED, $pass['pending_id'])); + break; + + case 2: + $this->clear_cookies(); + $this->error(self::ERR_REFUNDED); + break; + + case 3: + $this->clear_cookies(); + $this->error(self::ERR_DISPUTED); + break; + + case 4: + $this->clear_cookies(); + $this->error(self::ERR_REVOKED_SPAM); + break; + + case 5: + $this->clear_cookies(); + $this->error(self::ERR_REVOKED_ILLEGAL); + break; + + case 6: + $this->convert_new_pass_status($pass['user_hash'], $pin); + break; + + case 7: + $this->convert_delayed_pass_status($pass['user_hash'], $pin); + break; + } + + // Update country + $geo_data = GeoIP2::get_country($ip); + + if ($geo_data && isset($geo_data['country_code'])) { + $country_code = mysql_real_escape_string($geo_data['country_code']); + } + else { + $country_code = 'XX'; + } + + $update_country = ", last_country = '$country_code'"; + + $query = "UPDATE $table SET last_ip = '%s', last_used = NOW() $update_country WHERE user_hash = '%s' AND last_ip != '%s' AND status = 0 LIMIT 1"; + + mysql_global_call($query, $ip, $id, $ip); + + // Update session id + if (!$pass['session_id']) { + $pass_session = $this->get_random_base64bytes(32); + + if (!$pass_session) { + $this->error(sprintf(self::ERR_GENERIC, 'grb')); + } + + $query = "UPDATE $table SET session_id = '$pass_session' WHERE user_hash = '%s' AND status = 0 LIMIT 1"; + + mysql_global_call($query, $id); + } + else { + $pass_session = $pass['session_id']; + } + + $admin_salt = $this->get_salt(); + + $hashed_pass_session = substr(hash('sha256', $pass_session . $admin_salt), 0, 32); + + if (!$hashed_pass_session) { + $this->error(sprintf(self::ERR_GENERIC, 'hps')); + } + + if (isset($_POST['long_login'])) { + $cookie_time = $time_now + 31556900; + } + else { + $cookie_time = $time_now + 86400; + } + + $this->set_cookie('pass_id', "$id.$hashed_pass_session", $cookie_time, true, true); + $this->set_cookie('pass_enabled', '1', $cookie_time); + + $this->renderResponse(self::AUTH_SUCCESS); + } + + /** + * Index + */ + public function index() { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + if (isset($_POST['logout'])) { + $this->validate_referer(); + $this->clear_cookies(); + $this->renderResponse(self::AUTH_OUT); + } + else { + return $this->authenticate(); + } + } + + if (isset($_COOKIE['pass_enabled'])) { + $this->renderResponse(self::AUTH_YES); + } + else { + $this->renderResponse(self::AUTH_NO); + } + } + + /** + * Main + */ + public function run() { + $method = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET; + + if (isset($method['action'])) { + $action = $method['action']; + } + else { + $action = 'index'; + } + + if (in_array($action, $this->actions)) { + if (isset($method['xhr'])) { + /* + if (isset($_SERVER['HTTP_ORIGIN']) && preg_match('/^https:\/\/sys\.(4chan|4channel)\.org$/', $_SERVER['HTTP_ORIGIN'])) { + header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']); + header('Access-Control-Allow-Methods: OPTIONS, POST'); + header('Access-Control-Allow-Credentials: true'); + } + */ + $this->is_xhr = true; + } + + $this->$action(); + } + else { + $this->error('Bad request'); + } + } +} + +$ctrl = new App(); +$ctrl->run(); diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..cde6a11 --- /dev/null +++ b/auth.php @@ -0,0 +1,451 @@ +this page to renew it.', // status 1 + ERR_REFUNDED = 'This Pass has been refunded and disabled. You cannot use it anymore.', // status 2 + ERR_DISPUTED = 'This Pass has a disputed payment. You cannot use it until the dispute is resolved.', // status 3 + ERR_REVOKED_SPAM = 'This Pass has been revoked due to spamming, which is a violation of the Terms of Use.', // status 4 + ERR_REVOKED_ILLEGAL = 'This Pass has been revoked due to illegal content being posted, which is a violaton of the Terms of Use.' // status 5 + ; + + private function error($msg) { + $this->renderResponse(self::AUTH_ERROR, $msg); + } + + private function renderResponse($status, $msg = null) { + if ($this->is_xhr) { + header('Content-type: application/json'); + echo json_encode(array('status' => $status, 'message' => $msg)); + } + else { + $this->auth_status = $status; + $this->message = $msg; + require_once(self::VIEW_TPL); + } + die(); + } + + private function pretty_duration($sec) { + $duration = ''; + + $hours = (int)($sec / 3600); + $minutes = (int)($sec / 60); + + if ($hours) { + $duration .= str_pad($hours, 2, '0', STR_PAD_LEFT) . ' hour'; + + if ($hours != 1) { + $duration .= 's'; + } + + $duration .= ' '; + } + + if ($minutes) { + $minutes = (int)(($sec / 60) % 60); + + $duration .= str_pad($minutes, 2, '0', STR_PAD_LEFT). ' minute'; + + if ($minutes != 1) { + $duration .= 's'; + } + } + + $seconds = intval($sec % 60); + + return $duration; + } + + private function get_csrf_token() { + return bin2hex(openssl_random_pseudo_bytes(16)); + } + + private function validate_referer() { + if (!isset($_SERVER['HTTP_REFERER']) || $_SERVER['HTTP_REFERER'] === '') { + return; + } + + if (!preg_match('/^https:\/\/sys\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) { + $this->error(self::ERR_BAD_REQUEST); + } + } + + private function validate_csrf() { + if (!isset($_COOKIE['csrf']) || !isset($_POST['csrf']) + || $_COOKIE['csrf'] === '' || $_POST['csrf'] === '') { + $this->error(self::ERR_BAD_REQUEST); + } + + if ($_COOKIE['csrf'] !== $_POST['csrf']) { + $this->error(self::ERR_BAD_REQUEST); + } + } + + private function validate_auth_flood($long_ip) { + if (!$long_ip) { + return; + } + + $query = "SELECT COUNT(ip) FROM user_actions WHERE ip = $long_ip AND action = 'fail_pass_auth' AND time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)"; + + $res = mysql_global_call($query); + + if (!$res) { + return; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count >= LOGIN_FAIL_HOURLY) { + $this->error(self::ERR_FLOOD); + } + } + + private function register_auth_failure($long_ip) { + if (!$long_ip) { + return; + } + + $query = "INSERT INTO user_actions (ip, board, action, time) VALUES(%d, '', 'fail_pass_auth', NOW())"; + $res = mysql_global_call($query, $long_ip); + } + + private function convert_new_pass_status($user_hash, $hashed_pin) { + $table = self::PASS_TABLE; + + $query = "UPDATE $table SET pin = '%s', status = 0 WHERE user_hash = '%s' AND status = 6 LIMIT 1"; + + mysql_global_call($query, $hashed_pin, $user_hash); + + $this->set_cookie('pass_email', '', -1); + } + + private function convert_delayed_pass_status($user_hash, $hashed_pin) { + $table = self::PASS_TABLE; + + $query = "UPDATE $table SET pin = '%s', status = 0, expiration_date = NOW() + INTERVAL 1 YEAR WHERE user_hash = '%s' AND status = 7 LIMIT 1"; + + mysql_global_call($query, $hashed_pin, $user_hash); + } + + private function set_cookie($name, $value, $ttl, $secure = false, $http_only = false) { + $name = rawurlencode($name); + $value = rawurlencode($value); + + $domain = '.' . THIS_DOMAIN; + + $flags = array(); + + if ($secure) { + $flags[] = 'Secure'; + } + + if ($http_only) { + $flags[] = 'HttpOnly'; + } + + if (!empty($flags)) { + $flags = '; ' . implode('; ', $flags); + } + else { + $flags = ''; + } + + if ($ttl !== 0) { + $max_age = " Max-Age=$ttl;"; + } + else { + $max_age = ''; + } + + header("Set-Cookie: $name=$value; Path=/;$max_age Domain=$domain; SameSite=None$flags", false); + } + + private function clear_cookies() { + $cookie_time = -3600; + $this->set_cookie('pass_id', '', $cookie_time, true, true); + $this->set_cookie('pass_enabled', '', $cookie_time, true); + } + + private function get_random_base64bytes($length = 64) { + $data = openssl_random_pseudo_bytes($length); + + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + private function get_salt() { + $salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$salt) { + $this->error(sprintf(self::ERR_GENERIC, 'gs')); + } + + return $salt; + } + + /** + * Login + */ + private function authenticate() { + $this->validate_referer(); + + $table = self::PASS_TABLE; + + $time_now = time(); + + // Token + if (!isset($_POST['id']) || $_POST['id'] === '') { + $this->error(self::ERR_EMPTY_FIELD); + } + + if (strlen($_POST['id']) != 10) { + $this->error(self::ERR_TOKEN_LEN); + } + + $id = $_POST['id']; + + // Pin + if (!isset($_POST['pin']) || $_POST['pin'] === '') { + $this->error(self::ERR_EMPTY_FIELD); + } + + $pin = $_POST['pin']; + + // --- + + $ip = $_SERVER['REMOTE_ADDR']; + $long_ip = ip2long($ip); + + $this->validate_auth_flood($long_ip); + + // --- + + $plain_pin = $pin; + $pin = crypt($pin, substr($id, 4, 9)); + + $query = "SELECT * FROM $table WHERE user_hash = '%s' AND (pin = '%s' OR pin = '%s') LIMIT 1"; + + $res = mysql_global_call($query, $id, $pin, $plain_pin); + + if (!$res) { + $this->error(self::ERR_DB); + } + + if (mysql_num_rows($res) !== 1) { + $this->register_auth_failure($long_ip); + $this->error(self::ERR_BAD_AUTH); + } + + $pass = mysql_fetch_assoc($res); + + if (!$pass) { + $this->error(sprintf(self::ERR_GENERIC, 'mfa1')); + } + + $last_used = strtotime($pass['last_used']); + + $last_ip_mask = ip2long($pass['last_ip']) & (~65535); + + $ip_mask = $long_ip & (~65535); + + if ($last_ip_mask !== 0 && ($time_now - $last_used) < PASS_TIMEOUT && $last_ip_mask != $ip_mask) { + $remaining = $this->pretty_duration(PASS_TIMEOUT - ($time_now - $last_used)); + $this->error(sprintf(self::ERR_IN_USE, $remaining)); + } + + switch ($pass['status']){ + case 0: + break; + + case 1: + $this->clear_cookies(); + $this->error(sprintf(self::ERR_EXPIRED, $pass['pending_id'])); + break; + + case 2: + $this->clear_cookies(); + $this->error(self::ERR_REFUNDED); + break; + + case 3: + $this->clear_cookies(); + $this->error(self::ERR_DISPUTED); + break; + + case 4: + $this->clear_cookies(); + $this->error(self::ERR_REVOKED_SPAM); + break; + + case 5: + $this->clear_cookies(); + $this->error(self::ERR_REVOKED_ILLEGAL); + break; + + case 6: + $this->convert_new_pass_status($pass['user_hash'], $pin); + break; + + case 7: + $this->convert_delayed_pass_status($pass['user_hash'], $pin); + break; + } + + // Update country + $geo_data = GeoIP2::get_country($ip); + + if ($geo_data && isset($geo_data['country_code'])) { + $country_code = mysql_real_escape_string($geo_data['country_code']); + } + else { + $country_code = 'XX'; + } + + $update_country = ", last_country = '$country_code'"; + + $query = "UPDATE $table SET last_ip = '%s', last_used = NOW() $update_country WHERE user_hash = '%s' AND last_ip != '%s' AND status = 0 LIMIT 1"; + + mysql_global_call($query, $ip, $id, $ip); + + // Update session id + if (!$pass['session_id']) { + $pass_session = $this->get_random_base64bytes(32); + + if (!$pass_session) { + $this->error(sprintf(self::ERR_GENERIC, 'grb')); + } + + $query = "UPDATE $table SET session_id = '$pass_session' WHERE user_hash = '%s' AND status = 0 LIMIT 1"; + + mysql_global_call($query, $id); + } + else { + $pass_session = $pass['session_id']; + } + + $admin_salt = $this->get_salt(); + + $hashed_pass_session = substr(hash('sha256', $pass_session . $admin_salt), 0, 32); + + if (!$hashed_pass_session) { + $this->error(sprintf(self::ERR_GENERIC, 'hps')); + } + + if (isset($_POST['long_login'])) { + $cookie_time = 31556900; + } + else { + $cookie_time = 86400; + } + + $this->set_cookie('pass_id', "$id.$hashed_pass_session", $cookie_time, true, true); + $this->set_cookie('pass_enabled', '1', $cookie_time, true); + + $this->renderResponse(self::AUTH_SUCCESS); + } + + /** + * Index + */ + public function index() { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + if (isset($_POST['logout'])) { + $this->validate_referer(); + $this->clear_cookies(); + $this->renderResponse(self::AUTH_OUT); + } + else { + return $this->authenticate(); + } + } + + if (isset($_COOKIE['pass_enabled'])) { + $this->renderResponse(self::AUTH_YES); + } + else { + $this->renderResponse(self::AUTH_NO); + } + } + + /** + * Main + */ + public function run() { + $method = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET; + + if (isset($method['action'])) { + $action = $method['action']; + } + else { + $action = 'index'; + } + + if (in_array($action, $this->actions)) { + if (isset($method['xhr'])) { + /* + if (isset($_SERVER['HTTP_ORIGIN']) && preg_match('/^https:\/\/sys\.(4chan|4channel)\.org$/', $_SERVER['HTTP_ORIGIN'])) { + header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']); + header('Access-Control-Allow-Methods: OPTIONS, POST'); + header('Access-Control-Allow-Credentials: true'); + } + */ + $this->is_xhr = true; + } + + $this->$action(); + } + else { + $this->error('Bad request'); + } + } +} + +$ctrl = new App(); +$ctrl->run(); diff --git a/block_resync_removed b/block_resync_removed new file mode 100644 index 0000000..e69de29 diff --git a/boardlist.txt b/boardlist.txt new file mode 100644 index 0000000..49cdef4 --- /dev/null +++ b/boardlist.txt @@ -0,0 +1 @@ +a aco c d e f g gif h his hr k m n o p r s t u v w wg i ic cm y an cgl ck co fa fit jp mlp mu po sp tg toy trv tv x b soc r9k test adv lit int sci 3 vp diy pol hc vg hm j wsg out lgbt vr gd s4s biz qa trash news wsr qst bant vip vrpg vmg vst vt vm pw xs \ No newline at end of file diff --git a/captcha-test.php b/captcha-test.php new file mode 100644 index 0000000..22ba1ba --- /dev/null +++ b/captcha-test.php @@ -0,0 +1,1127 @@ +or verify your email address before making a post.

Click here for more information
or to verify your email.'); + +// --- + +function twister_captcha_output_data($data) { + if (isset($_GET['framed'])) { + twister_captcha_output_html($data); + } + else { + header('Content-Type: application/json'); + echo json_encode($data); + } +} + +function twister_captcha_output_html($data) { + header('Content-Security-Policy: frame-ancestors https://*.' . TWISTER_DOMAIN . ';'); + $now = $_SERVER['REQUEST_TIME']; +?> + + + + +

You can now close this page and try getting a captcha again.

+ true, 'ticket' => false ]); +} + +function twister_captcha_get_ticket_captcha_response() { + if (isset($_GET['ticket_resp'])) { + return $_GET['ticket_resp']; + } + + return false; +} + +function twister_captcha_get_hcaptcha_private_key() { + $path = '/www/global/yotsuba/config/captcha_config.ini'; + + $cfg = file_get_contents($path); + + if (!$cfg) { + return false; + } + + $res = preg_match('/^HCAPTCHA_API_KEY_PRIVATE ?= ?([^\s]+)$/m', $cfg, $m); + + if (!$res || empty($m) || !$m[1]) { + return false; + } + + return $m[1]; +} + +function twister_captcha_verify_ticket_captcha() { + $response = twister_captcha_get_ticket_captcha_response(); + + if (!$response || strlen($response) > 4096) { + return false; + } + + $captcha_private_key = twister_captcha_get_hcaptcha_private_key(); + + if (!$captcha_private_key) { + // Don't block in case of misconfiguration on our end + return true; + } + + $url = 'https://hcaptcha.com/siteverify'; + + $post = array( + 'secret' => $captcha_private_key, + 'response' => $response, + 'remoteip' => $_SERVER['REMOTE_ADDR'], + 'sitekey' => TWISTER_HCAPTCHA_SITEKEY, + ); + + $curl = curl_init(); + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2); + curl_setopt($curl, CURLOPT_TIMEOUT, 4); + curl_setopt($curl, CURLOPT_USERAGENT, '4chan'); + curl_setopt($curl, CURLOPT_POSTFIELDS, $post); + + $resp = curl_exec($curl); + + if ($resp === false) { + curl_close($curl); + return false; + } + + $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($resp_status >= 300) { + curl_close($curl); + return false; + } + + curl_close($curl); + + $json = json_decode($resp, true); + + // BAD + if (json_last_error() !== JSON_ERROR_NONE) { + return false; + } + + // GOOD + if ($json && isset($json['success']) && $json['success']) { + return true; + } + + // BAD + return false; +} + +function twister_captcha_process_ticket_captcha($pcd, $now, $long_ip, $board, $msg = null, $bypassable = false) { + // Captcha response was sent, verify it + if (twister_captcha_get_ticket_captcha_response()) { + if (twister_captcha_verify_ticket_captcha() === true) { + // Captcha is valid, return a new ticket + twister_captcha_output_new_ticket($pcd, $now, $long_ip, $board, $msg, $bypassable); + } + else { + // Wrong captcha or captcha malfunction + twister_captcha_error(TWISTER_ERR_GENERIC . ' (tcr0)'); + } + } + // No captcha reponse provided, tell the frontend to show a ticket captcha + else { + twister_captcha_output_ticket_captcha(); + } +} + +function twister_captcha_output_new_ticket($pcd, $now, $long_ip, $board, $msg = null, $bypassable = false) { + $ticket = twister_captcha_generate_ticket($now, $long_ip, $board); + + if (!$ticket) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (gt1)'); + } + + twister_captcha_output_ticket_pcd($ticket, $pcd, $msg, $bypassable); +} + +function twister_captcha_output_ticket_pcd($ticket, $pcd, $msg, $bypassable) { + $data = []; + + if ($ticket) { + $data['ticket'] = $ticket; + } + + $data['pcd'] = $pcd; + + if ($msg) { + $data['pcd_msg'] = $msg; + } + + if ($bypassable) { + $data['bpcd'] = true; + } + + twister_captcha_output_data($data); +} + +function twister_captcha_error($msg, $extra = null) { + //http_response_code(500); + $data = [ 'error' => $msg ]; + + if ($extra) { + $data = array_merge($data, $extra); + } + + twister_captcha_output_data($data); + + die(); +} + +function twister_captcha_is_req_suspicious() { + /* + if (isset($_SERVER['HTTP_X_HTTP_VERSION'])) { + if (strpos($_SERVER['HTTP_X_HTTP_VERSION'], 'HTTP/1') === 0) { + return true; + } + } + */ + $no_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) === false; + $no_accept = isset($_SERVER['HTTP_ACCEPT']) === false; + + if ($no_lang && $no_accept) { + return true; + } + + if ($no_lang && strpos($_SERVER['HTTP_USER_AGENT'], '; wv)') !== false) { + return true; + } + + if ($no_accept && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') === false) { + return true; + } + + if ($no_lang && isset($_SERVER['HTTP_REFERER'])) { + $ref = $_SERVER['HTTP_REFERER']; + + if (strpos($ref, 'sys.4chan.org') !== false || strpos($ref, '/thread/') !== false) { + return true; + } + } + + return false; +} + +function twister_captcha_need_hcaptcha() { + if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { + return true; + } + + $ua = $_SERVER['HTTP_USER_AGENT']; + + $score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + + // Skip Android Webviews + if ($score == 1 && strpos($ua, '; wv)') !== false) { + return false; + } + + return $score < 99; +} + +function twister_captcha_check_likely_automated($memcached, $now, $threshold = 29) { + if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { + return false; + } + + $ua = $_SERVER['HTTP_USER_AGENT']; + + // Skip Android Webviews + if (strpos($ua, '; wv)') !== false) { + return false; + } + + // Skip iPhone Webviews + if (preg_match('/iPhone|iPad/', $ua) && !preg_match('/Mobile|Safari/', $ua)) { + return false; + } + + $score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + + if ($score > 1 && $score <= $threshold) { + $key = 'bmbot' . $_SERVER['REMOTE_ADDR']; + $memcached->set($key, 1, $now + 43200); + return true; + } + + return false; +} + +function twister_captcha_get_pcd_penalty($board, $thread_id) { + $count = 0; + + return 0; // FIXME + + // Reports + if ($thread_id === 1 && $board !== '!') { + return 0; + } + + if (isset($_SERVER['HTTP_X_GEO_ASN'])) { + $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; + } + else { + $asn = 0; + } + + if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { + $country = $_SERVER['HTTP_X_GEO_COUNTRY']; + } + else { + $country = null; + } + + // Mobile clients + if ($asn > 0 && in_array($asn, TWISTER_MOBILE_ASNS)) { + $count++; + } + else if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/Mobile|iPhone|; wv/', $_SERVER['HTTP_USER_AGENT'])) { + $count++; + } + + // Rare countries + if ($country && !in_array($country, TWISTER_WHITELIST_COUNTRIES)) { + $count++; + } + + // Rare ISPs + if ($asn > 0 && !in_array($asn, TWISTER_WHITELIST_ASNS)) { + $count++; + } + + return $count * TWISTER_PRE_CD_PENALITY; +} + +function twister_captcha_store_challenge($memcached, $challenge_uid, $long_ip, $expiration) { + if (!$challenge_uid || !$long_ip || !$expiration) { + return false; + } + + return $memcached->set("ch$long_ip", $challenge_uid, $expiration); +} + +// Cooldowns +function twister_captcha_store_cooldown($memcached, $long_ip, $expiration) { + if (!$long_ip || !$expiration) { + return false; + } + + return $memcached->set("cd$long_ip", $expiration, $expiration); +} + +function twister_captcha_get_cooldown($memcached, $long_ip) { + if (!$long_ip) { + return false; + } + + $val = $memcached->get("cd$long_ip"); + + if ($val === false && $memcached->getResultCode() === Memcached::RES_NOTFOUND) { + return 0; + } + + return $val; +} + +function twister_captcha_generate_ticket($now, $long_ip, $board) { + if (!$long_ip || !$now) { + return false; + } + + $hmac_secret = base64_decode(TWISTER_HMAC_SECRET); + + if (!$hmac_secret) { + return false; + } + + $hash = hash_hmac('sha256', "$now.$long_ip.$board", $hmac_secret); + + if (!$hash) { + return false; + } + + return "$now.$hash"; +} + +function twister_captcha_decode_ticket($ticket, $long_ip, $board) { + if (!$long_ip || !$ticket) { + return false; + } + + $hmac_secret = base64_decode(TWISTER_HMAC_SECRET); + + if (!$hmac_secret) { + return false; + } + + list($ts, $hash) = explode('.', $ticket); + + $ts = (int)$ts; + + if (!$ts || !$hash) { + return false; + } + + $this_hash = hash_hmac('sha256', "$ts.$long_ip.$board", $hmac_secret); + + if ($this_hash === $hash) { + return $ts; + } + + return false; +} + +function twister_captcha_should_purge_ticket($ticket, $now) { + if (!$ticket || !$now) { + return false; + } + + list($ts, $hash) = explode('.', $ticket); + + if (!$ts) { + return false; + } + + return $ts + TWISTER_TICKET_TTL <= $now; +} + +function twister_captcha_store_session($memcached, $long_ip, $count, $expiration) { + if (!$long_ip || !$expiration || $count < 1) { + return false; + } + + $key = "us$long_ip"; + + $res = $memcached->replace($key, $count, $expiration); + + if ($res === false) { + if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) { + return $memcached->set($key, $count, $expiration); + } + else { + return false; + } + } + + return true; +} + +function twister_captcha_get_session($memcached, $long_ip) { + if (!$long_ip) { + return false; + } + + $val = $memcached->get("us$long_ip"); + + if ($val === false) { + if ($memcached->getResultCode() === Memcached::RES_NOTFOUND) { + return 0; + } + else { + return false; + } + } + + return $val; +} + +function twister_captcha_get_credits($memcached, $pwd) { + if (!$pwd) { + return false; + } + + $key = "cr-$pwd"; + $val = $memcached->get($key); + + if ($val === false) { + return false; + } + + $val = explode('.', $val); + + $count = (int)$val[0]; + $ts = (int)$val[1]; + + if ($count <= 0 || $ts <= 0) { + $memcached->delete($key); + return false; + } + + return [ $count, $ts ]; +} + +function twister_captcha_get_userpwd($user_ip) { + if (isset($_COOKIE['4chan_pass'])) { + $_c = $_COOKIE['4chan_pass']; + } + else { + $_c = null; + } + + return new UserPwd($user_ip, TWISTER_DOMAIN, $_COOKIE['4chan_pass']); +} + +// --- + +// Dummy page for cloudflare challenges +if (isset($_GET['opened'])) { + twister_captcha_output_dummy(); + die(); +} + +// --- + +// Block TOR immediately +if ($_SERVER['HTTP_X_GEO_CONTINENT'] === 'T1') { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (T1)'); +} + +// Check parameters +if (isset($_GET['board']) && preg_match('/^[!0-9a-z]{1,10}$/', $_GET['board'])) { + $param_board = $_GET['board']; +} +else { + $param_board = '!'; +} + +if (isset($_GET['thread_id']) && $_GET['thread_id']) { + $param_thread_id = (int)$_GET['thread_id']; +} +else { + $param_thread_id = 0; +} + +$now = $_SERVER['REQUEST_TIME']; +$user_ip = $_SERVER['REMOTE_ADDR']; +$user_long_ip = ip2long($user_ip); + +if (!isset($_SERVER['HTTP_USER_AGENT'])) { + $user_agent = '!'; +} +else { + $user_agent = md5($_SERVER['HTTP_USER_AGENT']); +} + +$userpwd = twister_captcha_get_userpwd($user_ip); + +if (!$userpwd) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (GU1)'); +} + +if (!$userpwd->isNew() && $param_board !== '!signin') { + $password = $userpwd->getPwd(); +} +else { + $password = '!'; +} + +// --- + +$use_static = false; +$difficulty = TwisterCaptcha::LEVEL_NORMAL; +$char_count = TWISTER_CHARS_MAX; + +// --- + +$m = new Memcached(); + +// Only call the following once (when getServerList() is empty) if using persistent connections +//$m->setOption(Memcached::OPT_TCP_NODELAY, true); +$m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); +$m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms +$m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms + +// Only use one server. Having multiple servers will break the captcha +// as "set" is used instead of "replace + add" +if ($m->addServer(TWISTER_MEMCACHED_HOST, TWISTER_MEMCACHED_PORT) === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (c0)'); +} + +// --- + +// If CF's bot score is low, store the information for 1h and use it during posting +twister_captcha_check_likely_automated($m, $now); + +/** + * Pre-cooldowns + */ +$pcd = 0; + +// Posting a thread +if ($param_thread_id === 0) { + if ($userpwd->threadCount() < 1 || ($userpwd->ipChanged() && $userpwd->postCount() < 2)) { + $pcd = TWISTER_PRE_CD_THREAD; + } +} +// Posting a reply (reporting uses a thread id of 1) +else if ($param_thread_id !== 1) { + if ($userpwd->postCount() < 1 || ($userpwd->ipChanged() && $userpwd->postCount() < 2)) { + $pcd = TWISTER_PRE_CD_REPLY; + } +} +// Reporting a post +else if ($param_board !== '!') { + if ($userpwd->reportCount() < 1 && $userpwd->postCount() < 1) { + $pcd = TWISTER_PRE_CD_REPORT; + } +} + +if ($param_thread_id != 1 && !$userpwd->verifiedLevel()) { + // Initial cooldown, bypassable by verifying your email + if ($userpwd->postCount() < 1 || !$userpwd->isUserKnown(15)) { + $pcd = 900; + } + // Cooldown for when the user is still new and the mask changes + else if ($userpwd->postCount() > 0 && $userpwd->pwdLifetime() < 86400 && $userpwd->maskChanged()) { // 24h + $pcd = 180; + } +} +$pcd = 0; // FIXME +// Pre-cooldown needed +if ($pcd > 0 && $param_board !== '!signin') { + // Extra pre-cooldown for unverified users + if (!$userpwd->verifiedLevel()) { + $pcd += twister_captcha_get_pcd_penalty($param_board, $param_thread_id); + } + + $ticket_ts = twister_captcha_decode_ticket($_GET['ticket'], $user_long_ip, $param_board); + + $bypassable = false; + + if ($param_thread_id === 0) { + $pcd_msg = TWISTER_ERR_PCD_THREAD; + } + else if ($param_thread_id !== 1 && $param_board !== '!') { + $pcd_msg = TWISTER_ERR_PCD_REPLY; + } + + if ($param_thread_id !== 1 && $pcd >= 900) { + $pcd_msg = TWISTER_ERR_PCD_SIGNIN; + $bypassable = true; + } + + if (!$ticket_ts) { + //if (TWISTER_USE_TICKET_CAPTCHA && !in_array((int)$_SERVER['HTTP_X_GEO_ASN'], TWISTER_WHITELIST_ASNS)) { + if (TWISTER_USE_TICKET_CAPTCHA && twister_captcha_need_hcaptcha()) { + twister_captcha_process_ticket_captcha($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable); + } + else { + twister_captcha_output_new_ticket($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable); + } + + die(); + } + + $ticket_lifetime = $now - $ticket_ts; + + if ($ticket_lifetime < $pcd) { + $pcd = $pcd - $ticket_lifetime; + twister_captcha_output_ticket_pcd(null, $pcd, $pcd_msg, $bypassable); + die(); + } + + // Ticket expired + if ($ticket_lifetime >= TWISTER_TICKET_TTL) { + if (TWISTER_USE_TICKET_CAPTCHA && twister_captcha_need_hcaptcha()) { + twister_captcha_process_ticket_captcha($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable); + } + else { + twister_captcha_output_new_ticket($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable); + } + + die(); + } +} + +/** + * Adjust difficulty + */ + +$ip_ttl_static = TWISTER_PWD_TTL_IP_STATIC; +$mask_ttl_static = TWISTER_PWD_TTL_MASK_STATIC; + +$ip_ttl_min = TWISTER_PWD_TTL_IP_MIN; +$idle_ttl = TWISTER_PWD_TTL_IDLE; + +// Serve max len twister to bad actors +$bad_actor = false; + +if (!isset($_SERVER['HTTP_USER_AGENT']) || !$_SERVER['HTTP_USER_AGENT']) { + $bad_actor = true; +} +else if (preg_match(TWISTER_BAD_UA, $_SERVER['HTTP_USER_AGENT'])) { + $bad_actor = true; +} +else if (twister_captcha_is_req_suspicious()) { + $bad_actor = true; +} + +// Serve static captcha to known users. +// Serve max length captchas for unknown users. +// Only applies to replies. Theads always use slider captchas. +if ($param_thread_id !== 0 && !$bad_actor) { + // Known and post count check + if ($userpwd->isUserKnown(TWISTER_PWD_STATIC_KNOWN) && $userpwd->postCount() >= TWISTER_PWD_STATIC_POSTS) { + // Inactivity and IP change checks for unverified users + if ($userpwd->verifiedLevel()) { + $use_static = true; + $char_count = TWISTER_CHARS_MIN; + } + else if ($userpwd->idleLifetime() <= TWISTER_PWD_SLIDER_IDLE && !$userpwd->ipChanged()) { + $use_static = true; + $char_count = TWISTER_CHARS; + } + } +} + +// --- + +// Debug controls +if (DEV_MODE) { + if (isset($_GET['mc_stats']) && $_GET['mc_stats']) { + $data = [ + $m->getServerList(), + $m->getStats() + ]; + echo json_encode($data); + die(); + } + + if (isset($_GET['del_key']) && $_GET['del_key']) { + $m->delete($_GET['del_key']); + die("ok"); + } + + if (isset($_GET['get_key']) && $_GET['get_key']) { + var_dump($m->get($_GET['get_key'])); + die(); + } + + if (isset($_GET['mode'])) { + if ($_GET['mode'] === 'static') { + $use_static = true; + } + else if ($_GET['mode'] === 'twister') { + $use_static = false; + } + } + + if (isset($_GET['difficulty']) && $_GET['difficulty']) { + $difficulty = (int)$_GET['difficulty']; + } +} + +// --- + +// Check captcha bypassing credits +if (TWISTER_ALLOW_NOOP && !in_array($param_board, TWISTER_NO_NOOP_BOARDS) && !$bad_actor && $param_thread_id !== 0) { + if ($userpwd->isUserKnown(TWISTER_PWD_NOOP_KNOWN) && $userpwd->postCount() >= TWISTER_PWD_NOOP_POSTS) { + $credits = twister_captcha_get_credits($m, $userpwd->getPwd()); + + if ($credits !== false && $credits[0] > 0) { + $data = [ + 'challenge' => 'noop', + 'ttl' => min(TWISTER_TTL, $credits[1] - $now), + 'cd' => TWISTER_COOLDOWN + ]; + + twister_captcha_output_data($data); + + die(); + } + } +} + +// Check cooldown +$should_cd_until = twister_captcha_get_cooldown($m, $user_long_ip); + +if ($should_cd_until === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (scdu)'); +} + +if ($should_cd_until > 1) { + twister_captcha_error(TWISTER_ERR_COOLDOWN, ['cd' => $should_cd_until - $now]); +} + +// Number of unsolved captchas requested recently +$unsolved_count = twister_captcha_get_session($m, $user_long_ip); + +if ($unsolved_count === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (gus1)'); +} + +if ($unsolved_count > 2) { + $cooldown = TWISTER_COOLDOWN_LONG * min($unsolved_count, 20); + + if ($unsolved_count > 10) { + $cooldown = 300; + } +} +else { + $cooldown = TWISTER_COOLDOWN; +} + +if (twister_captcha_store_cooldown($m, $user_long_ip, $now + $cooldown) === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (sc0)'); +} + +// Generate images +$c = new TwisterCaptcha(TWISTER_FONT_PATH); +$c->setDifficulty($difficulty); + +// Adjust features +// User is suspicious +$should_harden = $bad_actor && ($userpwd->ipChanged() || !$userpwd->isUserKnownOrVerified(7200) || mt_rand(0, 9) === 9); + +$should_fog = false; + +if ($should_harden) { + $use_static = false; + + if (mt_rand(0, 1)) { + $c->useInkBlot(true); + + if (mt_rand(0, 1)) { + $c->useNegateBlotFilter(true); + } + } + else { + $c->useEdgeBlock(true); + } + + $c->useFakeCharPadding(true); + $c->useJumpyMode(true); + + if (mt_rand(0, 9) === 9) { + $c->setDifficulty(TwisterCaptcha::LEVEL_LUNATIC); + } + else { + $c->setDifficulty(TwisterCaptcha::LEVEL_HARD); + } + + if (mt_rand(0, 9) === 9) { + $char_count = 8; + } + + if (mt_rand(0, 1)) { + $c->useGridLines(true); + } + else { + $c->useScoreLines(true); + } +} +// Other new users +else if (true || $userpwd->postCount() < 5 || $userpwd->maskLifetime() < 3600) { + if (!$use_static) { + $_boards = [ 'a', 'v', 'vg', 'co', 'vp', 'g', 'biz', 'b', 'vt', 'mu', 'pol', 'tv', 'sp', 'int', 'soc', 'test' ]; + + if (true || in_array($param_board, $_boards) || $param_thread_id == 0) { + $should_fog = true; + } + else { + $c->useInkBlot(true); + $c->useScoreLines(true); + } + } + else { + $c->useInkBlot(true); + //$c->useFakeCharPadding(true); + + if (mt_rand(0, 1)) { + $c->useJumpyMode(true); + } + } + + $char_count = mt_rand(TWISTER_CHARS, TWISTER_CHARS_MAX); +} + +if ($param_board === '!signin') { + $should_fog = true; + $use_static = false; +} + +if ($use_static) { + if (mt_rand(0, 9) === 9) { + $c->useScoreLines(true); + } + + list($challenge_str, $img, $img_width, $img_height) = $c->generateStatic($char_count); + $img_bg = null; +} +else { + if (true || $should_fog) { + if (false && $param_board == 'co' && $param_thread_id == 0) { + $c->useEdgeDetect(true); + $c->useSpecialRot(true); + //$c->useOverlayId(5, true); + $c->useInvert(true); + //$c->useAltBlackWhite(true); + $c->useGridLines(true); + $c->useSimplexBg(true); + //$c->useEmboss(true); + //list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew($char_count); + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(7, 28, 28); + //list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterV(3); + } + else { + $_olid = mt_rand(0, 6); + + if ($_olid) { + $c->useOverlayId($_olid, (bool)mt_rand(0, 1)); + } + + if (mt_rand(0, 1)) { + $c->useInvert(true); + } + + if (mt_rand(0, 3) === 3) { + $c->useFakeCharPadding(true); + } + + if ($param_thread_id == 0) { + $_olid = 2; + } + else { + $_olid = mt_rand(1, 2); + } + + $_olid = 2; + + // Simplex + if ($_olid === 1) { + if (mt_rand(0, 3) === 3) { + $c->useScoreLines(true); + } + + if (mt_rand(0, 1)) { + $c->useInvert(true); + } + + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHSimplex($char_count); + } + // Fog New + else if ($_olid === 2) { + if (mt_rand(0, 1)) { + $c->useSimplexBg(true); + } + + if (mt_rand(0, 1)) { + $c->useEmboss(true); + } + else { + $c->useEdgeDetect(true); + } + + if (mt_rand(0, 1)) { + $c->useAltBlackWhite(true); + } + + if (mt_rand(0, 1)) { + $c->useInvert(true); + } + + //if (mt_rand(0, 3) === 3) { + //$c->useSpecialRot(true); + //$char_count = 5; + //} + + if (isset($_SERVER['HTTP_X_BOT_SCORE'])) { + $_bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + } + else { + $_bot_score = 100; + } + + if ($param_board === '!signin') { + if ($_bot_score > 95) { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(5, 30, 50); + } + else { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateSimpleTask(2); + } + } + else { + //$c->useSpecialRot(true); + + if ($_bot_score <= 70 && mt_rand(0, 2) === 0) { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateSimpleTask(2); + } + else { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(5, 30, 50); + } + } + } + // Default + else { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew($char_count); + } + } + } + else { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterH($char_count); + } +} + +if (!$challenge_str) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (ncs1)'); +} + +list($challenge_uid, $challenge_hash) = TwisterCaptcha::getChallengeHash( + $challenge_str, + [$user_ip, $password, $user_agent, $param_board, $param_thread_id] +); + +if (!$challenge_uid || !$challenge_hash) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (gch1)'); +} + +// Register challenge +if (twister_captcha_store_challenge($m, $challenge_uid, $user_long_ip, $now + TWISTER_TTL) === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (sch1)'); +} + +// Register unsolved session +//if (twister_captcha_store_session($m, $user_long_ip, $unsolved_count + 1, $now + TWISTER_TTL_UNSOLVED) === false) { +// twister_captcha_error(TWISTER_ERR_GENERIC . ' (sus1)'); +//} + +// Generate base 64 urls of images +list($img_b64, $img_bg_b64) = TwisterCaptcha::getBase64Images($img, $img_bg); + +$data = [ + 'challenge' => "$challenge_uid.$challenge_hash", + 'ttl' => TWISTER_TTL, + 'cd' => $cooldown, + 'img' => $img_b64, + 'img_width' => $img_width, + 'img_height' => $img_height +]; + +if ($img_bg) { + $data['bg'] = $img_bg_b64; + $data['bg_width'] = $bg_width; +} + +if (twister_captcha_should_purge_ticket($_GET['ticket'], $now)) { + $data['ticket'] = false; +} + +twister_captcha_output_data($data); diff --git a/captcha.php b/captcha.php new file mode 100644 index 0000000..9942284 --- /dev/null +++ b/captcha.php @@ -0,0 +1,1075 @@ +or verify your email address before making a post.

Click here for more information
or to verify your email.'); + +// --- + +function twister_captcha_output_data($data) { + if (isset($_GET['framed'])) { + twister_captcha_output_html($data); + } + else { + header('Content-Type: application/json'); + echo json_encode($data); + } +} + +function twister_captcha_output_html($data) { + header('Content-Security-Policy: frame-ancestors https://*.' . TWISTER_DOMAIN . ';'); + $now = $_SERVER['REQUEST_TIME']; +?> + + + + +

You can now close this page and try getting a captcha again.

+ true, 'ticket' => false ]); +} + +function twister_captcha_get_ticket_captcha_response() { + if (isset($_GET['ticket_resp'])) { + return $_GET['ticket_resp']; + } + + return false; +} + +function twister_captcha_get_hcaptcha_private_key() { + $path = '/www/global/yotsuba/config/captcha_config.ini'; + + $cfg = file_get_contents($path); + + if (!$cfg) { + return false; + } + + $res = preg_match('/^HCAPTCHA_API_KEY_PRIVATE ?= ?([^\s]+)$/m', $cfg, $m); + + if (!$res || empty($m) || !$m[1]) { + return false; + } + + return $m[1]; +} + +function twister_captcha_verify_ticket_captcha() { + $response = twister_captcha_get_ticket_captcha_response(); + + if (!$response || strlen($response) > 4096) { + return false; + } + + $captcha_private_key = twister_captcha_get_hcaptcha_private_key(); + + if (!$captcha_private_key) { + // Don't block in case of misconfiguration on our end + return true; + } + + $url = 'https://hcaptcha.com/siteverify'; + + $post = array( + 'secret' => $captcha_private_key, + 'response' => $response, + 'remoteip' => $_SERVER['REMOTE_ADDR'], + 'sitekey' => TWISTER_HCAPTCHA_SITEKEY, + ); + + $curl = curl_init(); + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2); + curl_setopt($curl, CURLOPT_TIMEOUT, 4); + curl_setopt($curl, CURLOPT_USERAGENT, '4chan'); + curl_setopt($curl, CURLOPT_POSTFIELDS, $post); + + $resp = curl_exec($curl); + + if ($resp === false) { + curl_close($curl); + return false; + } + + $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($resp_status >= 300) { + curl_close($curl); + return false; + } + + curl_close($curl); + + $json = json_decode($resp, true); + + // BAD + if (json_last_error() !== JSON_ERROR_NONE) { + return false; + } + + // GOOD + if ($json && isset($json['success']) && $json['success']) { + return true; + } + + // BAD + return false; +} + +function twister_captcha_process_ticket_captcha($pcd, $now, $long_ip, $board, $msg = null, $bypassable = false) { + // Captcha response was sent, verify it + if (twister_captcha_get_ticket_captcha_response()) { + if (twister_captcha_verify_ticket_captcha() === true) { + // Captcha is valid, return a new ticket + twister_captcha_output_new_ticket($pcd, $now, $long_ip, $board, $msg, $bypassable); + } + else { + // Wrong captcha or captcha malfunction + twister_captcha_error(TWISTER_ERR_GENERIC . ' (tcr0)'); + } + } + // No captcha reponse provided, tell the frontend to show a ticket captcha + else { + twister_captcha_output_ticket_captcha(); + } +} + +function twister_captcha_output_new_ticket($pcd, $now, $long_ip, $board, $msg = null, $bypassable = false) { + $ticket = twister_captcha_generate_ticket($now, $long_ip, $board); + + if (!$ticket) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (gt1)'); + } + + twister_captcha_output_ticket_pcd($ticket, $pcd, $msg, $bypassable); +} + +function twister_captcha_output_ticket_pcd($ticket, $pcd, $msg, $bypassable) { + $data = []; + + if ($ticket) { + $data['ticket'] = $ticket; + } + + $data['pcd'] = $pcd; + + if ($msg) { + $data['pcd_msg'] = $msg; + } + + if ($bypassable) { + $data['bpcd'] = true; + } + + twister_captcha_output_data($data); +} + +function twister_captcha_error($msg, $extra = null) { + //http_response_code(500); + $data = [ 'error' => $msg ]; + + if ($extra) { + $data = array_merge($data, $extra); + } + + twister_captcha_output_data($data); + + die(); +} + +function twister_captcha_is_req_suspicious() { + /* + if (isset($_SERVER['HTTP_X_HTTP_VERSION'])) { + if (strpos($_SERVER['HTTP_X_HTTP_VERSION'], 'HTTP/1') === 0) { + return true; + } + } + */ + $no_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) === false; + $no_accept = isset($_SERVER['HTTP_ACCEPT']) === false; + + if ($no_lang && $no_accept) { + return true; + } + + if ($no_lang && strpos($_SERVER['HTTP_USER_AGENT'], '; wv)') !== false) { + return true; + } + + if ($no_accept && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') === false) { + return true; + } + + if ($no_lang && isset($_SERVER['HTTP_REFERER'])) { + $ref = $_SERVER['HTTP_REFERER']; + + if (strpos($ref, 'sys.4chan.org') !== false || strpos($ref, '/thread/') !== false) { + return true; + } + } + + return false; +} + +function twister_captcha_need_hcaptcha() { + if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { + return true; + } + + $ua = $_SERVER['HTTP_USER_AGENT']; + + $score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + + // Skip Android Webviews + if ($score == 1 && strpos($ua, '; wv)') !== false) { + return false; + } + + return $score < 99; +} + +function twister_captcha_check_likely_automated($memcached, $now, $threshold = 29) { + if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { + return false; + } + + $ua = $_SERVER['HTTP_USER_AGENT']; + + // Skip Android Webviews + if (strpos($ua, '; wv)') !== false) { + return false; + } + + // Skip iPhone Webviews + if (preg_match('/iPhone|iPad/', $ua) && !preg_match('/Mobile|Safari/', $ua)) { + return false; + } + + $score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + + if ($score > 1 && $score <= $threshold) { + $key = 'bmbot' . $_SERVER['REMOTE_ADDR']; + $memcached->set($key, 1, $now + 43200); + return true; + } + + return false; +} + +function twister_captcha_get_pcd_penalty($board, $thread_id) { + $count = 0; + + return 0; // FIXME + + // Reports + if ($thread_id === 1 && $board !== '!') { + return 0; + } + + if (isset($_SERVER['HTTP_X_GEO_ASN'])) { + $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; + } + else { + $asn = 0; + } + + if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { + $country = $_SERVER['HTTP_X_GEO_COUNTRY']; + } + else { + $country = null; + } + + // Mobile clients + if ($asn > 0 && in_array($asn, TWISTER_MOBILE_ASNS)) { + $count++; + } + else if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/Mobile|iPhone|; wv/', $_SERVER['HTTP_USER_AGENT'])) { + $count++; + } + + // Rare countries + if ($country && !in_array($country, TWISTER_WHITELIST_COUNTRIES)) { + $count++; + } + + // Rare ISPs + if ($asn > 0 && !in_array($asn, TWISTER_WHITELIST_ASNS)) { + $count++; + } + + return $count * TWISTER_PRE_CD_PENALITY; +} + +function twister_captcha_store_challenge($memcached, $challenge_uid, $long_ip, $expiration) { + if (!$challenge_uid || !$long_ip || !$expiration) { + return false; + } + + return $memcached->set("ch$long_ip", $challenge_uid, $expiration); +} + +// Cooldowns +function twister_captcha_store_cooldown($memcached, $long_ip, $expiration) { + if (!$long_ip || !$expiration) { + return false; + } + + return $memcached->set("cd$long_ip", $expiration, $expiration); +} + +function twister_captcha_get_cooldown($memcached, $long_ip) { + if (!$long_ip) { + return false; + } + + $val = $memcached->get("cd$long_ip"); + + if ($val === false && $memcached->getResultCode() === Memcached::RES_NOTFOUND) { + return 0; + } + + return $val; +} + +function twister_captcha_generate_ticket($now, $long_ip, $board) { + if (!$long_ip || !$now) { + return false; + } + + $hmac_secret = base64_decode(TWISTER_HMAC_SECRET); + + if (!$hmac_secret) { + return false; + } + + $hash = hash_hmac('sha256', "$now.$long_ip.$board", $hmac_secret); + + if (!$hash) { + return false; + } + + return "$now.$hash"; +} + +function twister_captcha_decode_ticket($ticket, $long_ip, $board) { + if (!$long_ip || !$ticket) { + return false; + } + + $hmac_secret = base64_decode(TWISTER_HMAC_SECRET); + + if (!$hmac_secret) { + return false; + } + + list($ts, $hash) = explode('.', $ticket); + + $ts = (int)$ts; + + if (!$ts || !$hash) { + return false; + } + + $this_hash = hash_hmac('sha256', "$ts.$long_ip.$board", $hmac_secret); + + if ($this_hash === $hash) { + return $ts; + } + + return false; +} + +function twister_captcha_should_purge_ticket($ticket, $now) { + if (!$ticket || !$now) { + return false; + } + + list($ts, $hash) = explode('.', $ticket); + + if (!$ts) { + return false; + } + + return $ts + TWISTER_TICKET_TTL <= $now; +} + +function twister_captcha_store_session($memcached, $long_ip, $count, $expiration) { + if (!$long_ip || !$expiration || $count < 1) { + return false; + } + + $key = "us$long_ip"; + + $res = $memcached->replace($key, $count, $expiration); + + if ($res === false) { + if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) { + return $memcached->set($key, $count, $expiration); + } + else { + return false; + } + } + + return true; +} + +function twister_captcha_get_session($memcached, $long_ip) { + if (!$long_ip) { + return false; + } + + $val = $memcached->get("us$long_ip"); + + if ($val === false) { + if ($memcached->getResultCode() === Memcached::RES_NOTFOUND) { + return 0; + } + else { + return false; + } + } + + return $val; +} + +function twister_captcha_get_credits($memcached, $pwd) { + if (!$pwd) { + return false; + } + + $key = "cr-$pwd"; + $val = $memcached->get($key); + + if ($val === false) { + return false; + } + + $val = explode('.', $val); + + $count = (int)$val[0]; + $ts = (int)$val[1]; + + if ($count <= 0 || $ts <= 0) { + $memcached->delete($key); + return false; + } + + return [ $count, $ts ]; +} + +function twister_captcha_get_userpwd($user_ip) { + if (isset($_COOKIE['4chan_pass'])) { + $_c = $_COOKIE['4chan_pass']; + } + else { + $_c = null; + } + + return new UserPwd($user_ip, TWISTER_DOMAIN, $_COOKIE['4chan_pass']); +} + +// --- + +// Dummy page for cloudflare challenges +if (isset($_GET['opened'])) { + twister_captcha_output_dummy(); + die(); +} + +// --- + +// Block TOR immediately +if ($_SERVER['HTTP_X_GEO_CONTINENT'] === 'T1') { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (T1)'); +} + +// Check parameters +if (isset($_GET['board']) && preg_match('/^[!0-9a-z]{1,10}$/', $_GET['board'])) { + $param_board = $_GET['board']; +} +else { + $param_board = '!'; +} + +if (isset($_GET['thread_id']) && $_GET['thread_id']) { + $param_thread_id = (int)$_GET['thread_id']; +} +else { + $param_thread_id = 0; +} + +$now = $_SERVER['REQUEST_TIME']; +$user_ip = $_SERVER['REMOTE_ADDR']; +$user_long_ip = ip2long($user_ip); + +if (!isset($_SERVER['HTTP_USER_AGENT'])) { + $user_agent = '!'; +} +else { + $user_agent = md5($_SERVER['HTTP_USER_AGENT']); +} + +$userpwd = twister_captcha_get_userpwd($user_ip); + +if (!$userpwd) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (GU1)'); +} + +if (!$userpwd->isNew() && $param_board !== '!signin') { + $password = $userpwd->getPwd(); +} +else { + $password = '!'; +} + +// --- + +$use_static = false; +$difficulty = TwisterCaptcha::LEVEL_NORMAL; +$char_count = TWISTER_CHARS_MAX; + +// --- + +$m = new Memcached(); + +// Only call the following once (when getServerList() is empty) if using persistent connections +//$m->setOption(Memcached::OPT_TCP_NODELAY, true); +$m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); +$m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms +$m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms + +// Only use one server. Having multiple servers will break the captcha +// as "set" is used instead of "replace + add" +if ($m->addServer(TWISTER_MEMCACHED_HOST, TWISTER_MEMCACHED_PORT) === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (c0)'); +} + +// --- + +// If CF's bot score is low, store the information for 1h and use it during posting +twister_captcha_check_likely_automated($m, $now); + +/** + * Pre-cooldowns + */ +$pcd = 0; + +// Posting a thread +if ($param_thread_id === 0) { + if ($userpwd->threadCount() < 1 || ($userpwd->ipChanged() && $userpwd->postCount() < 2)) { + $pcd = TWISTER_PRE_CD_THREAD; + } +} +// Posting a reply (reporting uses a thread id of 1) +else if ($param_thread_id !== 1) { + if ($userpwd->postCount() < 1 || ($userpwd->ipChanged() && $userpwd->postCount() < 2)) { + $pcd = TWISTER_PRE_CD_REPLY; + } +} +// Reporting a post +else if ($param_board !== '!') { + if ($userpwd->reportCount() < 1 && $userpwd->postCount() < 1) { + $pcd = TWISTER_PRE_CD_REPORT; + } +} + +if ($param_thread_id != 1 && !$userpwd->verifiedLevel()) { + // Initial cooldown, bypassable by verifying your email + if ($userpwd->postCount() < 1 || !$userpwd->isUserKnown(15)) { + $pcd = 900; + } + // Cooldown for when the user is still new and the mask changes + else if ($userpwd->postCount() > 0 && $userpwd->pwdLifetime() < 86400 && $userpwd->maskChanged()) { // 24h + $pcd = 180; + } +} + +// Pre-cooldown needed +if ($pcd > 0 && $param_board !== '!signin') { + // Extra pre-cooldown for unverified users + if (!$userpwd->verifiedLevel()) { + $pcd += twister_captcha_get_pcd_penalty($param_board, $param_thread_id); + } + + $ticket_ts = twister_captcha_decode_ticket($_GET['ticket'], $user_long_ip, $param_board); + + $bypassable = false; + + if ($param_thread_id === 0) { + $pcd_msg = TWISTER_ERR_PCD_THREAD; + } + else if ($param_thread_id !== 1 && $param_board !== '!') { + $pcd_msg = TWISTER_ERR_PCD_REPLY; + } + + if ($param_thread_id !== 1 && $pcd >= 900) { + $pcd_msg = TWISTER_ERR_PCD_SIGNIN; + $bypassable = true; + } + + if (!$ticket_ts) { + //if (TWISTER_USE_TICKET_CAPTCHA && !in_array((int)$_SERVER['HTTP_X_GEO_ASN'], TWISTER_WHITELIST_ASNS)) { + if (TWISTER_USE_TICKET_CAPTCHA && twister_captcha_need_hcaptcha()) { + twister_captcha_process_ticket_captcha($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable); + } + else { + twister_captcha_output_new_ticket($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable); + } + + die(); + } + + $ticket_lifetime = $now - $ticket_ts; + + if ($ticket_lifetime < $pcd) { + $pcd = $pcd - $ticket_lifetime; + twister_captcha_output_ticket_pcd(null, $pcd, $pcd_msg, $bypassable); + die(); + } + + // Ticket expired + if ($ticket_lifetime >= TWISTER_TICKET_TTL) { + if (TWISTER_USE_TICKET_CAPTCHA && twister_captcha_need_hcaptcha()) { + twister_captcha_process_ticket_captcha($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable); + } + else { + twister_captcha_output_new_ticket($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable); + } + + die(); + } +} + +/** + * Adjust difficulty + */ + +$ip_ttl_static = TWISTER_PWD_TTL_IP_STATIC; +$mask_ttl_static = TWISTER_PWD_TTL_MASK_STATIC; + +$ip_ttl_min = TWISTER_PWD_TTL_IP_MIN; +$idle_ttl = TWISTER_PWD_TTL_IDLE; + +// Serve max len twister to bad actors +$bad_actor = false; + +if (!isset($_SERVER['HTTP_USER_AGENT']) || !$_SERVER['HTTP_USER_AGENT']) { + $bad_actor = true; +} +else if (preg_match(TWISTER_BAD_UA, $_SERVER['HTTP_USER_AGENT'])) { + $bad_actor = true; +} +else if (twister_captcha_is_req_suspicious()) { + $bad_actor = true; +} + +// Serve static captcha to known users. +// Serve max length captchas for unknown users. +// Only applies to replies. Theads always use slider captchas. +if ($param_thread_id !== 0 && !$bad_actor) { + // Known and post count check + if ($userpwd->isUserKnown(TWISTER_PWD_STATIC_KNOWN) && $userpwd->postCount() >= TWISTER_PWD_STATIC_POSTS) { + // Inactivity and IP change checks for unverified users + if ($userpwd->verifiedLevel()) { + $use_static = true; + $char_count = TWISTER_CHARS_MIN; + } + else if ($userpwd->idleLifetime() <= TWISTER_PWD_SLIDER_IDLE && !$userpwd->ipChanged()) { + $use_static = true; + $char_count = TWISTER_CHARS; + } + } +} + +// Check captcha bypassing credits +if (TWISTER_ALLOW_NOOP && !in_array($param_board, TWISTER_NO_NOOP_BOARDS) && !$bad_actor && $param_thread_id !== 0) { + if ($userpwd->isUserKnown(TWISTER_PWD_NOOP_KNOWN) && $userpwd->postCount() >= TWISTER_PWD_NOOP_POSTS) { + $credits = twister_captcha_get_credits($m, $userpwd->getPwd()); + + if ($credits !== false && $credits[0] > 0) { + $data = [ + 'challenge' => 'noop', + 'ttl' => min(TWISTER_TTL, $credits[1] - $now), + 'cd' => TWISTER_COOLDOWN + ]; + + twister_captcha_output_data($data); + + die(); + } + } +} + +// Check cooldown +$should_cd_until = twister_captcha_get_cooldown($m, $user_long_ip); + +if ($should_cd_until === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (scdu)'); +} + +if ($should_cd_until > 1) { + twister_captcha_error(TWISTER_ERR_COOLDOWN, ['cd' => $should_cd_until - $now]); +} + +// Number of unsolved captchas requested recently +$unsolved_count = twister_captcha_get_session($m, $user_long_ip); + +if ($unsolved_count === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (gus1)'); +} + +if ($unsolved_count > 2) { + $cooldown = TWISTER_COOLDOWN_LONG * min($unsolved_count, 20); + + if ($unsolved_count > 10) { + $cooldown = 300; + } +} +else { + $cooldown = TWISTER_COOLDOWN; +} + +if (twister_captcha_store_cooldown($m, $user_long_ip, $now + $cooldown) === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (sc0)'); +} + +// Generate images +$c = new TwisterCaptcha(TWISTER_FONT_PATH); +$c->setDifficulty($difficulty); + +// Adjust features +// User is suspicious +$should_harden = $bad_actor && ($userpwd->ipChanged() || !$userpwd->isUserKnownOrVerified(7200) || mt_rand(0, 9) === 9); + +$should_fog = false; + +if ($should_harden) { + $use_static = false; + + if (mt_rand(0, 1)) { + $c->useInkBlot(true); + + if (mt_rand(0, 1)) { + $c->useNegateBlotFilter(true); + } + } + else { + $c->useEdgeBlock(true); + } + + $c->useFakeCharPadding(true); + $c->useJumpyMode(true); + + if (mt_rand(0, 9) === 9) { + $c->setDifficulty(TwisterCaptcha::LEVEL_LUNATIC); + } + else { + $c->setDifficulty(TwisterCaptcha::LEVEL_HARD); + } + + if (mt_rand(0, 9) === 9) { + $char_count = 8; + } + + if (mt_rand(0, 1)) { + $c->useGridLines(true); + } + else { + $c->useScoreLines(true); + } +} +// Other new users +else if ($userpwd->postCount() < 5 || $userpwd->maskLifetime() < 3600) { + if (!$use_static) { + $_boards = [ 'a', 'v', 'vg', 'co', 'vp', 'g', 'biz', 'b', 'vt', 'mu', 'pol', 'tv', 'sp', 'int', 'soc', 'test' ]; + + if (true || in_array($param_board, $_boards) || $param_thread_id == 0) { + $should_fog = true; + } + else { + $c->useInkBlot(true); + $c->useScoreLines(true); + } + } + else { + $c->useInkBlot(true); + //$c->useFakeCharPadding(true); + + if (mt_rand(0, 1)) { + $c->useJumpyMode(true); + } + } + + $char_count = mt_rand(TWISTER_CHARS, TWISTER_CHARS_MAX); +} + +if ($param_board === '!signin') { + $should_fog = true; + $use_static = false; +} + +if ($use_static) { + if (mt_rand(0, 9) === 9) { + $c->useScoreLines(true); + } + + list($challenge_str, $img, $img_width, $img_height) = $c->generateStatic($char_count); + $img_bg = null; +} +else { + if ($should_fog) { + if (false && $param_board == 'co' && $param_thread_id == 0) { + $c->useEdgeDetect(true); + $c->useSpecialRot(true); + //$c->useOverlayId(5, true); + $c->useInvert(true); + //$c->useAltBlackWhite(true); + $c->useGridLines(true); + $c->useSimplexBg(true); + //$c->useEmboss(true); + //list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew($char_count); + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(7, 28, 28); + //list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterV(3); + } + else { + $_olid = mt_rand(0, 6); + + if ($_olid) { + $c->useOverlayId($_olid, (bool)mt_rand(0, 1)); + } + + if (mt_rand(0, 1)) { + $c->useInvert(true); + } + + if (mt_rand(0, 3) === 3) { + $c->useFakeCharPadding(true); + } + + if ($param_thread_id == 0) { + $_olid = 2; + } + else { + $_olid = mt_rand(1, 2); + } + + $_olid = 2; + + // Simplex + if ($_olid === 1) { + if (mt_rand(0, 3) === 3) { + $c->useScoreLines(true); + } + + if (mt_rand(0, 1)) { + $c->useInvert(true); + } + + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHSimplex($char_count); + } + // Fog New + else if ($_olid === 2) { + if (mt_rand(0, 1)) { + $c->useSimplexBg(true); + } + + if (mt_rand(0, 1)) { + $c->useEmboss(true); + } + else { + $c->useEdgeDetect(true); + } + + if (mt_rand(0, 1)) { + $c->useAltBlackWhite(true); + } + + if (mt_rand(0, 1)) { + $c->useInvert(true); + } + + //if (mt_rand(0, 3) === 3) { + //$c->useSpecialRot(true); + //$char_count = 5; + //} + + if (isset($_SERVER['HTTP_X_BOT_SCORE'])) { + $_bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + } + else { + $_bot_score = 100; + } + + if ($param_board === '!signin') { + if ($_bot_score > 95) { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(5, 30, 50); + } + else { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateSimpleTask(2); + } + } + else { + //$c->useSpecialRot(true); + + if ($_bot_score <= 80 && mt_rand(0, 1) === 0) { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateSimpleTask(2); + } + else { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(5, 30, 50); + } + } + } + // Default + else { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew($char_count); + } + } + } + else { + list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterH($char_count); + } +} + +if (!$challenge_str) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (ncs1)'); +} + +list($challenge_uid, $challenge_hash) = TwisterCaptcha::getChallengeHash( + $challenge_str, + [$user_ip, $password, $user_agent, $param_board, $param_thread_id] +); + +if (!$challenge_uid || !$challenge_hash) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (gch1)'); +} + +// Register challenge +if (twister_captcha_store_challenge($m, $challenge_uid, $user_long_ip, $now + TWISTER_TTL) === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (sch1)'); +} + +// Register unsolved session +if (twister_captcha_store_session($m, $user_long_ip, $unsolved_count + 1, $now + TWISTER_TTL_UNSOLVED) === false) { + twister_captcha_error(TWISTER_ERR_GENERIC . ' (sus1)'); +} + +// Generate base 64 urls of images +list($img_b64, $img_bg_b64) = TwisterCaptcha::getBase64Images($img, $img_bg); + +$data = [ + 'challenge' => "$challenge_uid.$challenge_hash", + 'ttl' => TWISTER_TTL, + 'cd' => $cooldown, + 'img' => $img_b64, + 'img_width' => $img_width, + 'img_height' => $img_height +]; + +if ($img_bg) { + $data['bg'] = $img_bg_b64; + $data['bg_width'] = $bg_width; +} + +if (twister_captcha_should_purge_ticket($_GET['ticket'], $now)) { + $data['ticket'] = false; +} + +twister_captcha_output_data($data); diff --git a/catalog-test.php b/catalog-test.php new file mode 100644 index 0000000..9d469c5 --- /dev/null +++ b/catalog-test.php @@ -0,0 +1,553 @@ + $catalogjson, + + 'count' => count( $log['THREADS'] ), + 'slug' => BOARD_DIR, + 'anon' => S_ANONAME, + 'mtime' => time(), + 'pagesize' => DEF_PAGES + ); + + if (!REPLIES_SHOWN && IS_REBUILDD) { + $catalogjson['no_lr'] = true; + } + + if (SHOW_COUNTRY_FLAGS) { + $catalogjson['flags'] = true; + } + + if( SPOILERS ) $catalogjson['custom_spoiler'] = (int)SPOILER_NUM; + + $catalogjson = json_encode( $catalogjson ); + + $catalog = catalog( $catalogjson ); + + print_page( INDEX_DIR . 'catalog.html', $catalog ); + + return true; +} + +function catalog_thread($res, &$json, $pos) +{ + global $log; + + $reps = $res['replycount']; + $sub = $res['sub']; + + $imgs = $res['imgreplycount']; + $last_reply_id = null; + $capcodelist = array(); + + if (TEXT_ONLY) { + $time_prop = 'now'; + } + else { + $time_prop = 'time'; + } + + foreach( $res['children'] as $reply => $unused ) { + $last_reply_id = $reply; + + if( META_BOARD && $log[$reply]['capcode'] != 'none' ) { + $tCapcode = $log[$reply]['capcode']; + if( $tCapcode == 'admin_highlight' ) $tCapcode = 'admin'; + + if( $tCapcode != 'none' ) { + $capcodelist[$tCapcode] = 1; + } + } + } + + if ($last_reply_id === null) { + $last_reply = array( 'id' => $res['no'] ); + } + else { + $lr_data = $log[$last_reply_id]; + + $last_reply = array( + 'id' => $last_reply_id, + 'date' => $lr_data['time'] + ); + + if( $lr_data['capcode'] != 'none' ) $last_reply['capcode'] = $lr_data['capcode']; + + $force_anon = ( ( FORCED_ANON || META_BOARD ) && $lr_data['capcode'] != 'admin' && $lr_data['capcode'] != 'admin_highlight' ); + + if( !$force_anon ) { + if( strpos( $lr_data['name'], ' ' ) !== false ) { + list( $last_reply['author'], $last_reply['trip'] ) = explode( ' ', $lr_data['name'] ); + } else { + $last_reply['author'] = $lr_data['name']; + } + } else { + $last_reply['author'] = S_ANONAME; + } + } + + $json[$res['no']] = array( + 'date' => $res[$time_prop], + 'file' => mb_convert_encoding($res['filename'], 'UTF-8', 'UTF-8') . $res['ext'], + 'r' => $reps, + 'i' => $imgs, + 'lr' => $last_reply, + 'b' => $pos + ); + /* + if( META_BOARD && $capcodelist ) { + $json[$res['no']]['capcodereps'] = implode(',', array_keys($capcodelist)); + } + */ + if ($res['capcode'] == 'none') { + if (SHOW_COUNTRY_FLAGS && (!ENABLE_BOARD_FLAGS || $res['board_flag'] == '')) { + $json[$res['no']]['country'] = $res['country']; + } + } + + $com = $res['com']; + + if( strpos( $com, 'class="abbr"' ) !== false ) { + $com = preg_replace( '#(
)+(.+)$#s', '', $com ); + } + + if (!TEXT_ONLY) { + $com = preg_replace( '#(
)+#', ' ', $com ); + } + else { + $com = preg_replace( '#(
)+#', "\n", $com ); + } + + if (BOARD_DIR === 'b') { // fixme, hardcoded for now + $com = truncate_comment($com, 300, true); + } + else { + if (SJIS_TAGS) { + $com = preg_replace('//', '[SJIS]', $com); + } + $com = strip_tags($com, ''); + } + + $has_spoilers = (bool)SPOILERS; + + if (!$res['permaage'] && !$res['sticky']) { + if( $reps >= MAX_RES ) $json[$res['no']]['bumplimit'] = 1; + if( $imgs >= MAX_IMGRES ) $json[$res['no']]['imagelimit'] = 1; + } + + if( $res['sticky'] ) $json[$res['no']]['sticky'] = 1; + if( $res['closed'] ) $json[$res['no']]['closed'] = 1; + + if( $res['capcode'] != 'none' ) $json[$res['no']]['capcode'] = $res['capcode']; + + $force_anon = ( ( FORCED_ANON || META_BOARD ) && $res['capcode'] != 'admin' && $res['capcode'] != 'admin_highlight' ); + + if( !$force_anon ) { + if( strpos( $res['name'], ' ' ) !== false ) { + list( $json[$res['no']]['author'], $json[$res['no']]['trip'] ) = explode( ' ', $res['name'] ); + } else { + $json[$res['no']]['author'] = $res['name']; + } + } else { + $json[$res['no']]['author'] = S_ANONAME; + } + + if( $res['fsize'] != 0 && $res['filedeleted'] != 1 ) { + $json[$res['no']]['imgurl'] = $res['tim']; + $json[$res['no']]['tn_w'] = $res['tn_w']; + $json[$res['no']]['tn_h'] = $res['tn_h']; + } + + if( $res['filedeleted'] == 1 ) $json[$res['no']]['imgdel'] = true; + + if( strpos( $res['sub'], 'SPOILER<>' ) !== false ) { + $json[$res['no']]['imgspoiler'] = true; + $sub = substr( $res['sub'], 9 ); + } + + $json[$res['no']]['sub'] = $sub; + $json[$res['no']]['teaser'] = $com; +} + +function catalog($catjson) { + $nav = file_get_contents_cached( NAV_TXT ); + $foot = file_get_contents_cached( NAV2_TXT ); + + $nav = preg_replace( '/href="(\/\/boards.(?:4chan|4channel).org)?\/([a-z0-9]+)\/"/', 'href="$1/$2/catalog"', $nav ); + $nav = preg_replace( '/href="(\/\/boards.(?:4chan|4channel).org)?\/f\/catalog"/', 'href="$1/f/"', $nav ); + + $title = strip_tags( TITLE ); + + $js = ''; + + // danbo ads start + if (defined('ADS_DANBO') && ADS_DANBO) { + $js .= ''; + + $js .= ''; + } + // danbo ads end + + // PubFuture + if (DEFAULT_BURICHAN) { + $js .= ''; + } + + if (TEST_BOARD) { + // Main catalog JS + $js .= ''; + + // Painter JS + CSS + if (ENABLE_PAINTERJS) { + $js .= '' + . ''; + } + + // Core JS + $js .= ''; + } + else { + // Main catalog JS + $js .= ''; + + // Painter JS + CSS + if (ENABLE_PAINTERJS) { + $js .= '' + . ''; + } + + // Core JS + $js .= ''; + } + + $css = STATIC_SERVER . 'css'; + $cssv = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION_CATALOG; + $style_group = style_group(); + + $flags = SHOW_COUNTRY_FLAGS ? '' : ''; + + $titlepart = $subtitle = ''; + + if( TITLE_IMAGE_TYPE == 1 ) { + $titleimg = rand_from_flatfile( YOTSUBA_DIR, 'title_banners.txt' ); + $titlepart .= '
'; + } elseif( TITLE_IMAGE_TYPE == 2 ) { + $titlepart .= ''; + } + + if( defined( 'SUBTITLE' ) ) { + $subtitle = '
' . SUBTITLE . '
'; + } + + /** + * ADS + */ + $topad = ''; + + $bottomad = ''; + + if (defined('AD_CUSTOM_BOTTOM') && AD_CUSTOM_BOTTOM) { + $bottomad .= '
' . AD_CUSTOM_BOTTOM . '
'; + }/* + else if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) { + $bottomad .= '

'; + } + else if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM) { + $bottomad .= '
'; + }*/ + else if (defined('ADS_DANBO') && ADS_DANBO) { + $bottomad .= '

'; + } + + $favicon = FAVICON; + $meta_robots = META_ROBOTS; + $meta_description = META_DESCRIPTION; + $meta_keywords = META_KEYWORDS; + + $body_class = explode('_', $style_group); + $body_class = $body_class[0]; + $body_class .= ' is_catalog board_' . BOARD_DIR; + + $canonical = ''; + + $embedearly = EMBEDEARLY; + $embedlate = EMBEDLATE; + $adembedearly = AD_EMBEDEARLY; + + $start_thread = S_FORM_THREAD; + + $jsVersion = TEST_BOARD ? JS_VERSION_TEST : JS_VERSION; + $comlen = MAX_COM_CHARS; + $maxfs = MAX_KB * 1024; + $jsCooldowns = json_encode(array( + 'thread' => RENZOKU3, + 'reply' => RENZOKU, + 'image' => RENZOKU2 + )); + + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) { + $event_css_html = ''; + + // Christmas 2021 + if (CSS_EVENT_NAME === 'tomorrow') { + $js .= << + + +JJS; + } + + $js .= ''; + } + else { + $event_css_html = ''; + } + + if (PARTY) { + $partyHats = 'var partyHats = "' . PARTY_IMAGE . '";'; + } + else { + $partyHats = ''; + } + + if (ENABLE_ARCHIVE) { + $archive_link = ' ' . S_ARCHIVE . ''; + } + else { + $archive_link = ''; + } + + if (TEXT_ONLY) { + $text_only = 'var text_only = true;'; + $body_text_css = ' text_only'; + $ctrl_css = ' hidden'; + } + else { + $text_only = $body_text_css = ''; + $ctrl_css = ''; + } + + $adg_js = 'var _adg = 1;'; + + $postform = ''; + form( $postform, 0, '', false, true ); + + $cat = << + + + + $title - Catalog - 4chan + + + + + $canonical + + $js + $flags + + +$embedearly +$adembedearly + + +
$nav
+
+ $titlepart +
$title
+ $subtitle +
+
+$topad +
$start_thread
+$postform +
+
+
+
+ Return$archive_link Bottom Refresh — Filtered threads: — Hidden threads: Show — Search results for: +
+
+
+ Sort By: + + Image Size: + + Show OP Comment: + + Filters + Search + +
+
+

+
+
+Return$archive_link Top Refresh — Filtered threads: — Hidden threads: Show — Search results for: +
+$bottomad +
Style:
+
+$foot + + + + + +$embedlate + +HTML; + + return $cat; + +} diff --git a/catalog.php b/catalog.php new file mode 100644 index 0000000..9d469c5 --- /dev/null +++ b/catalog.php @@ -0,0 +1,553 @@ + $catalogjson, + + 'count' => count( $log['THREADS'] ), + 'slug' => BOARD_DIR, + 'anon' => S_ANONAME, + 'mtime' => time(), + 'pagesize' => DEF_PAGES + ); + + if (!REPLIES_SHOWN && IS_REBUILDD) { + $catalogjson['no_lr'] = true; + } + + if (SHOW_COUNTRY_FLAGS) { + $catalogjson['flags'] = true; + } + + if( SPOILERS ) $catalogjson['custom_spoiler'] = (int)SPOILER_NUM; + + $catalogjson = json_encode( $catalogjson ); + + $catalog = catalog( $catalogjson ); + + print_page( INDEX_DIR . 'catalog.html', $catalog ); + + return true; +} + +function catalog_thread($res, &$json, $pos) +{ + global $log; + + $reps = $res['replycount']; + $sub = $res['sub']; + + $imgs = $res['imgreplycount']; + $last_reply_id = null; + $capcodelist = array(); + + if (TEXT_ONLY) { + $time_prop = 'now'; + } + else { + $time_prop = 'time'; + } + + foreach( $res['children'] as $reply => $unused ) { + $last_reply_id = $reply; + + if( META_BOARD && $log[$reply]['capcode'] != 'none' ) { + $tCapcode = $log[$reply]['capcode']; + if( $tCapcode == 'admin_highlight' ) $tCapcode = 'admin'; + + if( $tCapcode != 'none' ) { + $capcodelist[$tCapcode] = 1; + } + } + } + + if ($last_reply_id === null) { + $last_reply = array( 'id' => $res['no'] ); + } + else { + $lr_data = $log[$last_reply_id]; + + $last_reply = array( + 'id' => $last_reply_id, + 'date' => $lr_data['time'] + ); + + if( $lr_data['capcode'] != 'none' ) $last_reply['capcode'] = $lr_data['capcode']; + + $force_anon = ( ( FORCED_ANON || META_BOARD ) && $lr_data['capcode'] != 'admin' && $lr_data['capcode'] != 'admin_highlight' ); + + if( !$force_anon ) { + if( strpos( $lr_data['name'], '
' ) !== false ) { + list( $last_reply['author'], $last_reply['trip'] ) = explode( ' ', $lr_data['name'] ); + } else { + $last_reply['author'] = $lr_data['name']; + } + } else { + $last_reply['author'] = S_ANONAME; + } + } + + $json[$res['no']] = array( + 'date' => $res[$time_prop], + 'file' => mb_convert_encoding($res['filename'], 'UTF-8', 'UTF-8') . $res['ext'], + 'r' => $reps, + 'i' => $imgs, + 'lr' => $last_reply, + 'b' => $pos + ); + /* + if( META_BOARD && $capcodelist ) { + $json[$res['no']]['capcodereps'] = implode(',', array_keys($capcodelist)); + } + */ + if ($res['capcode'] == 'none') { + if (SHOW_COUNTRY_FLAGS && (!ENABLE_BOARD_FLAGS || $res['board_flag'] == '')) { + $json[$res['no']]['country'] = $res['country']; + } + } + + $com = $res['com']; + + if( strpos( $com, 'class="abbr"' ) !== false ) { + $com = preg_replace( '#(
)+(.+)$#s', '', $com ); + } + + if (!TEXT_ONLY) { + $com = preg_replace( '#(
)+#', ' ', $com ); + } + else { + $com = preg_replace( '#(
)+#', "\n", $com ); + } + + if (BOARD_DIR === 'b') { // fixme, hardcoded for now + $com = truncate_comment($com, 300, true); + } + else { + if (SJIS_TAGS) { + $com = preg_replace('//', '[SJIS]', $com); + } + $com = strip_tags($com, ''); + } + + $has_spoilers = (bool)SPOILERS; + + if (!$res['permaage'] && !$res['sticky']) { + if( $reps >= MAX_RES ) $json[$res['no']]['bumplimit'] = 1; + if( $imgs >= MAX_IMGRES ) $json[$res['no']]['imagelimit'] = 1; + } + + if( $res['sticky'] ) $json[$res['no']]['sticky'] = 1; + if( $res['closed'] ) $json[$res['no']]['closed'] = 1; + + if( $res['capcode'] != 'none' ) $json[$res['no']]['capcode'] = $res['capcode']; + + $force_anon = ( ( FORCED_ANON || META_BOARD ) && $res['capcode'] != 'admin' && $res['capcode'] != 'admin_highlight' ); + + if( !$force_anon ) { + if( strpos( $res['name'], ' ' ) !== false ) { + list( $json[$res['no']]['author'], $json[$res['no']]['trip'] ) = explode( ' ', $res['name'] ); + } else { + $json[$res['no']]['author'] = $res['name']; + } + } else { + $json[$res['no']]['author'] = S_ANONAME; + } + + if( $res['fsize'] != 0 && $res['filedeleted'] != 1 ) { + $json[$res['no']]['imgurl'] = $res['tim']; + $json[$res['no']]['tn_w'] = $res['tn_w']; + $json[$res['no']]['tn_h'] = $res['tn_h']; + } + + if( $res['filedeleted'] == 1 ) $json[$res['no']]['imgdel'] = true; + + if( strpos( $res['sub'], 'SPOILER<>' ) !== false ) { + $json[$res['no']]['imgspoiler'] = true; + $sub = substr( $res['sub'], 9 ); + } + + $json[$res['no']]['sub'] = $sub; + $json[$res['no']]['teaser'] = $com; +} + +function catalog($catjson) { + $nav = file_get_contents_cached( NAV_TXT ); + $foot = file_get_contents_cached( NAV2_TXT ); + + $nav = preg_replace( '/href="(\/\/boards.(?:4chan|4channel).org)?\/([a-z0-9]+)\/"/', 'href="$1/$2/catalog"', $nav ); + $nav = preg_replace( '/href="(\/\/boards.(?:4chan|4channel).org)?\/f\/catalog"/', 'href="$1/f/"', $nav ); + + $title = strip_tags( TITLE ); + + $js = ''; + + // danbo ads start + if (defined('ADS_DANBO') && ADS_DANBO) { + $js .= ''; + + $js .= ''; + } + // danbo ads end + + // PubFuture + if (DEFAULT_BURICHAN) { + $js .= ''; + } + + if (TEST_BOARD) { + // Main catalog JS + $js .= ''; + + // Painter JS + CSS + if (ENABLE_PAINTERJS) { + $js .= '' + . ''; + } + + // Core JS + $js .= ''; + } + else { + // Main catalog JS + $js .= ''; + + // Painter JS + CSS + if (ENABLE_PAINTERJS) { + $js .= '' + . ''; + } + + // Core JS + $js .= ''; + } + + $css = STATIC_SERVER . 'css'; + $cssv = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION_CATALOG; + $style_group = style_group(); + + $flags = SHOW_COUNTRY_FLAGS ? '' : ''; + + $titlepart = $subtitle = ''; + + if( TITLE_IMAGE_TYPE == 1 ) { + $titleimg = rand_from_flatfile( YOTSUBA_DIR, 'title_banners.txt' ); + $titlepart .= '
'; + } elseif( TITLE_IMAGE_TYPE == 2 ) { + $titlepart .= ''; + } + + if( defined( 'SUBTITLE' ) ) { + $subtitle = '
' . SUBTITLE . '
'; + } + + /** + * ADS + */ + $topad = ''; + + $bottomad = ''; + + if (defined('AD_CUSTOM_BOTTOM') && AD_CUSTOM_BOTTOM) { + $bottomad .= '
' . AD_CUSTOM_BOTTOM . '
'; + }/* + else if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) { + $bottomad .= '

'; + } + else if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM) { + $bottomad .= '
'; + }*/ + else if (defined('ADS_DANBO') && ADS_DANBO) { + $bottomad .= '

'; + } + + $favicon = FAVICON; + $meta_robots = META_ROBOTS; + $meta_description = META_DESCRIPTION; + $meta_keywords = META_KEYWORDS; + + $body_class = explode('_', $style_group); + $body_class = $body_class[0]; + $body_class .= ' is_catalog board_' . BOARD_DIR; + + $canonical = ''; + + $embedearly = EMBEDEARLY; + $embedlate = EMBEDLATE; + $adembedearly = AD_EMBEDEARLY; + + $start_thread = S_FORM_THREAD; + + $jsVersion = TEST_BOARD ? JS_VERSION_TEST : JS_VERSION; + $comlen = MAX_COM_CHARS; + $maxfs = MAX_KB * 1024; + $jsCooldowns = json_encode(array( + 'thread' => RENZOKU3, + 'reply' => RENZOKU, + 'image' => RENZOKU2 + )); + + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) { + $event_css_html = ''; + + // Christmas 2021 + if (CSS_EVENT_NAME === 'tomorrow') { + $js .= << + + +JJS; + } + + $js .= ''; + } + else { + $event_css_html = ''; + } + + if (PARTY) { + $partyHats = 'var partyHats = "' . PARTY_IMAGE . '";'; + } + else { + $partyHats = ''; + } + + if (ENABLE_ARCHIVE) { + $archive_link = ' ' . S_ARCHIVE . ''; + } + else { + $archive_link = ''; + } + + if (TEXT_ONLY) { + $text_only = 'var text_only = true;'; + $body_text_css = ' text_only'; + $ctrl_css = ' hidden'; + } + else { + $text_only = $body_text_css = ''; + $ctrl_css = ''; + } + + $adg_js = 'var _adg = 1;'; + + $postform = ''; + form( $postform, 0, '', false, true ); + + $cat = << + + + + $title - Catalog - 4chan + + + + + $canonical + + $js + $flags + + +$embedearly +$adembedearly + + +
$nav
+
+ $titlepart +
$title
+ $subtitle +
+
+$topad +
$start_thread
+$postform +
+
+
+
+ Return$archive_link Bottom Refresh — Filtered threads: — Hidden threads: Show — Search results for: +
+
+
+ Sort By: + + Image Size: + + Show OP Comment: + + Filters + Search + +
+
+

+
+
+Return$archive_link Top Refresh — Filtered threads: — Hidden threads: Show — Search results for: +
+$bottomad +
Style:
+
+$foot + + + + + +$embedlate + +HTML; + + return $cat; + +} diff --git a/clippy.html b/clippy.html new file mode 100644 index 0000000..800d02a --- /dev/null +++ b/clippy.html @@ -0,0 +1,77 @@ + + + + + diff --git a/config/ads/adblock.txt b/config/ads/adblock.txt new file mode 100644 index 0000000..3478371 --- /dev/null +++ b/config/ads/adblock.txt @@ -0,0 +1 @@ + diff --git a/config/ads/nws.bottom.txt b/config/ads/nws.bottom.txt new file mode 100644 index 0000000..2dee0da --- /dev/null +++ b/config/ads/nws.bottom.txt @@ -0,0 +1 @@ +
diff --git a/config/ads/nws.middle.txt b/config/ads/nws.middle.txt new file mode 100644 index 0000000..7748b19 --- /dev/null +++ b/config/ads/nws.middle.txt @@ -0,0 +1 @@ +
diff --git a/config/ads/nws.top.txt b/config/ads/nws.top.txt new file mode 100644 index 0000000..f5cef80 --- /dev/null +++ b/config/ads/nws.top.txt @@ -0,0 +1 @@ +
diff --git a/config/ads/ws.bottom.txt b/config/ads/ws.bottom.txt new file mode 100644 index 0000000..2dee0da --- /dev/null +++ b/config/ads/ws.bottom.txt @@ -0,0 +1 @@ +
diff --git a/config/ads/ws.middle.txt b/config/ads/ws.middle.txt new file mode 100644 index 0000000..7748b19 --- /dev/null +++ b/config/ads/ws.middle.txt @@ -0,0 +1 @@ +
diff --git a/config/ads/ws.top.txt b/config/ads/ws.top.txt new file mode 100644 index 0000000..f5cef80 --- /dev/null +++ b/config/ads/ws.top.txt @@ -0,0 +1 @@ +
diff --git a/config/boards/3.config.ini b/config/boards/3.config.ini new file mode 100644 index 0000000..0a4e997 --- /dev/null +++ b/config/boards/3.config.ini @@ -0,0 +1,14 @@ +[3's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Features and related config] + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/3/ - 3DCG" is 4chan's board for 3D modeling and imagery. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,worksafe,3d,3dcg,modeling,rendering,maya,adobe +; The board's category +CATEGORY = ws diff --git a/config/boards/a.config.ini b/config/boards/a.config.ini new file mode 100644 index 0000000..3435b6e --- /dev/null +++ b/config/boards/a.config.ini @@ -0,0 +1,45 @@ +[a's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Features and related config] + +JSON_TAIL_SIZE = 50 + +; J-List ads for index pages +AD_INTERTHREAD_ENABLED = no + +; Archived threads max age in hours for archive trimming. 0 disables trimming. +ARCHIVE_MAX_AGE = 180 + +; Don't allow user deletion of threads +NO_DELETE_OP = yes + +; Enable spoilers +SPOILERS = yes +; URL of thumbnail for spoilers +SPOILER_THUMB = {{STATIC_SERVER}}image/spoiler-a1.png +; How many custom spoilers? +SPOILER_NUM = 1 + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/a/ - Anime & Manga" is 4chan's imageboard dedicated to the discussion of Japanese animation and manga. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,worksafe,japan,anime,manga,japanese,animation +; The board's category +CATEGORY = ws + +;EMBED_INDEX = + +; Use rebuildd +STATIC_REBUILD = 1 + +; maximum number of threads per user, per board +MAX_USER_THREADS = 3 + +[Limits] + +MAX_RES = 500 +MAX_IMGRES = 300 diff --git a/config/boards/aco.config.ini b/config/boards/aco.config.ini new file mode 100644 index 0000000..173abff --- /dev/null +++ b/config/boards/aco.config.ini @@ -0,0 +1,27 @@ +[aco's config] + +[Advertisements] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5148,5f599fefc6c73,0,2,5149,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5187,5f5ef92eec034,176,2,5188,5f5ef93fac7ed + +;ADS_BIDGLASS_TOP_DESKTOP_FB =
<\/div> + +[Features and related config] + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/out/ - Outdoors" is 4chan's imageboard for discussing survivalist skills and outdoor activities such as hiking. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,outdoors,survivalist,hiking,outdoor activities +; The board's category +CATEGORY = ws + +[Limits] + +; Maximum upload size in KB +MAX_KB = 5120 +; Maximum width or height of an image (MUST be 11000 or less) +MAX_DIMENSION = 8000 diff --git a/config/boards/p.config.ini b/config/boards/p.config.ini new file mode 100644 index 0000000..f35a807 --- /dev/null +++ b/config/boards/p.config.ini @@ -0,0 +1,26 @@ +[p's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Limits] + +; Maximum upload size in KB +MAX_KB = 5120 + +[Features and related config] + +; Strip EXIF tags from images +; for privacy, but changes MD5s! +STRIP_EXIF = no + +; Enable EXIF parsing during post (needs /www/global/bin/exiftags) +ENABLE_EXIF = yes + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/p/ - Photography" is 4chan's imageboard for sharing and critiquing photos. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,photography,camera,critique,photos +; The board's category +CATEGORY = ws diff --git a/config/boards/po.config.ini b/config/boards/po.config.ini new file mode 100644 index 0000000..bf56eb1 --- /dev/null +++ b/config/boards/po.config.ini @@ -0,0 +1,26 @@ +[po's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Limits] + +; Maximum upload size in KB +MAX_KB = 8192 + +; Allow PDF upload and thumbnailing (needs /usr/local/bin/gs) +ENABLE_PDF = yes + +META_DESCRIPTION = "/po/ - Papercraft & Origami" is 4chan's imageboard for posting papercraft and origami templates and instructions. +; Text to go inside of the meta-description tag +META_KEYWORDS = imageboard,worksafe,papercraft,origami,japan,paper,crafting +; Text to go inside of the meta-keywords tag +; The board's category +CATEGORY = ws + +[Misc] + +; Rules below form (tip: use backslash to continue to next line) +S_RULES =
  • Please read the Rules and FAQ before posting.
  • \ +
  • Additional supported file types are: PDF
  • diff --git a/config/boards/pol.config.ini b/config/boards/pol.config.ini new file mode 100644 index 0000000..479232e --- /dev/null +++ b/config/boards/pol.config.ini @@ -0,0 +1,121 @@ +[pol's config] +CSS_VERSION_BOARD_FLAGS = 2 + +; desktop top +AD_ADGLARE_TOP = +; desktop bottom +AD_ADGLARE_BOTTOM = +; mobile top +AD_ADGLARE_TOP_MOBILE = +; mobile bottom +AD_ADGLARE_BOTTOM_MOBILE = +; desktop catalog top +AD_ADGLARE_TOP_CATALOG = +; desktop catalog bottom +AD_ADGLARE_BOTTOM_CATALOG = +; desktop revcontent top +AD_RC_TOP = +; desktop revcontent bottom +AD_RC_BOTTOM = +; desktop revcontent top catalog +AD_RC_TOP_CATALOG = +; desktop revcontent bottom catalog +AD_RC_BOTTOM_CATALOG = +; mobile revcontent top +AD_RC_TOP_MOBILE = +; mobile revcontent bottom +AD_RC_BOTTOM_MOBILE = +; adnium mobile top +AD_ADNIUM_TOP_MOBILE = +; adnium mobile bottom +AD_ADNIUM_BOTTOM_MOBILE = +; content abc desktop top: left,right +AD_ABC_TOP_DESKTOP = +; content abc mobile top +AD_ABC_TOP_MOBILE = +; content abc mobile top +AD_ABC_BOTTOM_MOBILE = +; content abc mobile inter-replies +AD_ABC_REPLIES_MOBILE = +; juicyads catalog top +AD_JUICY_TOP_CATALOG = +; juicyads catalog bottom +AD_JUICY_BOTTOM_CATALOG = +; juicyads index bottom +AD_JUICY_BOTTOM_DESKTOP = +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5148,5f599fefc6c73,0,2,5149,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5187,5f5ef92eec034,0,2,5188,5f5ef93fac7ed + +ADS_BIDGLASS = no + +ADS_BIDGLASS_TOP_DESKTOP = +ADS_BIDGLASS_BOTTOM_DESKTOP = + +ADS_BIDGLASS_TOP_MOBILE = +ADS_BIDGLASS_BOTTOM_MOBILE = + +HTML_IFRAME_WHITELIST = %^(https?:)?//(www\.youtube(-nocookie)?\.com/embed/|interactives\.ap\.org/)% + +[Features and related config] + +; maximum number of threads per user, per board +MAX_USER_THREADS = 3 +; period for user thread limit, in hours +MAX_USER_THREADS_PERIOD = 6 + +JSON_TAIL_SIZE = 50 + +MOBILE_IMG_RESIZE = yes + +; Archived threads max age in hours for archive trimming. 0 disables trimming. +ARCHIVE_MAX_AGE = 72 + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/pol/ - Politically Incorrect" is 4chan's board for discussing and debating politics and current events. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,politics,current events,debate,politically incorrect +; The board's category +CATEGORY = nws + +; Don't allow user deletion of threads +NO_DELETE_OP = yes + +; Show poster's unique ID based on IP and date, if email field is blank +DISP_ID = yes +; If DISP_ID is enabled, make IDs per-thread instead of per-board? +DISP_ID_PER_THREAD = yes +; If DISP_ID is enabled, stop ID from being Heaven when sage is used +DISP_ID_NO_HEAVEN = yes +; Show country flags next to names +SHOW_COUNTRY_FLAGS = yes + +; Only works on /pol/, will break other boards. Requires SHOW_COUNTRY_FLAGS. +USE_TROLL_FLAGS = no + +; User selectable flags. Will break boards without the board_flag table column. +ENABLE_BOARD_FLAGS = yes + +; Use rebuildd +STATIC_REBUILD = 1 + +; Allow credit allocation for captcha bypassing. +CAPTCHA_ALLOW_BYPASS = no + +[Limits] + +; Seconds between posts +RENZOKU = 30 +; Seconds between image posts +RENZOKU2 = 30 +; Seconds between intra thread posts +RENZOKU_INTRA = 30 +; Seconds between intra thread image posts +RENZOKU2_INTRA = 30 +; Seconds between new threads +RENZOKU3 = 90 + +; Maximum number of pages (overrides LOG_MAX; 0 = disable) +;PAGE_MAX = 15 +; Threads per page +DEF_PAGES = 20 diff --git a/config/boards/pw.config.ini b/config/boards/pw.config.ini new file mode 100644 index 0000000..209174a --- /dev/null +++ b/config/boards/pw.config.ini @@ -0,0 +1,17 @@ +[pw's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Features and related config] + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/pw/ - Professional Wrestling" is 4chan's imageboard for the discussion of professional wrestling. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,worksafe,sports,alternative,wrestling +; The board's category +CATEGORY = ws + +; Don't allow user deletion of threads +NO_DELETE_OP = yes diff --git a/config/boards/qa.config.ini b/config/boards/qa.config.ini new file mode 100644 index 0000000..06211b5 --- /dev/null +++ b/config/boards/qa.config.ini @@ -0,0 +1,28 @@ +[qa's config] + +;AD_TERRA_TOP_DESKTOP = tumultrecast.com,eaed4d55144c813097d5aa84a204a7c2 +;AD_TERRA_BOTTOM_DESKTOP = tumultrecast.com,c0b83c42f274bdfa2042504993ae51a2 + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +LOCKDOWN = yes + +JSON_TAIL_SIZE = 50 + +META_DESCRIPTION = "/qa/ - Question & Answer" is 4chan's imageboard for question and answer threads. +; Text to go inside of the meta-description tag +META_KEYWORDS = imageboard,worksafe,question,answer,q&a,ama,ask me anything,question and answer +; Text to go inside of the meta-keywords tag +; The board's category +CATEGORY = ws + +;NO_DELETE_REPLY = yes +NO_DELETE_OP = yes + +MAX_USER_THREADS_PERIOD = 48 +MAX_USER_THREADS = 3 + +; Permasage threads after X hours. 0 to disable. +PERMASAGE_HOURS = 168 diff --git a/config/boards/qb.config.ini b/config/boards/qb.config.ini new file mode 100644 index 0000000..103439e --- /dev/null +++ b/config/boards/qb.config.ini @@ -0,0 +1,16 @@ +[qb's config] + +LOCKDOWN = yes + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5148,5f599fefc6c73,0,2,5149,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5187,5f5ef92eec034,0,2,5188,5f5ef93fac7ed + +[Features and related config] + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/qb/ - Quebec" +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard +; The board's category +CATEGORY = nws diff --git a/config/boards/qst.config.ini b/config/boards/qst.config.ini new file mode 100644 index 0000000..1531414 --- /dev/null +++ b/config/boards/qst.config.ini @@ -0,0 +1,79 @@ +[qst's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Features and related config] + +JSON_TAIL_SIZE = 50 + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/qst/ - Quests" is 4chan's imageboard for grinding XP. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,quests,choose your own adventure +; The board's category +CATEGORY = ws + +; Enable spoilers +SPOILERS = yes +; URL of thumbnail for spoilers +;SPOILER_THUMB = {{STATIC_SERVER}}image/{{!rand spoiler-tg1.png,spoiler-tg2.png}} +; How many custom spoilers? +;SPOILER_NUM = 2 + +; Allow PDF upload and thumbnailing (needs /usr/local/bin/gs) +ENABLE_PDF = yes + +; Allow rolling of dice +DICE_ROLL = yes + +; Require a subject +REQUIRE_SUBJECT = yes + +OP_MARKUP = yes + +; maximum number of threads per user, per board +MAX_USER_THREADS = 5 +; period for user thread limit, in hours +MAX_USER_THREADS_PERIOD = 72 + +ENABLE_PAINTERJS = yes + +MAX_COM_CHARS = 3000 + +NO_DELETE_OP = yes + +; Show poster's unique ID based on IP and date, if email field is blank +DISP_ID = yes +; If DISP_ID is enabled, make IDs per-thread instead of per-board? +DISP_ID_PER_THREAD = yes +; If DISP_ID is enabled, stop ID from being Heaven when sage is used +DISP_ID_NO_HEAVEN = yes + +; Permasage threads after X hours. 0 to disable. +PERMASAGE_HOURS = 120 + +; Number of bumps +MAX_RES = 750 +; Maximum amount of image replies in a thread +MAX_IMGRES = 375 +; Maximum number of pages (overrides LOG_MAX; 0 = disable) +;PAGE_MAX = 5 +; Threads per page +;DEF_PAGES = 30 +; Maximum number of lines allowed in a post +MAX_LINES = 100 +; Maximum number of replies shown to a thread on index pages + +[Limits] + +; Maximum upload size in KB +MAX_KB = 8192 + +[Misc] + +; Rules below form (tip: use backslash to continue to next line) +S_RULES =
  • Please read the Rules and FAQ before posting.
  • \ +
  • Additional supported file types are: PDF
  • \ +
  • Roll dice with "dice+numberdfaces" in the options field (without quotes).
  • diff --git a/config/boards/r.config.ini b/config/boards/r.config.ini new file mode 100644 index 0000000..39c1b98 --- /dev/null +++ b/config/boards/r.config.ini @@ -0,0 +1,26 @@ +[r's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5148,5f599fefc6c73,0,2,5149,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5187,5f5ef92eec034,176,2,5188,5f5ef93fac7ed + +;ADS_BIDGLASS_TOP_DESKTOP_FB =
    <\/div> + +; content abc mobile inter-replies +;AD_RC_TOP = 107704 +;AD_RC_TOP_MOBILE = 107761 + +;AD_INTERTHREAD_ENABLED = yes + +; Text-only mode +;TEXT_ONLY = yes +; Allow thread OPs to have files when in TEXT_ONLY mode +;TEXT_ONLY_ALLOW_OP = no + +;PERMASAGE_HOURS = 0 +;COM_REGEX = no + +;S_INVALID_COM = Your post must contain at least one valid URL + +; Event CSS. Name must correspond to a stylesheet file. +;CSS_EVENT_NAME = spooky + +;ROBOT9000 = yes +;ROBOT9000_POSTS = r9k_posts +;ROBOT9000_MUTES = r9k_mutes + +OP_MARKUP = yes +;MAX_IMGRES = 3 + +MAX_USER_THREADS = 50 +;MAX_USER_THREADS_PERIOD = 24 + +SQLLOGMD5 = f_md5 +;UPLOAD_BOARD = yes + +;GIF_ONLY = yes +;FORTUNE_TRIP = yes + +;CAPTCHA = no + +;DICE_ROLL = yes + +;FORCED_ANON = yes + +; Maximum upload size in KB +MAX_KB = 4096 +; Maximum filesize for webm files. Must be <= MAX_KB +MAX_WEBM_FILESIZE = 4096 +; Maximum duration of a video stream in seconds +;MAX_WEBM_DURATION = 30 + +ENABLE_WEBM_AUDIO = yes + +;JSMATH = yes + +META_DESCRIPTION = /test/ - Testing +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,worksafe,test,testing + +;PASS_POST_ONLY = yes + +;PARTY = yes +;PARTY_IMAGE = xmashat.gif +;CSS_EVENT_NAME = tomorrow +;CSS_EVENT_VERSION = 1 + +;STICKY_CAP = 3 + +ENABLE_ARCHIVE = yes +; Archived threads max age in hours for archive trimming. 0 disables trimming. +;ARCHIVE_MAX_AGE = 0 +ARCHIVE_REBUILD_DELAY = 60 + +EMBEDEARLY = + +;PAGE_MAX = 1 +; Threads per page +;DEF_PAGES = 3 + +; Maximum number of entries before oldest thread is pruned +LOG_MAX = 500 + +;EXPIRE_NEGLECTED = no + +; Disable lockdown mode for /test/ +LOCKDOWN = no + +CLEANUP_UPLOADS = yes +NOSCRIPT_CAPTCHA_ONLY = no + +; Show poster's unique ID based on IP and date, if email field is blank +;DISP_ID = yes +; If DISP_ID is enabled, make IDs per-thread instead of per-board? +;DISP_ID_PER_THREAD = yes +; If DISP_ID is enabled, stop ID from being Heaven when sage is used +;DISP_ID_NO_HEAVEN = yes + +SHOW_BLOTTER = yes + +S_RULES = + +CODE_TAGS = yes +SJIS_TAGS = no + +STRIP_EXIF = yes + +;SHOW_COUNTRY_FLAGS = yes +;ENABLE_BOARD_FLAGS = yes +; If not set, BOARD_DIR will be used. +; Flag settings are inside lib/flags_*.php +; Flag images and CSS files are inside image/flags/* on STATIC_SERVER +;BOARD_FLAGS_TYPE = pol + +;CSS_VERSION_BOARD_FLAGS = 1 + +;PREUPLOAD_CAPTCHA = yes + +[General Locations] + +; The default value for BOARD_DIR is automatically set by config.php. +; it can be overridden, but it can't be defined as any useful value in this file. +; Directory name of this board relative to DOCUMENT_ROOT +; e.g. "b", "e", "hr" +BOARD_DIR = test + +; The category +CATEGORY = ws + +; CloudFlare API +;CLOUDFLARE_PURGE_ON_DEL = no + +; Enable code tags +;CODE_TAGS = yes + +; The title of the image board +TITLE = /test/ - Test + +; Text to be shown at the top of each board +;GLOBAL_MSG_FILE = /www/global/globalmsg.test.txt + +; Paths of header and fooder +NAV_TXT = /www/global/yotsuba/header.txt +NAV2_TXT = /www/global/yotsuba/footer.txt + +[Features and related config] + +; Enable recording of seconds in the date (changing this does not affect old posts) +SHOW_SECONDS = yes +SPOILERS = yes + +WORD_FILT = yes + +FAVICON = //s.4cdn.org/image/favicon-test.ico + +ENABLE_CATALOG = yes + +[Misc] + +USE_RSS = no + +; JSON Stuff +ENABLE_JSON = yes +ENABLE_JSON_INDEXES = no +ENABLE_JSON_CATALOG = no +ENABLE_JSON_THREADS = no + +; Enable EXIF parsing during post (needs /www/global/bin/exiftags) +ENABLE_EXIF = yes + +[Testing] + +SQL_DEBUG = yes + +TEST_BOARD = yes + +RENZOKU3 = 30 + +TENSORCHAN_MODE = 2 +TENSORCHAN_LOG_ONLY = no + +;AD_MIDDLE_PLEA = +;AD_BOTTOM_PLEA = + +;RECORD_POSTS = no + +;PROFILING = no + +;PROCESSLIST = no + +;META_BOARD = yes + +;STRIP_TRIPCODE = yes diff --git a/config/boards/tg.config.ini b/config/boards/tg.config.ini new file mode 100644 index 0000000..a89ba73 --- /dev/null +++ b/config/boards/tg.config.ini @@ -0,0 +1,46 @@ +[tg's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Features and related config] + +JSON_TAIL_SIZE = 50 + +MOBILE_IMG_RESIZE = yes + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/tg/ - Traditional Games" is 4chan's imageboard for discussing traditional gaming, such as board games and tabletop RPGs. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,d&d,warhammer,traditional games,board games,quests +; The board's category +CATEGORY = ws + +; Enable spoilers +SPOILERS = yes +; URL of thumbnail for spoilers +SPOILER_THUMB = {{STATIC_SERVER}}image/{{!rand spoiler-tg1.png,spoiler-tg2.png}} +; How many custom spoilers? +SPOILER_NUM = 2 + +; Allow PDF upload and thumbnailing (needs /usr/local/bin/gs) +ENABLE_PDF = yes + +; Allow rolling of dice +DICE_ROLL = yes + +; Permasage threads after X hours. 0 to disable. +;PERMASAGE_HOURS = 168 + +[Limits] + +; Maximum upload size in KB +MAX_KB = 8192 + +[Misc] + +; Rules below form (tip: use backslash to continue to next line) +S_RULES =
  • Please read the Rules and FAQ before posting.
  • \ +
  • Additional supported file types are: PDF
  • \ +
  • Roll dice with "dice+numberdfaces" in the options field (without quotes).
  • diff --git a/config/boards/toy.config.ini b/config/boards/toy.config.ini new file mode 100644 index 0000000..e00d856 --- /dev/null +++ b/config/boards/toy.config.ini @@ -0,0 +1,18 @@ +[toy's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Features and related config] + +; Permasage threads after X hours. 0 to disable. +; 14 days +PERMASAGE_HOURS = 336 + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/toy/ - Toys" is 4chan's imageboard for talking about all kinds of toys! +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,worksafe,toy,toys,figures +; The board's category +CATEGORY = ws diff --git a/config/boards/trash.config.ini b/config/boards/trash.config.ini new file mode 100644 index 0000000..4fffa2e --- /dev/null +++ b/config/boards/trash.config.ini @@ -0,0 +1,42 @@ +[trash's config] + +AD_JUICY_TOP_CATALOG = 665069 + +[Features and related config] + +JSON_TAIL_SIZE = 50 + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/trash/ - Off-topic" is 4chan's imageboard jail for off-topic threads. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,anonymous,random,memes +; The board's category +CATEGORY = nws + +; Remove the threads least recently replied to +EXPIRE_NEGLECTED = yes + +; Enable archiving of expired posts? +ENABLE_ARCHIVE = no + +; skip doubles enabled at /b/ postcount ~381720221, disabled at ~584248512 +SKIP_DOUBLES = no + +; Show number of unique IPs in the post log +SHOW_UNIQUES = no + +; Hide name and subject field +;FORCED_ANON = yes + +; Show poster's unique ID based on IP and date, if email field is blank +DISP_ID = no + +; Strip tripcodes from names +;STRIP_TRIPCODE = yes + +; Allow credit allocation for captcha bypassing. +CAPTCHA_ALLOW_BYPASS = no + +[Misc] + +SUBTITLE = The stories and information posted here are artistic works of fiction and falsehood.
    Only a fool would take anything posted here as fact. diff --git a/config/boards/trv.config.ini b/config/boards/trv.config.ini new file mode 100644 index 0000000..cbb62d5 --- /dev/null +++ b/config/boards/trv.config.ini @@ -0,0 +1,27 @@ +[trv's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Limits] + +; Maximum upload size in KB +MAX_KB = 8192 +; Maximum width or height of an image (MUST be 11000 or less) +MAX_DIMENSION = 10000 + +[Features and related config] + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/trv/ - Travel" is 4chan's imageboard dedicated to travel and the countries of the world. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,worksafe,travel,japan,vacation,world +; The board's category +CATEGORY = ws + +[Misc] + +S_RULES =
  • Please read the Rules and FAQ before posting.
  • \ +
  • Maximum file size allowed is {{MAX_KB}} KB.
  • \ +
  • Images greater than {{MAX_DIMENSION}}x{{MAX_DIMENSION}} pixels are not allowed.
  • diff --git a/config/boards/tv.config.ini b/config/boards/tv.config.ini new file mode 100644 index 0000000..ba660d2 --- /dev/null +++ b/config/boards/tv.config.ini @@ -0,0 +1,35 @@ +[tv's config] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5050,5f599fefc6c73,176,2,470,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5078,5f5ef92eec034,176,2,5079,5f5ef93fac7ed + +[Features and related config] + +JSON_TAIL_SIZE = 50 + +MOBILE_IMG_RESIZE = yes + +; Archived threads max age in hours for archive trimming. 0 disables trimming. +ARCHIVE_MAX_AGE = 250 + +; Text to go inside of the meta-description tag +META_DESCRIPTION = "/tv/ - Television & Film" is 4chan's imageboard dedicated to the discussion of television and film. +; Text to go inside of the meta-keywords tag +META_KEYWORDS = imageboard,worksafe,tv,television,film,movies,celebrity +; The board's category +CATEGORY = ws + +; Don't allow user deletion of threads +NO_DELETE_OP = yes + +; Enable spoilers +SPOILERS = yes +; URL of thumbnail for spoilers +SPOILER_THUMB = {{STATIC_SERVER}}image/{{!rand spoiler-tv1.png,spoiler-tv2.png,spoiler-tv3.png,spoiler-tv4.png,spoiler-tv5.png}} +; How many custom spoilers? +SPOILER_NUM = 5 + +; Use inference server to detect NSFW content. +; 0 = disabled, 1 = OPs only, 2 = All posts +TENSORCHAN_MODE = 2 diff --git a/config/boards/u.config.ini b/config/boards/u.config.ini new file mode 100644 index 0000000..f450a4e --- /dev/null +++ b/config/boards/u.config.ini @@ -0,0 +1,27 @@ +[u's config] + +[Advertisements] + +; domainid,sizeid,zoneid,k (desktop then mobile) +AD_BIDGEAR_TOP = 176,1,5148,5f599fefc6c73,0,2,5149,5f59a00f01673 +AD_BIDGEAR_BOTTOM = 176,1,5187,5f5ef92eec034,176,2,5188,5f5ef93fac7ed + +;ADS_BIDGLASS_TOP_DESKTOP_FB =
    <\/div> + +AD_TOP_ENABLE = no +AD_TOP_TEXT = /www/global/yotsuba/config/ads/nws.top.txt + +AD_MIDDLE_ENABLE = no +AD_MIDDLE_TEXT = /www/global/yotsuba/config/ads/nws.middle.txt + +AD_BOTTOM_ENABLE = no +AD_BOTTOM_TEXT = /www/global/yotsuba/config/ads/nws.bottom.txt + +ARCHIVE_AD_TAG =
    + +RTA = 1 diff --git a/config/categories/ws.config.ini b/config/categories/ws.config.ini new file mode 100644 index 0000000..5ae2e31 --- /dev/null +++ b/config/categories/ws.config.ini @@ -0,0 +1,95 @@ +[work safe config] + +;AD_EMBEDEARLY = + +;AD_BSA_TOP =
    +;AD_BSA_BOTTOM =
    + +[Features and related config] + +; Link to the favicon override +FAVICON = {{STATIC_SERVER}}image/favicon-ws.ico + +; Default to burichan.css (for worksafe boards) +DEFAULT_BURICHAN = yes + +; Use inference server to detect NSFW content. +; 0 = disabled, 1 = OPs only, 2 = All posts +TENSORCHAN_MODE = 1 +TENSORCHAN_LOG_ONLY = no + +[Limits] + +; Seconds between posts +RENZOKU = 60 +; Seconds between image posts +RENZOKU2 = 60 +; Seconds between intra thread posts +RENZOKU_INTRA = 60 +; Seconds between intra thread image posts +RENZOKU2_INTRA = 60 +; Seconds between new threads +RENZOKU3 = 600 + +; Number of bumps +MAX_RES = 310 +; Maximum amount of image replies in a thread +MAX_IMGRES = 150 + +; Maximum number of lines allowed in a post +MAX_LINES = 100 + +[Advertisements] + +; desktop top +;AD_ADGLARE_TOP = +; desktop bottom +;AD_ADGLARE_BOTTOM = +; mobile top +;AD_ADGLARE_TOP_MOBILE = +; mobile bottom +;AD_ADGLARE_BOTTOM_MOBILE = +; desktop catalog top +;AD_ADGLARE_TOP_CATALOG = +; desktop catalog bottom +;AD_ADGLARE_BOTTOM_CATALOG = +; desktop revcontent top +;AD_RC_TOP = 107704 +; desktop revcontent bottom +;AD_RC_BOTTOM = +; desktop revcontent top catalog +;AD_RC_TOP_CATALOG = +; desktop revcontent bottom catalog +;AD_RC_BOTTOM_CATALOG = +; mobile revcontent top +;AD_RC_TOP_MOBILE = 107761 +; mobile revcontent bottom +;AD_RC_BOTTOM_MOBILE = +; AdsTerra desktop, subdomain,placement +;AD_TERRA_TOP_DESKTOP = tumultrecast.com,eaed4d55144c813097d5aa84a204a7c2 +;AD_TERRA_BOTTOM_DESKTOP = tumultrecast.com,c0b83c42f274bdfa2042504993ae51a2 + +ADS_BIDGLASS = no + +ADS_DANBO = yes + +ADS_BIDGLASS_TOP_DESKTOP = +ADS_BIDGLASS_BOTTOM_DESKTOP = + +ADS_BIDGLASS_TOP_MOBILE = +ADS_BIDGLASS_BOTTOM_MOBILE = + +;AD_EMBEDEARLY = + +;EMBEDLATE = + +AD_TOP_ENABLE = no +AD_TOP_TEXT = /www/global/yotsuba/config/ads/ws.top.txt + +AD_MIDDLE_ENABLE = no +AD_MIDDLE_TEXT = /www/global/yotsuba/config/ads/ws.middle.txt + +AD_BOTTOM_ENABLE = no +AD_BOTTOM_TEXT = /www/global/yotsuba/config/ads/ws.bottom.txt + +ARCHIVE_AD_TAG =
    \ No newline at end of file diff --git a/config/cloudflare_config.ini b/config/cloudflare_config.ini new file mode 100644 index 0000000..e69de29 diff --git a/config/config_db.php b/config/config_db.php new file mode 100644 index 0000000..e69de29 diff --git a/config/global_config.ini b/config/global_config.ini new file mode 100644 index 0000000..c2047ed --- /dev/null +++ b/config/global_config.ini @@ -0,0 +1,664 @@ +[Config - Global] + +; Version suffix for production JS +; JS_VERSION is for mod.js and janitor.js +JS_VERSION = 1057 +JS_VERSION_CORE = 1123 +JS_VERSION_EXT = 1178 +JS_VERSION_CATALOG = 1024 +JS_VERSION_PAINTER = 1050 + +; Version suffix for production CSS +CSS_VERSION = 715 +CSS_VERSION_CATALOG = 705 +CSS_VERSION_FLAGS = 690 +CSS_VERSION_PAINTER = 619 +; This should be overriden on a per-board basis +CSS_VERSION_BOARD_FLAGS = 1 + +; Version suffix for /test/ CSS and JS +CSS_VERSION_TEST = 100595 +JS_VERSION_TEST = 100876 + +; Divs where we can embed stuff +EMBEDEARLY = +EMBEDLATE = +EMBED_INDEX = + +; Lockdown +LOCKDOWN = no +LOCKDOWN_MSG = Performing site maintenance. Try again in a little while. + +; Temporary setting to toggle mobile adglare ads +AD_ADGLARE_MOBILE = yes + +; Enable archiving of expired posts? +ENABLE_ARCHIVE = yes +; Archived threads max age in hours for archive trimming. 0 disables trimming. +ARCHIVE_MAX_AGE = 276 +; Minimum delay between archive list rebuilds, in seconds +ARCHIVE_REBUILD_DELAY = 600 + +; Enable CAPTCHA? +CAPTCHA = yes +CAPTCHA_TWISTER = yes +; Allow credit allocation for captcha bypassing. +CAPTCHA_ALLOW_BYPASS = yes + +; Cap and delete old replies in undead stickies +; Has to be > 1 +STICKY_CAP = 1000 +; Auto-archive threads after X replies, 0 to disable +AUTOARCHIVE_CAP = 0 + +; Enable pre-upload CAPTCHA? +PREUPLOAD_CAPTCHA = no +NOSCRIPT_CAPTCHA_ONLY = no + +; Memcached +MEMCACHED_HOST = localhost +MEMCACHED_PORT = 11211 + +; Pre-upload captcha token expiration (seconds) +CAPTCHA_TOKEN_TTL = 240 + +; CloudFlare API +CLOUDFLARE_PURGE_ON_DEL = yes +CLOUDFLARE_EMAIL = cloudflare@4chan.org +CLOUDFLARE_ZONE = 4chan.org +CLOUDFLARE_ZONE_2 = 4cdn.org + +; Detect and handle embedded data in file uploads: +; - Rejects PNGs and JPGs. +; - Removes embedded data from GIFs, modifying the original file. +CLEANUP_UPLOADS = yes + +; Stats logging +STATS_USER_JS = no + +; Various enhancements +NEW_HTML = yes +CODE_TAGS = no +SJIS_TAGS = no +NEW_CAPTCHA = no +META_BOARD = no +CAN_REPORT_POSTS = yes +ENABLE_CATALOG = yes +; TRIPCODE_CHARSET = CP932 + +; Upload board like /f/ +UPLOAD_BOARD = no + +; rebuildd stuff +STATIC_REBUILD = no +IS_REBUILDD = no +; how many rebuilds before we also rebuild the catalog +CATALOG_DAEMON_REBUILD_INTERVAL = 6 + +; JSON Stuff +ENABLE_JSON = yes +ENABLE_JSON_INDEXES = yes +ENABLE_JSON_CATALOG = yes +ENABLE_JSON_THREADS = yes + +; number of posts to include in partial JSON files. 0 to disable. +JSON_TAIL_SIZE = 0 + +; rebuildd timers +MAX_REBUILD_TIMER = 8 +MIN_REBUILD_TIMER = 2 + +; admin and janitor JS extension filenames +ADMIN_JS_PATH = 9a9f422e4fc10c549b9e2c6519433d76 +JANITOR_JS_PATH = dbb7da08ac5a75139c3cd257870ae466 + +; Block posting if ban request pending? +; BR Detection (huehuehuehue) +BLOCK_ON_BR = yes +; MYSQL DATE_SUB(NOW(), --> BLOCK_ON_BR_LEN <-- ) +BLOCK_ON_BR_LEN = INTERVAL 15 MINUTE + +; 4chan pass stuff +; pass timeout in seconds +PASS_TIMEOUT = 1800 + +; Text-only mode +TEXT_ONLY = no +; Allow thread OPs to have files when in TEXT_ONLY mode +TEXT_ONLY_ALLOW_OP = no +; Permasage threads after X hours. 0 to disable. +PERMASAGE_HOURS = 0 +; Regex for comment validation. +COM_REGEX = no + +; Use inference server to detect NSFW content. +; 0 = disabled, 1 = OPs only, 2 = All posts +TENSORCHAN_MODE = 0 +; Input image dimensions +TENSORCHAN_DIM = 300 +; Threshold for blocking (float 0.5 to 1.0) +TENSORCHAN_THRES = 0.92 +; Don't block, only log +TENSORCHAN_LOG_ONLY = yes + +[Limits] + +; Maximum number of entries before oldest thread is pruned +LOG_MAX = 700 +; Maximum number of pages (overrides LOG_MAX; 0 = disable) +PAGE_MAX = 10 +; Threads per page +DEF_PAGES = 15 +; Number of bumps +MAX_RES = 300 +; Maximum number of replies shown to a thread on index pages +REPLIES_SHOWN = 5 +; Maximum number of lines shown on index pages +MAX_LINES_SHOWN = 15 +; Maximum number of lines allowed in a post +MAX_LINES = 70 + +; Maximum comment length +MAX_COM_CHARS = 2000 +; For mods+ +MAX_COM_CHARS_AUTHED = 10000 + +; Maximum amount of image replies in a thread +MAX_IMGRES = 150 +; Maximum upload size in KB +MAX_KB = 4096 +; Minimum dimensions of an image +MIN_W = 1 +MIN_H = 1 +; Maximum width or height of an image (MUST be 11000 or less) +MAX_DIMENSION = 10000 +; Only allow gifs +GIF_ONLY = no + +; Resize images for mobile +MOBILE_IMG_RESIZE = no + +; Enable webm support +ENABLE_WEBM = yes +; Allow audio streams in webm +ENABLE_WEBM_AUDIO = no +; Maximum width or height of a video stream +MAX_WEBM_DIMENSION = 2048 +; Maximum duration of a video stream in seconds +MAX_WEBM_DURATION = 120 +; Maximum filesize for webm files. Must be <= MAX_KB +MAX_WEBM_FILESIZE = 4096 + +; maximum number of threads per user, per board +MAX_USER_THREADS = 5 +; period for user thread limit, in hours +MAX_USER_THREADS_PERIOD = 24 + +; maximum number of reposted live reply images, 0 for no limit +MAX_IMG_REPOST_COUNT = 0 + +; Seconds between posts +RENZOKU = 60 +; Seconds between image posts +RENZOKU2 = 60 +; Seconds between intra thread posts +RENZOKU_INTRA = 60 +; Seconds between intra thread image posts +RENZOKU2_INTRA = 60 +; Seconds between new threads +RENZOKU3 = 600 + +; Seconds between duplicate comments and images +; (for shared IPs) +RENZOKU_DUPE = 300 +; Seconds between sage posts +RENZOKU_SAGE = 120 +; Seconds before you can delete a new post +RENZOKU_DEL = 60 +; Seconds after which you cannot delete a post +RENZOKU_DEL_CANT_AFTER = 1800 +; Number of posts you can delete per hour +RENZOKU_DEL_HOURLY = 2 +; Number of posts you can delete per day +RENZOKU_DEL_DAILY = 10 + +; Number of reports you can make per hour (was 30 then 50) +RENZOKU_REP_HOURLY = 30 +; Number of reports you can make per day (was 80 then 100) +RENZOKU_REP_DAILY = 80 +; FIXME: unused +RENZOKU_REP_DAILY_SOFT = 15 + +; Sage OPs replies if the length since their last post is too short +RENZOKU_OP = yes +; Time in seconds after thread creation during which the OP cannot bump his thread +RENZOKU_OP_TIME = 900 +; OP can only bump his thread every RENZOKU_OP_TIME2 seconds +RENZOKU_OP_TIME2 = 300 + +; Seconds between any request from the same IP (currently unused) +RENZOKU_REQ = 5 + +; Anti flood interval in minutes for thread creation +ANTI_FLOOD_INTERVAL = 5 +; Anti flood threshold for threads. If the number of recent suspicous threads is >= this value, the post will be blocked +ANTI_FLOOD_THRES = 3 +; Same as above but for replies, per thread. FIXME: ALT no longer used +ANTI_FLOOD_INTERVAL_REP = 6 +ANTI_FLOOD_THRES_REP = 3 +ANTI_FLOOD_INTERVAL_REP_ALT = 10 +ANTI_FLOOD_THRES_REP_ALT = 6 +; Same as above but for the whole board +ANTI_FLOOD_INTERVAL_GLOBAL = 5 +ANTI_FLOOD_THRES_GLOBAL = 7 +; Same as above but for the whole site +ANTI_FLOOD_INTERVAL_SITE = 3 +ANTI_FLOOD_THRES_SITE = 30 +; Secondary anti flood interval and threshold for threads (only the IP's posting history and cookie used here) +ANTI_FLOOD_INTERVAL_RAW = 3 +ANTI_FLOOD_THRES_RAW = 5 + +; Anti flood interval in minutes and threshold for replies per thread +ANTIFLOOD_INTERVAL_REPLY = 5 +ANTIFLOOD_THRES_REPLY = 6 +; Anti flood interval in minutes and threshold for thread creation +ANTIFLOOD_INTERVAL_OP = 5 +ANTIFLOOD_THRES_OP = 4 + +; reCAPTCHA failures +;CAPTCHA_FAIL_HOURLY = 15 +;CAPTCHA_FAIL_DAILY = 45 +CAPTCHA_FAIL_HOURLY = 60 +CAPTCHA_FAIL_DAILY = 120 + +; number of times a person can fail login AND FLOOD CHECKS before being banned +LOGIN_FAIL_HOURLY = 10 + +; Refuse text only posts (yes = refuse them, no = allow them!) +NO_TEXTONLY = yes + +; Remove the threads least recently replied to +EXPIRE_NEGLECTED = yes + +; New threads require subject lines +REQUIRE_SUBJECT = no + +; Reduce occurrence of doubles by re-inserting once +SKIP_DOUBLES = no + +; Don't allow user deletion of threads +NO_DELETE_OP = no +; Don't allow user deletion of replies +NO_DELETE_REPLY = no + +[General Locations] + +; The default value for BOARD_DIR is automatically set by config.php. +; it can be overridden, but it can't be defined as any useful value in this file. +; Directory name of this board relative to DOCUMENT_ROOT +; e.g. "b", "e", "hr" +BOARD_DIR = + +; The title of the image board +TITLE = Yotsuba Image Board + +; The board's category +CATEGORY = + +; Table for this board's posts +SQLLOG = {{BOARD_DIR}} + +; Where is yotsuba/? +YOTSUBA_DIR = /www/global/yotsuba/ + +; Paths of header and fooder +NAV_TXT = /www/global/yotsuba/header.txt +NAV2_TXT = /www/global/yotsuba/footer.txt +NAV3_TXT = /www/global/yotsuba/header-sys.txt + +; Text to be shown at the top of each board +GLOBAL_MSG_FILE = /www/global/yotsuba/globalmsg.txt +; Text to be shown to the right of board rules +DONATE = +; Text to be shown to the right of board rules, on index pages and catalog only +DONATE_INDEX = + +; Paths of saltfile for secure tripcodes +SALTFILE = /www/keys/legacy.salt + +; Main domain used for linking to non-board pages +MAIN_DOMAIN = 4chan.org + +; Server locations +STATIC_SERVER = //s.4cdn.org/ +DATA_SERVER = //boards.4chan.org/ +PHP_SERVER = https://sys.4chan.org/ +IMG_SERVER = //i.4cdn.org/ + +; internal folder locations +BOARDS_ROOT = /www/4chan.org/web/boards/ +DATA_ROOT = /www/4chan.org/web/boards/{{BOARD_DIR}}/ +IMG_ROOT = /www/4chan.org/web/images/{{BOARD_DIR}}/ +THUMB_ROOT = /www/4chan.org/web/thumbs/{{BOARD_DIR}}/ +SYS_ROOT = /www/4chan.org/web/sys +BANTHUMB_ROOT = /www/4chan.org/web/images/bans/thumb/ +BANIMG_ROOT = /www/4chan.org/web/images/bans/src/ + +INDEX_DIR = {{DATA_ROOT}} +IMG_DIR = {{IMG_ROOT}} +IMG_DIR2 = {{IMG_SERVER}}{{BOARD_DIR}}/ +THUMB_DIR = {{THUMB_ROOT}} +RES_DIR = {{DATA_ROOT}}thread/ +BANTHUMB_DIR = {{BANTHUMB_ROOT}}{{BOARD_DIR}}/ + +; link to /image/ (for closed.gif, filedeleted, etc) +STATIC_IMG_DIR2 = {{STATIC_SERVER}}image/ + +; image URLs, note RES_DIR2 is not an absolute URL atm +THUMB_DIR2_PART = i.4cdn.org/{{BOARD_DIR}}/ +THUMB_DIR2 = //{{THUMB_DIR2_PART}} +RES_DIR2 = thread/ + +; Name of the script +SELF_PATH = imgboard.php +; Name of the first index page in a url +SELF_PATH2 = ./ +; the extension of saved HTML files +PHP_EXT = .html + +; the suffix of HTML files in the URLs +; might be nothing if there's rewrites +PHP_EXT2 = + +; Path to first index page +SELF_PATH2_FILE = {{DATA_ROOT}}imgboard.html +; Path to URL for posting +SELF_PATH_POST = {{PHP_SERVER}}{{BOARD_DIR}}/post +SELF_PATH_DEL = {{PHP_SERVER}}{{BOARD_DIR}}/delete + +; URL to imgboard.php +SELF_PATH_ABS = {{PHP_SERVER}}{{BOARD_DIR}}/{{SELF_PATH}} +SELF_PATH_DEL = {{PHP_SERVER}}{{BOARD_DIR}}/delete +; URL to index.html +SELF_PATH2_ABS = {{DATA_SERVER}}{{BOARD_DIR}}/{{SELF_PATH2}} +; URL to /catalog +PHP_CATA_ABS = {{DATA_SERVER}}{{BOARD_DIR}}/catalog + +; link to Home +HOME = ../ + +[Features and related config] + +; Moderator pass +ADMIN_PASS = DISHSIS +; Janitor pass +ADMIN_PASS2 = BISCUITS + +; Display title (0: no image, 1: random image from sql, 2: random image via TITLEIMG, click for different image) +TITLE_IMAGE_TYPE = 1 +; URL of title image +TITLEIMG = //sys.4chan.org/image/title/rid.php + +; Show blotter (including show/hide controls) +SHOW_BLOTTER = yes + +; Enable spoilers +SPOILERS = no +; URL of thumbnail for spoilers +SPOILER_THUMB = {{STATIC_IMG_DIR2}}spoiler.png +; How many custom spoilers? +SPOILER_NUM = 0 + +; Enable thumbnailing +USE_THUMB = yes +; Images bigger than these dimensions will be thumbnailed +MAX_W = 250 +MAX_H = 250 +; Reply images bigger than these dimensions will be thumbnailed +MAXR_W = 125 +MAXR_H = 125 + +; Enable wordfilters +WORD_FILT = yes + +; HTML Purifier whitelist string. See HTML Purifier doc. +HTML_WHITELIST = a[href|target],video[autoplay|controls|height|loop|muted|src|width],div,br,p,s,strong,em,span,b,u,i,li,ul,ol,img[src|alt|width|height],iframe[width|height|src|frameborder|allowfullscreen|scrolling],*[style|title|class] + +; HTML Purifier iframe src whitelist. Regex. See HTML Purifier doc. +HTML_IFRAME_WHITELIST = %^(https?:)?//www\.youtube(-nocookie)?\.com/embed/% + +; Pass-only boards +PASS_POST_ONLY = no + +; Enable recording of seconds in the date (changing this does not affect old posts) +SHOW_SECONDS = yes + +; Show number of unique IPs in the post log +SHOW_UNIQUES = no + +; Show number of unique posters inside threads +SHOW_THREAD_UNIQUES = no + +; Enable fortune-teller (use #fortune as tripcode) +FORTUNE_TRIP = no + +; Hide name and subject field +FORCED_ANON = no + +; Enable EXIF parsing during post (needs /www/global/bin/exiftags) +ENABLE_EXIF = no + +; Allow PDF upload and thumbnailing (needs /usr/local/bin/gs) +ENABLE_PDF = no + +; Show poster's unique ID based on IP and date, if Options field is blank +DISP_ID = no + +; If DISP_ID is enabled, stop ID from being Heaven when sage is used +DISP_ID_NO_HEAVEN = no + +; If DISP_ID is enabled, make IDs per-thread instead of per-board? +DISP_ID_PER_THREAD = yes + +; Show random IDs instead of ones based on IP +DISP_ID_RANDOM = no + +; Save X-Forwarded-For proxies in xff table +SAVE_XFF = no + +; Trick people who post 'sage' in every field by removing it from the Options +SAGE_FILTER = no + +; Default to burichan.css (for worksafe boards) +DEFAULT_BURICHAN = no + +; Plugin system (testing) +PLUGIN_DIR = /www/global/yotsuba/plugins/ +; Log rapidshare links +;PLUGINS = filedumpidx + +; Save index.rss file +USE_RSS = yes + +; Put party hats on images :) +PARTY = no +;PARTY_IMAGE = partyhat.gif +PARTY_IMAGE = xmashat.gif + +; Event CSS. Name must correspond to a stylesheet file. +;CSS_EVENT_NAME = tomorrow +CSS_EVENT_VERSION = 1 + +; April 2016 cfg +;CSS_FORCE = //s.4cdn.org/css/yotsubluenew.647.css +;CSS_MATERIAL = yes + +; Force comment text +FORCE_COM = no +FORCE_COM_TEXT = + +; Show country flags next to names +SHOW_COUNTRY_FLAGS = no + +; Alternate trolling flags for above +; Only works on /pol/, will break other boards. Requires SHOW_COUNTRY_FLAGS. +USE_TROLL_FLAGS = no + +; User selectable flags. Will break boards without the board_flag table column. +ENABLE_BOARD_FLAGS = no + +; Enable janitor board features +JANITOR_BOARD = no + + +; Enable JS oekaki app +ENABLE_PAINTERJS = no +; Default dimensions in pixels for JS oekaki app +PAINTERJS_DIMS = 400 +; Allow oekaki replays +ENABLE_OEKAKI_REPLAYS = no + +; Enable "Ban entire thread" feature in the ban panel +THREAD_BANNING = yes + +; Prevent redundant updating of imgboard.html +; ('no' or number of seconds to let imgboard.html get out of date during a flood) +UPDATE_THROTTLING = no + +; Auto-deletion limits +REPORTS_AUTODELETE_ENABLED = no +; Delete a post after this many reports +REPORTS_AUTODELETE = 50 +; Delete a post after this many illegal reports +REPORTS_AUTODELETE_ILLEGAL = 25 + +; Maximum number of illegal reports one IP can make (FIXME: unused) +REPORTS_MAX_ILLEGAL = 25 +; Maximum number of reports an IP can make (FIXME: unused) +REPORTS_MAX = 100 +; Note there is a hardcoded limit of 1 minute between reports + +; Text to go inside of the meta-robots tag +META_ROBOTS = noarchive +; Text to go inside of the meta-description tag +META_DESCRIPTION = +; Text to go inside of the meta-keywords tag +META_KEYWORDS = + +; log rapidshare links to rapidsearch queue +RAPIDSEARCH_LOGGING = no + +; Generatic static gzipped HTML files +USE_GZIP = yes + +; URL of the favicon to link to +; No rel=icon is emitted if not defined +;FAVICON = /favicon.ico + +; whether to forward HTML deletion/rebuild +; to a php process on the static server +; 'no' if not, the hostname of the server if yes +REMOTE_HTML_HOST = no + +; whether to load jsMath +JSMATH = no + +; Shouldn't this be in the database config file and not here? +; RS database details +;RS_HOST = db2.int +;RS_USER = rs +;RS_PASS = f00n3rs1 +;RS_DB = rs + +; Record posts to post_log +RECORD_POSTS = no + +; is test +TEST_BOARD = no + +; where xhprof is +XHPROF_ROOT = /www/php_xhprof + +; Strip EXIF tags from images +; for privacy, but changes MD5s! +STRIP_EXIF = yes + +; Strip EXIF tags from images when capcode is active +STRIP_EXIF_ON_CAPCODE = yes + +; Strip tripcodes from names +STRIP_TRIPCODE = no + +; Strip email addresses (sage still works) +STRIP_EMAILS = no + +; le reddit mode XD +UPVOTES = no + +USE_SRC_CGI = no +SORT_NATURAL = no +PROFILING = no + +; show last digit of no twice +FAKE_DOUBLES = no + +; Allow rolling of dice +DICE_ROLL = no + +; Allow text markup for OPs +OP_MARKUP = no + +[Advertisements] + +; Show plea if ads are blocked? +AD_PLEA = yes +AD_PLEA_TEXT = Please support 4chan by disabling your ad blocker on *.4chan.org/*, purchasing a self-serve ad, or buying a 4chan Pass. + +; Ad tag for archives +ARCHIVE_AD_TAG =
    + +; Div to embed ad code in head (JavaScript) +AD_EMBEDEARLY = + +; Location of file for anti adblock js tag +AD_ADBLOCK_TEXT = +;AD_ADBLOCK_TEXT = + +; Inter thread ads on index pages +AD_INTERTHREAD_ENABLED = no +AD_INTERTHREAD_TAG =
    Advertisement
    + +; Show top ad (728x90) +AD_TOP_ENABLED = no +; Table name for top ad +AD_TOP_TABLE = +; Location of file for top ad (overrides table) +AD_TOP_TEXT = + +; Plea for top ad +;AD_TOP_PLEA = +AD_TOP_PLEA = + +; Show middle ad (468x60) +AD_MIDDLE_ENABLED = no +; Table name for middle ad +AD_MIDDLE_TABLE = +; Location of file for middle ad (overrides table) +AD_MIDDLE_TEXT = +; Plea for middle ad +AD_MIDDLE_PLEA = + +; Show top ad (728x90) +AD_BOTTOM_ENABLED = no +; Table name for bottom ad +AD_BOTTOM_TABLE = +; Location of file for bottom ad (overrides table) +AD_BOTTOM_TEXT = +; Plea for bottom ad +AD_BOTTOM_PLEA = + +FIXED_LEFT_AD = no +FIXED_RIGHT_AD = no +THREAD_AD = no + +RTA = no diff --git a/config/global_strings.ini b/config/global_strings.ini new file mode 100644 index 0000000..79c789e --- /dev/null +++ b/config/global_strings.ini @@ -0,0 +1,379 @@ +[Strings - Global] +S_UPDATING_INDEX = Updating index... +S_POSTING_DONE = Post successful! +S_POST_DEAD = That post doesn't exist anymore. + +; Dice roll prefix. No trailing space. +S_DICE_PFX = Rolled + +; Exif show/hide +S_EXIF_TOGGLE = [EXIF data available. Click here to show/hide.] + +[Post Form Strings] +; Toggle post form labels for mobile view and catalog +S_FORM_THREAD = Start a New Thread +S_FORM_REPLY = Post a Reply + +; Form field descriptions +S_NAME = Name +S_EMAIL = Options +S_SUBJECT = Subject +S_SUBMIT = Post +S_COMMENT = Comment +S_UPLOADFILE = File +S_DELPASS = Password +S_SPOILERS = Spoiler? +S_CAPTCHA = Verification +; Checkbox +S_NOFILE = No File +; Password explanation +S_DELEXPL = (Password used for deletion) +; For /pol/ flags +S_NONE = None +S_FLAG = Flag + +; When the OP doesn't pass the regex validation +S_INVALID_COM = Invalid comment + +S_TEXT_ONLY = You cannot upload files on this board + +; Capcode without the 'capcode' flag error +S_CANTCAPCODE = Error: You do not have permission to use this capcode. + +; Thread closed +S_THREAD_CLOSED = Thread closed.
    You may not reply at this time. + +; Thread archived +S_THREAD_ARCHIVED = Thread archived.
    You cannot reply anymore. + +; Message for the notification post appended to moved threads +; %s is the link to the new location +S_THREAD_MOVED = This thread was moved to %s + +; Global announcement toggle in mobile view +S_VIEW_GMSG = View Announcement + +; unique user posts + +S_UNIQUE_POSTS = There are %d posters on this board. +; unique user posts (per thread) +S_UNIQUE_POSTS_TH = %d poster%s in this thread. + +; 4chan Pass notice +S_PASS_NOTICE = 4chan Pass users can bypass this verification. [Learn More] [Login] + +; Titlebar over form on reply page +S_POSTING = Reply - Thread No.%d +; Notice when logged in as admin +S_NOTAGS = HTML tags are allowed. + +; Rules below form (tip: use backslash to continue to next line) +S_RULES =
  • Please read the Rules and FAQ before posting.
  • + +[Delete Form Strings] + +; Prints text next to S_DELPICONLY (left) +S_REPDEL = Delete Post: +; Prints text next to checkbox for file deletion (right) +S_DELPICONLY = File Only +; Prints text next to password field for deletion (left) +S_DELKEY = Password +; Defines deletion button's name +S_DELETE = Delete + +[Admin Mode Strings] + +; Admin mode title +S_MANAMODE = Manager Mode +; Submit button label +S_MANASUB = Submit +; Delete button label +S_ITDELETES = Delete +; File only checkbox label +S_MDONLYPIC = File Only +; Reset button label +S_MDRESET = Reset +; Another reset button? +S_RESET = Reset +; Space used at the bottom +S_IMGSPACEUSAGE = Space used: + + +[Navigation Strings] +; mostly for mobile layout +S_TOP = Top +S_BOTTOM = Bottom +S_REFRESH = Refresh +S_CATALOG = Catalog +S_SEARCH = Search OPs… +S_ARCHIVE = Archive + +; Return from reply pages +S_RETURN = Return +; Return to SELF_PATH2 (index HTML page) +S_RETURNS = Return +; Return to SELF_PATH (update) +S_LOGUPD = Rebuild Index +S_LOGUPDALL = Rebuild All + +;Previous button label +S_PREV = Previous +;Previous text (on first page) +S_FIRSTPG = Previous +;Next button label +S_NEXT = Next +;Next text (on last page) +S_LASTPG = Next + +;Footer text - XXX: this shouldn't be used +S_FOOT = + + +[Error Strings] +; Blocked after ban request +S_BRBLOCKED = Error: You are temporarily blocked from posting for violating %s.
    This block will expire in %s. [More Info] +S_BRBLOCKED_2 = Error: You are temporarily blocked from posting for violating %s. + +; Banned posting error +S_BANNED = Error: You are banned. + +; Warned posting error +S_WARNED = Error: You were warned. You must first view this warning to post again. + +; Can't delete posts when banned +S_BANNED_DEL = Error: You can't delete posts because you are banned. + +; Return string on error page +S_RELOAD = Return + +; failed upload (reason: unknown?) +S_FAILEDUPLOAD = Error: Upload failed. +; record cannot be found (image is inaccessible or invalid) +S_NOREC = Error: Corrupted file or unsupported file type. +; blocked MD5 detected +S_SAMEPIC = Error: Duplicate MD5 checksum detected. +; image is too big (filesize) +S_TOOLARGE = Error: File too large. +S_TOOLARGEJS = Error: Maximum file size allowed is {{MAX_KB}} KB. +; image is too big (filesize) or none selected +S_TOOLARGEORNONE = Error: File too large, or no image selected. +; images is too big (resolution) +S_TOOLARGERES = Error: Image resolution is too large. +; image is too small (resolution) +S_TOOSMALL = Error: Image resolution is too small. +S_IMGFAIL = Error: Image appears corrupt. + +S_NOUPLOAD = Error: You must attach a file. + +; image md5 is DMCA-blacklisted +S_DMCAFAIL = Error: This image cannot be uploaded because it is the subject of a copyright infringement claim. Please refrain from posting it. +; public ban reason for reposting blacklisted images +S_DMCABANREASON = Repeatedly uploading image(s) subject to a copyright infringement claim. + +; webm validation messages +; wemb files can contain only one VP8 video stream and one optional vorbis audio stream. +S_NOVIDEOSTREAM = Error: No video streams detected. Must contain 1 video stream. +; not audio or video +S_BADSTREAM = Error: File contains an invalid stream. +; audio is not Vorbis +S_BADAUDIO = Error: Bad audio stream. Supported codecs are: vorbis/opus for webm and aac for mp4. +; video is not VP8 +S_BADVIDEO = Error: Bad video stream. Supported codecs are: vp8/vp9 for webm and h264 for mp4. +; duration controlled by MAX_WEBM_DURATION setting +S_VIDEOTOOLONG = Error: Maximum allowed video duration is {{MAX_WEBM_DURATION}} seconds. +; audio streams are controlled by ENABLE_WEBM_AUDIO setting +S_AUDIODISABLED = Error: Audio streams are not allowed. + +; a string is refused +S_STRREF = Error: String refused. +;Returns error if trying to post using GET method +S_UNJUST = Error: Bad POST. +; no file selected and override unchecked +S_NOPIC = Error: No file selected. +; no text entered in to subject/comment +S_NOTEXT = Error: No text entered. +S_NOSUB = Error: New threads require a subject. +S_NOTEXT_OP = Error: New threads require a subject or comment. +; too many newlines +S_TOOMANYLINES = Error: Too many lines. + +; too many characters in a given field +S_TOOLONG = Error: Field too long. +; abnormal reply? (this is a mystery!) +S_GENERICERROR = Error: Abnormal reply. +; proxy detection on port 80 +S_PROXY80 = Error: Proxy detected on port 80. +; proxy detection on port 8080 +S_PROXY8080 = Error: Proxy detected on port 8080. + +; user reached the maximum number of allowed threads per board +S_TOOMANYTHREADS = Error: You may not post more than %s active thread%s at a time. + +; $sec/post spam filter +S_RENZOKU = Error: You must wait %s before posting a reply. +; $sec/upload spam filter +S_RENZOKU2 = Error: You must wait %s before posting an image reply. +; $sec/thread spam filter +; FIXME +S_RENZOKU3 = Error: You must wait longer before posting a new thread. +; $sec/duplicate comment spam filter +S_RENZOKU_DUP = Error: You must wait %s before posting a duplicate reply. +; $sec/duplicate comment spam filter +S_RENZOKU2_DUP = Error: You must wait %s before posting a duplicate image. +; for non-Pass users +S_RENZOKU_PASS =
    4chan Pass users have lower cooldowns. [Learn More] + +; for Pass-only boards +S_PASS_POST_ONLY = Error: Only 4chan Pass users can post on this board. [Learn More] + +; for "pass user since" messages +S_PASS_USER_SINCE =

    4chan Pass user since %s. + +; a duped file (same upload name or same tim/time) +S_DUPE = Error: Duplicate file exists. +; a non-existant thread is accessed +S_NOTHREADERR = Error: Specified thread does not exist. +;Returns error when a reply (res) cannot be found (shouldn't happen) +S_REPORTERR = Error: Cannot find reply. + +; wrong password (when user tries to delete file) +S_BADDELPASS = Error: Password incorrect. +; wrong password (when trying to access Manager modes) +S_WRONGPASS = Error: Management password incorrect. +; unable to delete post (sticky, /vg/ thread) +S_MAYNOTDEL = Error: You cannot delete this post. +; unable to delete post (too long) +S_RENZOKU_DEL_CANT_AFTER = Error: You cannot delete a post this old. +; too soon +S_RENZOKU_DEL = Error: You must wait longer before deleting this post. +; too fast +S_FLOOD_DEL = Error: You cannot delete posts this often. +; post is stickied +S_MAYNOTDELSTICKY = Error: You must un-sticky this thread before doing this. +; when a moderator deletes a protected thread +S_MAYNOTDELPROTECTED = Error: You must un-protect this thread before deleting it. +; when a janitor deletes a protected thread +S_THREADPROTECTED = Error: This thread is protected. +; A thread is closed +S_MAYNOTREPLY = Error: You cannot reply to this thread anymore. +S_MAXIMAGESREACHED = Error: Max limit of {{MAX_IMGRES}} image replies has been reached. + +; MySQL connection failed - host is not accepting connections or bad user/pass +S_SQLCONF = Error: Database connection failure. + +; failed spam filter in some way (the last three don't ban) +S_BANNEDTEXT = Error: Your post contained banned text. +S_BANNEDLINK = Error: Your post contained a banned URL. +S_REJECTTEXT = Error: Our system thinks your post is spam. Please reformat and try again. +S_REJECTTEXTBAN = Error: Our system thinks your post is spam. +S_IMGCONTAINSFILE = Error: Your image contains an embedded file. + +; IP Rangebans +S_IPRANGE_BLOCKED = Posting from your IP range +S_IPRANGE_BLOCKED_IMG = Uploading files from your IP range +S_IPRANGE_BLOCKED_OP = Posting threads from your IP range +S_IPRANGE_BLOCKED_TEMP = has been temporarily blocked due to abuse [More Info]. +S_IPRANGE_BLOCKED_PERM = has been permanently blocked due to abuse [More Info]. + +; Lenient rangeban bypassed by verified or known users +S_IPRANGE_BLOCKED_L1 =
    Please verify your e-mail address or try again later. +; Lenient rangeban bypassed by known users only +S_IPRANGE_BLOCKED_L2 =
    Please try again in a while. +; Lenient rangeban bypassed by verified users only +S_IPRANGE_BLOCKED_L3 =
    Please verify your e-mail address to continue. + +S_IPRANGE_BLOCKED_PASS =

    4chan Pass users can bypass this block. [Learn More] + +; failed CAPTCHA check +S_NOCAPTCHA = Error: You forgot to solve the CAPTCHA. Please try again.

    4chan Pass users can bypass this CAPTCHA. [Learn More] +S_BADCAPTCHA = Error: You seem to have mistyped the CAPTCHA. Please try again.

    4chan Pass users can bypass this CAPTCHA. [Learn More] +S_CAPTCHATIMEOUT = Error: This CAPTCHA is no longer valid because it has expired. Please try again.

    4chan Pass users can bypass this CAPTCHA. [Learn More] + +; couldn't post to captcha server +S_CAPTCHACOMM = Error: Couldn't connect to the CAPTCHA server. + +S_VIRUS = Error: Automated posting and/or virus detected. + +; Captcha spam / validity check +S_CAPTCHA_TOO_FAST = Error: You are requesting CAPTCHAs too quickly. Please wait 2 minutes and try again. +S_CAPTCHA_WRITE_ERROR = Error: CAPTCHA connection error. Please try again. +S_CAPTCHA_READ_ERROR = Error: CAPTCHA retrieval error. Please try again. +S_CAPTCHA_EXPIRED = Error: This CAPTCHA is no longer valid because it is too old. Please try again. + +; CAPTCHA description before loading a CAPTCHA +S_CAPTCHA_LOAD_DESC = When you've finished writing your post, click the box below to load a CAPTCHA.

    \ +Note: CAPTCHAs are only valid for 1 minute, and will automatically refresh if time runs out. + +S_CAPTCHA_TIP_BEFORE_SUBMIT = Instructions: Enter the CAPTCHA into the box above and click "Solve CAPTCHA." +S_CAPTCHA_TIP_AFTER_SUBMIT = Instructions: Copy and paste this code into the box below and submit your post. + +S_CAPTCHA_REFRESH = Refresh +S_CAPTCHA_TIME_REMAINING = Time: + +; Ban Request (HUEHUEHUE) +S_BLOCKEDBYBR = Error: You are temporarily blocked from posting. [More Info] + + +; Report errors +S_ALREADYREPORTED = You have already reported this post, or someone with your IP has already reported it. +S_REPORTFLOOD = You must wait longer before reporting another post. + +; Generic "cannot report this post" error +S_CANNOTREPORT = Error: You cannot report this post. +S_CANNOTREPORTSTICKY = Error: You cannot report a sticky. +S_CANNOTREPORTPOSTS = You cannot report posts on this board. + +; Internal assert failed (what does this even mean?) +S_ASSERT = Error: Internal server error. + +[Success strings] + +;Defines message to be displayed when file is successfully uploaded +S_UPGOOD = uploaded! +;Defines message to be displayed when post is successful +S_SCRCHANGE = Updating page. +;Message when new table was created (when imgboard.php is first run) +S_TABLE = Table created. + +[Post Formatting Strings] + +;Day abbreviations +S_SUN = Sun +S_MON = Mon +S_TUE = Tue +S_WED = Wed +S_THU = Thu +S_FRI = Fri +S_SAT = Sat + +[Default Text] + +; Text used when no name is entered +S_ANONAME = Anonymous +; Text used when no comment is entered +S_ANOTEXT = +; Text used when no subject is entered +S_ANOTITLE = + +; Text to the left of filename link +S_PICNAME = File +; Text to left of ad link +S_ADNAME = Ad +; Reply link +S_REPLY = Reply +; Shown in a thread when it will expire soon +S_OLD = Marked for deletion (old). + +[4chan Pass Strings] + +S_PASSFORMATCHANGED = You must re-authorize this device in order to continue using your 4chan Pass. [Re-authorize Device] +S_INVALIDPASS = 4chan Pass Token or PIN is invalid. [Re-authorize Device] +S_INVALIDPASSSESSION = Error: Your 4chan Pass session is invalid or expired. [Re-authorize Device] +S_PASSDISABLED = This 4chan Pass has been disabled. +S_PASSINUSE = This 4chan Pass is currently in use by another IP. [More Info] +S_PASSINUSE2 = This 4chan Pass is currently in use in another country. [Re-authorize Device] +S_PASSEXPIRED = This 4chan Pass has expired. [Renew] + +[User was X for this post strings] +S_USERWARNEDFORPOST = USER WAS WARNED FOR THIS POST +S_USERBANNEDFORPOST = USER WAS BANNED FOR THIS POST diff --git a/css/0ch.css b/css/0ch.css new file mode 100644 index 0000000..15d416a --- /dev/null +++ b/css/0ch.css @@ -0,0 +1,1725 @@ +/** GENERIC / ELEMENT STYLING **/ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #EFEFEF;; + color: #000000; + font-size: 12pt; + font-family: 'Times New Roman', serif; + margin: 0; + padding: 10px; +} + +body.is_index { + background-image: url('https://s.4cdn.org/image/ba.gif'); + padding: 10px 40px; +} + +.is_index #boardNavDesktop, +.is_index #boardNavDesktopFoot, +.is_index .boardBanner, +.is_index .sec-wrap, +.is_index .thread, +.is_index .navLinksBot, +.is_index .pagelist { + border-image: url('data:image/gif;base64,R0lGODlhGwAbAJECAP///6qqqv///wAAACH5BAEAAAIALAAAAAAbABsAAAJRhI+pGSAPo5yx0YuFzRzuzn0gJo5UaUqoFrSu60xrgMWqXdUn7uky/6BdgBqikGI0+m6nZUU53DWjv93CIE1dVtpiN/t9hpljD7HbeKnX7FcBADs=') 9 / 9px; + padding: 14px; + image-rendering: crisp-edges; +} + +#boardNavDesktop, +#boardNavDesktopFoot, +.boardBanner, +.sec-wrap, +.thread, +.navLinksBot, +.pagelist { + margin-bottom: 20px; +} + +.is_index #boardNavDesktop, +.is_index #boardNavDesktopFoot, +.is_index .boardBanner, +.is_index .navLinksBot, +.is_index .sec-wrap, +.is_index .pagelist { + background-color: #CCFFCC; +} + +.sec-wrap, +#boardNavDesktopFoot { + display: flow-root; +} + +.thread { + background-color: #EFEFEF; +} + +hr { + height: 1px; + border: 0; + background-color: #aaa; +} + +body > hr, +div.board > hr, +.sideArrows { display: none } + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +.adc-resp-bg { + margin: auto; + width: 728px; + height: 112px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } + + #quickReply { + z-index: 9000 !important; + } + + .adc-resp-bg { + width: 300px; + height: 250px; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.n-atb { + border-radius: 6px; + padding: 1px 4px; + color: #353839; +} + +.atsb2018 table { + margin: auto; + font-size: 90%; +} +.atsb2018 td { + white-space: nowrap; + padding: 0px 4px; +} +.atsb2018 .atsgc { + width: 280px; +} +.atsb2018 .atgg { + height: 20px; +} + +.n-atb-0 { background-color: #F56FA1; } +.n-atb-1 { background-color: #7B3F00; color: #F2F3F4; } +.n-atb-2 { background-color: #FFFDD0; } +.n-atb-3 { background-color: #E4D00A; } +.n-atb-4 { background-color: #50C878; } + +.n-atb-0::after { content: 'Team Peep'; } +.n-atb-1::after { content: 'Team Chocolate'; } +.n-atb-2::after { content: 'Team Creme'; } +.n-atb-3::after { content: 'Team Peanut Butter'; } +.n-atb-4::after { content: 'Team Mini'; } + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer a { + color: #0000ff; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #d6daf0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #b7c5d9; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 102px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline { + display: none !important; +} + +a, a:visited { + color: #34345C; +} + +a.replylink, div#absbot a { + text-decoration: underline !important; +} + +a:hover { + color: #DD0000; +} + +div#absbot { + color: #000000; + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img { + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +div.board > hr { + clear: both; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +div.container { + margin: 0px !important; + padding: 0px !important; + + display: block; + + /** This fixes annoying margins and makes it a real container **/ + line-height: 0em; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#qr-painter-ctrl .oe-r-cb { + vertical-align: sub; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/** MOBILE ONLY DISABLES **/ +.mobile { + display: none; +} + +/** HEADER **/ +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules > li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; + + color: #AF0A0F; +} + +#bannerCnt { + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + + font-size: 24pt; + font-weight: bold; + + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: 10pt; +} + +div#boardNavDesktop { + font-size: 11pt; + display: block; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; +} + +/** General Containers **/ +div.pContainer { + +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + + float: left; + + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +/** Thread Container **/ +div.thread { + clear: both; +} + +/** Post Container **/ +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread > div:nth-of-type(2) > div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #EFEFEF; + display: table; + padding: 2px; +} + +div.reply input { + float: none; +} + +/** Post Information **/ +div.post div.postInfo { + display: block; + width: 100%; +} + +.fileText { + max-width: 600px; + white-space: nowrap; +} + +div.post div.postInfo span.postNum { + +} + +div.post div.postInfo span.postNum a { + text-decoration: none; + color: #000000; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: #DD0000 !important; +} + +/* Name */ +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + color: #117743; + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + color: #117743; + font-weight: normal !important; +} + +/* Date/Time */ +div.post div.postInfo span.date { + +} + +div.post div.postInfo span.time { + +} + +/* Subject */ +div.post div.postInfo span.subject { + color: #FF0000; + font-weight: bold; + font-size: 22px; +} + +/** Message **/ +div.post blockquote.postMessage { + display: block; +} + +blockquote > span.quote { + color: #789922; +} + +.quoteLink, .quotelink, .deadlink { + color: #D00 !important; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-list .quotelink { + color: #34345C !important; +} + +#arc-list .quotelink:hover { + color: #D00 !important; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +/** File Information **/ +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + + margin-bottom: 5px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + + float: left; +} + +/** Summary **/ +span.summary { + margin-top: 10px; +} + +/** POST FORM **/ +div.postingMode { + background-color: #e04000; + padding: 1px; + text-align: center; + + color: #fff; + font-weight: bold; + + margin-top: 8px; +} + +.is_thread div.navLinks { + margin-top: 20px; +} + +.is_thread div.navLinksBot { + margin-bottom: 20px; +} + +#verification table { + border: none !important; + margin: 0px; +} + +/** FOOTER **/ +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; + +} + +div.pagelist { + overflow: hidden; +} + +div.pagelist > div { + float: left; + margin: 1px; + margin-right: 4px; +} + +div.pagelist div.pages, div.pagelist div span { + padding-top: 3px; + padding-bottom: 3px; + display: inline-block; +} + +div.pagelist form { + display: inline; +} + +div.pagelist strong { + color: #000000; +} + +.pages.cataloglink::before { + content: '['; +} + +.pages.cataloglink::after { + content: ']'; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; +} + +div#boardNavDesktopFoot { + clear: both; + font-size: 11pt; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: 8pt !important; + + padding-bottom: 4px; + padding-top: 10px; + clear: both; + + color: #000; +} + +#recaptcha_response_field { + padding: 0px; +} + +/** POST FORM **/ +table#postForm { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +#postForm { + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #aaa; + color: #000; + font-weight: bold; + +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +table#postForm td { + margin: 0px; + padding: 0px; + +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 260px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +input[type=password] { + width: 70px; +} + +table.postForm input[type="submit"] { + margin-left: 5px; +} + +.postblock { + background-color: #aaa; + color: #000; + font-weight: bold; + padding: 0 5px; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.reply:target, .reply.highlight { + background: #aaa !important; +} + +.highlight-anti { + background-color: #eee !important; +} + +.hand { + cursor: pointer; +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: red; + text-align: center; +} + +.highlightPost:not(.op) { + background: #c1c6e2 !important; +} + +span.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +span.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; +} + +span.capcodeDeveloper span.name, span.capcodeDeveloper span.name a, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +span.capcodeManager span.name, span.capcodeManager span.name a, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +span.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +.summary, .omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #070707; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +#navtopright, #navbotright { + float: right; + font-size: 11pt; +} + +#settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #D6DAF0; + overflow: hidden; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/** QUICK REPLY **/ +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; + +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; + +} + +div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; + +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail .name, .useremail .postertrip { + color: #34345C !important; +} + +.useremail:hover * { + color: #DD0000 !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; + + margin: 0; + font-size: 14px; +} + +.preview { + background-color: #D6DAF0; + border: 1px solid rgba(0, 0, 0, 0.20); + border-bottom: 2px solid rgba(0, 0, 0, 0.20); + border-right: 2px solid rgba(0, 0, 0, 0.20); +} + +/** this is not important **/ +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing { + margin: 0 auto; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #117743; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #117743; +} + +table.flashListing .subject { + color: #cc1105; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: #e0e5f6; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +textarea[name="com"] { + width: 296px; +} + +#recaptcha_response_field { + border: 1px solid #aaa !important; + width: 300px !important; + font-size: 10pt !important; +} + +table.postForm > tbody > tr > td > input[type="text"] { + width: 244px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + background: inherit; + font-family: serif; + color: #0F0C5D; + font-weight: 800; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; +} +#postForm textarea { + width: 292px; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 13px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #EFEFEF; + border: 1px solid #aaa; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #aaa; +} +.dd-menu li:hover { + background-color: #fafafa; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +#g-search-form { + text-align: center; +} +.g-search-ctrl { + vertical-align: top; + margin: 0 3px; +} +#js-sf-qf { + width: 185px; + padding: 2px 4px 3px 4px; +} +#js-sf-bf { + padding: 2px 4px 3px 4px; + width: 125px; +} +#js-sf-status { + text-align: center; + font-size: 24px; +} +.js-sf-err { + color: #C41E3A; +} +.boardBlock { + font-weight: bold; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.ogv-cnt { + position: relative; + display: inline-block; + text-align: center; + min-height: 16px; +} + +.ogv-cnt:not(.ogv-detached)::before { + content: 'Loading…'; + position: absolute; + top: 8px; + left: 0; + font-weight: bold; +} + +.ogv-cnt.ogv-loaded::before { + display: none; +} + +.ogv-cnt > ogvjs { + position: initial !important; +} + +.ogv-ctrl { + position: absolute; + bottom: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.65); + width: 100%; + height: 32px; + gap: 0; + display: none; +} + +.ogv-btn { + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + color: white; +} + +.ogv-btn svg { + vertical-align: middle; +} + +.ogv-btn svg:last-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:first-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:last-child { + display: inline; +} + +.ogv-ts { + font-family: sans-serif; + font-size: 10px; + text-align: center; + line-height: 32px; + width: 70px; + color: white; + overflow: hidden; +} + +.ogv-vol { + width: 50px; + margin: 0; +} + +.ogv-seek { + width: 0; + margin: 0; + flex-grow: 1; +} diff --git a/css/burichan.css b/css/burichan.css new file mode 100644 index 0000000..ba87605 --- /dev/null +++ b/css/burichan.css @@ -0,0 +1,320 @@ +body { + font-size: 12pt; + background: #EEF2FF; + color: #000000; + padding-left: 5px; + padding-right: 5px; + margin-right: 0px; + margin-left: 0px; + margin-top: 5px; +} + +center iframe { + border: 1px solid black; +} + +a, a:visited { + background: inherit; + color: #34345C; + font-family: serif; +} + +a:hover { + color: #DD0000; + background: inherit; + font-family: serif; +} + +a.quotelink { + background: inherit; + color: #DD0000; + font-family: serif; +} + +.logo { + clear: both; + text-align: center; + background: inherit; + font-size: 24pt; + color: #AF0A0F; + width: 100%; +} + +.postarea { + background: inherit; +} + +form { + margin-top: 0px; +} + +.rules { + width: 468px; + font-size: 10px; + font-family: sans-serif; +} + +.rules a, .rules a:visited, .rules a:link { + font-family: sans-serif; +} + +.rules li { + margin-left: 1em; + text-indent: -1em; +} + +.postblock { + background: #9988EE; + color: #000000; + font-weight: 800; +} + +.footer { + text-align: center; + font-size: 12px; + font-family: serif; +} + +.unkfunc { + color: #789922; +} + +.filesize { + font-size: 16px; + font-family: serif; + text-decoration: none; +} + +.filesize span, span.postername, span.filetitle, span.commentpostername { + unicode-bidi: embed; +} + +.filetitle, .replytitle { + background: inherit; + font-size: 18px; + font-family: serif; + color: #0F0C5D; + font-weight: 800; +} + +.postername, .commentpostername { + background: inherit; + font-size: 16px; + font-family: serif; + color: #117743; + font-weight: 800; +} + +.postertrip { + background: inherit; + font-size: 16px; + font-family: serif; + color: #228854; +} + +.oldpost { + background: inherit; + font-family: serif; + color: #0F0C5D; + font-weight: 800; +} + +.omittedposts, .abbr { + background: inherit; + color: #070707; +} + +.reply { + background: #D6DAF0; + color: #000000; + font-family: serif; +} + +.replyhl { + background: #D6BAD0; + color: #000000; + font-family: serif; +} + +.doubledash { + vertical-align: top; + clear: both; + float: left; +} + +a.quotejs:active, a.quotejs:link, a.quotejs:visited { + color: #000000; + text-decoration: none; +} + +a.quotejs:hover { + font-weight: bold; +} + +.tn_thread { + width: 200px; + height: 100px; + margin: 0px 20px 20px 20px; + float: left; + background: #eef2ff; + border: #98e 1px solid; + text-align: center; +} + +.tn_reply { + width: 100px; + height: 100px; + margin: 0px 20px 20px 20px; + float: left; + background: #eef2ff; + border: #98e 1px solid; + text-align: center; +} + +.adHeadline { + font: bold 10pt serif; + text-decoration: underline; + color: #00e +} + +.adText { + font: normal 10pt serif; + text-decoration: none; + color: #000 +} + +#ad { + width: 120px; + + margin: 0; + padding: 0; + + position: absolute; + right: 150px; + + border: 1px solid #98E; + + font-family: arial, helvetica, sans-serif; + font-size: 11px; + +} + +#ad div { + margin: 0; + padding: 0.4em; + +} + +#ad div.ad-title { + padding: 0em; + background: #98E; + color: #000; + font-size: 11px; +} + +#ad div.ad-title a { + font-family: arial, helvetica, sans-serif; + color: #000; +} + +#ad div.ad-text { +} + +#ad div.ad-text a { + font-family: arial, helvetica, sans-serif; +} + +#ad div.ad-text a:visited { +} + +.bottomAdTitle { + font-family: arial, helvetica, sans-serif; + background: #98E; + color: #000000; +} + +#bottomAd { + height: 52px; + font-size: 11px; +} + +#bottomAdOuter { + width: 600px; + border: 1px solid #98E; + font-size: 11px; +} + +.spoiler a.quotelink, .spoiler .unkfunc { + color: inherit; +} + +.exif { + display: none; + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td { + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td b { + font-weight: 800; + text-decoration: underline; + font-size: x-small; +} + +#header { + position: absolute; + left: 5px; + right: 5px; +} + +* html #header { + width: 100%; +} + +#navtop, #navbot { + left: 0px; + float: left; + font-size: 11pt; +} + +#navtopr, #navbotr { + right: 0px; + display: block; + text-align: right; + font-size: 11pt; +} + +#footer { + clear: both; +} + +.fstitle { + float: left; + width: 25px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#recaptcha_response_field { + border: 1px solid #AAA !important; +} + +#recaptcha_div { + height: 107px; + width: 442px; +} + +#recaptcha_challenge_field { + width: 400px +} \ No newline at end of file diff --git a/css/burichannew.css b/css/burichannew.css new file mode 100644 index 0000000..a670964 --- /dev/null +++ b/css/burichannew.css @@ -0,0 +1,1682 @@ +/** GENERIC / ELEMENT STYLING **/ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #EEF2FF; + + color: #000000; + font-size: 12pt; + font-family: 'Times New Roman', serif; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +.adc-resp-bg { + margin: auto; + width: 728px; + height: 112px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } + + #quickReply { + z-index: 9000 !important; + } + + .adc-resp-bg { + width: 300px; + height: 250px; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.n-atb { + border-radius: 6px; + padding: 1px 4px; + color: #353839; +} + +.atsb2018 table { + margin: auto; + font-size: 90%; +} +.atsb2018 td { + white-space: nowrap; + padding: 0px 4px; +} +.atsb2018 .atsgc { + width: 280px; +} +.atsb2018 .atgg { + height: 20px; +} + +.n-atb-0 { background-color: #F56FA1; } +.n-atb-1 { background-color: #7B3F00; color: #F2F3F4; } +.n-atb-2 { background-color: #FFFDD0; } +.n-atb-3 { background-color: #E4D00A; } +.n-atb-4 { background-color: #50C878; } + +.n-atb-0::after { content: 'Team Peep'; } +.n-atb-1::after { content: 'Team Chocolate'; } +.n-atb-2::after { content: 'Team Creme'; } +.n-atb-3::after { content: 'Team Peanut Butter'; } +.n-atb-4::after { content: 'Team Mini'; } + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer a { + color: #0000ff; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #d6daf0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #b7c5d9; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 102px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline { + display: none !important; +} + +a, a:visited { + color: #34345C; +} + +a.replylink, div#absbot a { + text-decoration: underline !important; +} + +a:hover { + color: #DD0000; +} + +div#absbot { + color: #000000; + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img { + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +div.board > hr { + clear: both; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +div.container { + margin: 0px !important; + padding: 0px !important; + + display: block; + + /** This fixes annoying margins and makes it a real container **/ + line-height: 0em; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#qr-painter-ctrl .oe-r-cb { + vertical-align: sub; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/** MOBILE ONLY DISABLES **/ +.mobile { + display: none; +} + +/** HEADER **/ +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules > li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; + + color: #AF0A0F; +} + +#bannerCnt { + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + + font-size: 24pt; + font-weight: bold; + + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: 10pt; +} + +div#boardNavDesktop { + font-size: 11pt; + display: block; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; +} + +/** General Containers **/ +div.pContainer { + +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + + float: left; + + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +/** Thread Container **/ +div.thread { + + margin: 0px; + + clear: both; +} + +/** Post Container **/ +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread > div:nth-of-type(2) > div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #D6DAF0; + + display: table; + + padding: 2px; +} + +div.reply input { + float: none; +} + +/** Post Information **/ +div.post div.postInfo { + display: block; + width: 100%; +} + +.fileText { + max-width: 600px; + white-space: nowrap; +} + +div.post div.postInfo span.postNum { + +} + +div.post div.postInfo span.postNum a { + text-decoration: none; + color: #000000; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: #DD0000 !important; +} + +/* Name */ +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + color: #117743; + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + color: #117743; + font-weight: normal !important; +} + +/* Date/Time */ +div.post div.postInfo span.date { + +} + +div.post div.postInfo span.time { + +} + +/* Subject */ +div.post div.postInfo span.subject { + color: #0F0C5D; + font-weight: bold; +} + +/** Message **/ +div.post blockquote.postMessage { + display: block; +} + +blockquote > span.quote { + color: #789922; +} + +.quoteLink, .quotelink, .deadlink { + color: #D00 !important; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-list .quotelink { + color: #34345C !important; +} + +#arc-list .quotelink:hover { + color: #D00 !important; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +/** File Information **/ +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + + margin-bottom: 5px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + + float: left; +} + +/** Summary **/ +span.summary { + margin-top: 10px; +} + +/** POST FORM **/ +div.postingMode { + background-color: #e04000; + padding: 1px; + text-align: center; + + color: #fff; + font-weight: bold; + + margin-top: 8px; +} + +div.navLinks { + margin-bottom: 10px; +} + +div.navLinksBot { + margin-bottom: 0px; +} + +#verification table { + border: none !important; + margin: 0px; +} + +/** FOOTER **/ +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; + +} + +div.pagelist { + overflow: hidden; + + border: 1px solid gray; + background: #EEF2FF; + border: 1px solid black; + border-left-color: gray; + border-top-color: gray; + padding: 1px; + + display: inline-block; +} + +div.pagelist > div { + float: left; + + margin: 1px; + + border: 1px solid gray; + border-left-color: black; + border-top-color: black; +} + +div.pagelist div.pages, div.pagelist div span { + padding-top: 3px; + padding-bottom: 3px; + display: inline-block; +} + +div.pagelist form { + display: inline; +} + +div.pagelist strong { + color: #000000; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; +} + +div#boardNavDesktopFoot { + clear: both; + font-size: 11pt; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: 8pt !important; + + padding-bottom: 4px; + padding-top: 10px; + clear: both; + + color: #000; +} + +#recaptcha_response_field { + padding: 0px; +} + +/** POST FORM **/ +table#postForm { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +#postForm { + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #98E; + color: #000; + font-weight: bold; + +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +table#postForm td { + margin: 0px; + padding: 0px; + +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 260px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +input[type=password] { + width: 70px; +} + +table.postForm input[type="submit"] { + margin-left: 5px; +} + +.postblock { + background-color: #98E; + color: #000; + font-weight: bold; + padding: 0 5px; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.reply:target, .reply.highlight { + background: #D6BAD0 !important; +} + +.hand { + cursor: pointer; +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: red; + text-align: center; +} + +.highlightPost:not(.op) { + background: #c1c6e2 !important; +} + +span.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +span.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; +} + +span.capcodeDeveloper span.name, span.capcodeDeveloper span.name a, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +span.capcodeManager span.name, span.capcodeManager span.name a, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +span.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +.summary, .omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #070707; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +#navtopright, #navbotright { + float: right; + font-size: 11pt; +} + +#settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #D6DAF0; + overflow: hidden; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/** QUICK REPLY **/ +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; + +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; + +} + +div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; + +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail .name, .useremail .postertrip { + color: #34345C !important; +} + +.useremail:hover * { + color: #DD0000 !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; + + margin: 0; + font-size: 14px; +} + +.preview { + background-color: #D6DAF0; + border: 1px solid rgba(0, 0, 0, 0.20); + border-bottom: 2px solid rgba(0, 0, 0, 0.20); + border-right: 2px solid rgba(0, 0, 0, 0.20); +} + +/** this is not important **/ +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing { + margin: 0 auto; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #117743; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #117743; +} + +table.flashListing .subject { + color: #cc1105; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: #e0e5f6; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +textarea[name="com"] { + width: 296px; +} + +#recaptcha_response_field { + border: 1px solid #aaa !important; + width: 300px !important; + font-size: 10pt !important; +} + +table.postForm > tbody > tr > td > input[type="text"] { + width: 244px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + background: inherit; + font-family: serif; + color: #0F0C5D; + font-weight: 800; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; +} +#postForm textarea { + width: 292px; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 13px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #B7C5D9; +} +.dd-menu li:hover { + background-color: #EEF2FF; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +#g-search-form { + text-align: center; +} +.g-search-ctrl { + vertical-align: top; + margin: 0 3px; +} +#js-sf-qf { + width: 185px; + padding: 2px 4px 3px 4px; +} +#js-sf-bf { + padding: 2px 4px 3px 4px; + width: 125px; +} +#js-sf-status { + text-align: center; + font-size: 24px; +} +.js-sf-err { + color: #C41E3A; +} +.boardBlock { + font-weight: bold; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.ogv-cnt { + position: relative; + display: inline-block; + text-align: center; + min-height: 16px; +} + +.ogv-cnt:not(.ogv-detached)::before { + content: 'Loading…'; + position: absolute; + top: 8px; + left: 0; + font-weight: bold; +} + +.ogv-cnt.ogv-loaded::before { + display: none; +} + +.ogv-cnt > ogvjs { + position: initial !important; +} + +.ogv-ctrl { + position: absolute; + bottom: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.65); + width: 100%; + height: 32px; + gap: 0; + display: none; +} + +.ogv-btn { + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + color: white; +} + +.ogv-btn svg { + vertical-align: middle; +} + +.ogv-btn svg:last-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:first-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:last-child { + display: inline; +} + +.ogv-ts { + font-family: sans-serif; + font-size: 10px; + text-align: center; + line-height: 32px; + width: 70px; + color: white; + overflow: hidden; +} + +.ogv-vol { + width: 50px; + margin: 0; +} + +.ogv-seek { + width: 0; + margin: 0; + flex-grow: 1; +} diff --git a/css/catalog_0ch.css b/css/catalog_0ch.css new file mode 100644 index 0000000..1ac377e --- /dev/null +++ b/css/catalog_0ch.css @@ -0,0 +1,1669 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; + font-family: 'Times New Roman', serif; +} + +div.reply { + background-color: #D6DAF0; +} +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.burichan_new .click-me { + color: #000; + background-color: #D6DAF0; + border: 2px solid #B7C5D9; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.burichan_new .click-me:before { + border-color: #B7C5D9 transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.burichan_new .click-me:after { + border-color: #D6DAF0 transparent; + border-style: solid; +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer a { + color: #0000ff; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #d6daf0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #b7c5d9; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input, +#qr-painter-ctrl input { + width: 30px !important; + text-align: center; +} + +/* reCaptcha */ +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +@media only screen and (min-width: 481px) { + .recaptcha_input_area { + padding: 0!important; + } + #recaptcha_table tr:first-child { + height: auto!important; + } + + #recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; + } + + #recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; + } + + #recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; + } + #recaptcha_image { + cursor: pointer; + } + #recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; + } + input:-moz-placeholder { color: gray !important; } + #recaptcha_image { + border: 1px solid #aaa !important; + } + #recaptcha_table tr > td:last-child { + display: none !important; + } + #captchaContainer { + width: 343px; + height: 86px; + line-height: 102px; + overflow: hidden; + } + #captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; + } +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; +} + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + background-color: #EEF2FF; + color: #000; + font-size: 12pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +.button { + color: #34345C; +} + +a:hover, +.button:hover { + color: #DD0000; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; +} + +p { + margin-bottom: 15px; +} + +#content textarea { + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +#content input[type="text"], +#content input[type="password"] { + font-size: 11px; + margin: 0px 2px; + padding: 1px; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +.button { + text-decoration: underline; + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +.clickbox { + margin-right: 3px; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: 1px solid #aaa; + text-decoration: none; + color: #000; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 9002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 9003; + font-size: 14px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + background-color: #D6DAF0; +} + +.panel hr { + border: none; + border-top: 1px solid rgba(0, 0, 0, 0.2); +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #EEF2FF; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: red; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +div#boardNavDesktopFoot, +.boardnav { + color: #333; + font-size: 11pt; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +#bottomnav { + clear: both; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #D6DAF0; + overflow: hidden; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; +} + +div#boardNavMobile { + font-size: 12px; +} + +div#boardNavDesktop a { + padding: 1px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: red; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + color: #AF0A0F; + text-align: center; + clear: both; +} + +#bannerCnt { + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-size: 24pt; + font-weight: bold; + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: 10pt; +} + +div#boardNavDesktopFoot { + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: 8pt !important; + padding-bottom: 4px; + padding-top: 10px; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 9004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 165px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 10px; + line-height: 8px; + margin-top: 1px; +} + +.teaser { + display: none; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +#absbot a { + text-decoration: underline; +} + +#absbot a:hover { + color: #DD0000; +} + +/***************************************************************/ + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + padding: 3px; + border: 1px solid rgba(0, 0, 0, 0.20); + background-color: #D6DAF0; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + line-height: 1; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; + border-top: 1px solid rgba(0, 0, 0, 0.20); +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/***************************************************************/ + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + color: #000; + background-color: #D6DAF0; + border: 2px solid #B7C5D9; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #B7C5D9 transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #D6DAF0 transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: black; + border-radius: 3px; + font-size: 11pt; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + max-width: 400px; + color: white; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + color: #00A550; + font-weight: bold; +} + +.post-subject { + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #98E; + color: #000; + font-weight: bold; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +table#postForm td { + margin: 0px; + padding: 0px; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +textarea[name="com"] { + width: 296px; +} + +#recaptcha_response_field { + border: 1px solid #aaa !important; + width: 300px !important; + font-size: 10pt !important; + padding: 0px; +} + +table.postForm input[type="submit"] { + margin-left: 5px; +} + +.postblock { + background-color: #98E; + color: #000; + font-weight: bold; + padding: 0 5px; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + width: 70px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +iframe[src="about:blank"] { + display: none; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 90px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; + opacity: 0.5; +} + +.postMenuBtn:hover{ + opacity: 1; +} + +.postMenuBtn { + color: #000080; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 13px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #B7C5D9; +} +.dd-menu li:hover { + background-color: #EEF2FF; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} diff --git a/css/catalog_burichan_new.css b/css/catalog_burichan_new.css new file mode 100644 index 0000000..24c3c45 --- /dev/null +++ b/css/catalog_burichan_new.css @@ -0,0 +1,1765 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; + font-family: 'Times New Roman', serif; +} + +.nwsb { display: none; } + +div.reply { + background-color: #D6DAF0; +} +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.burichan_new .click-me { + color: #000; + background-color: #D6DAF0; + border: 2px solid #B7C5D9; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.burichan_new .click-me:before { + border-color: #B7C5D9 transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.burichan_new .click-me:after { + border-color: #D6DAF0 transparent; + border-style: solid; +} + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer a { + color: #0000ff; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #d6daf0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #b7c5d9; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +/* reCaptcha */ +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 102px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + background-color: #EEF2FF; + color: #000; + font-size: 12pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +.button { + color: #34345C; +} + +a:hover, +.button:hover { + color: #DD0000; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; +} + +p { + margin-bottom: 15px; +} + +#content textarea { + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +#content input[type="text"], +#content input[type="password"] { + font-size: 11px; + margin: 0px 2px; + padding: 1px; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +.button { + text-decoration: underline; + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +.clickbox { + margin-right: 3px; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: 1px solid #aaa; + text-decoration: none; + color: #000; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 100002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 100003; + font-size: 14px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + background-color: #D6DAF0; +} + +.panel hr { + border: none; + border-top: 1px solid rgba(0, 0, 0, 0.2); +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.extButton { + cursor: pointer; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #EEF2FF; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: red; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +div#boardNavDesktopFoot, +.boardnav { + color: #333; + font-size: 11pt; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +#bottomnav { + clear: both; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #D6DAF0; + overflow: hidden; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; +} + +div#boardNavMobile { + font-size: 12px; +} + +div#boardNavDesktop a { + padding: 1px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: red; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + color: #AF0A0F; + text-align: center; + clear: both; +} + +#bannerCnt { + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-size: 24pt; + font-weight: bold; + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: 10pt; +} + +div#boardNavDesktopFoot { + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: 8pt !important; + padding-bottom: 4px; + padding-top: 10px; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 100004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + margin-bottom: 20px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 180px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 11px; + line-height: 8px; + margin-top: 2px; + margin-bottom: 1px; +} + +.meta .postMenuBtn { + position: absolute; + margin-top: -1px; + display: none; +} + +.thread:hover .meta .postMenuBtn { display: inline-block; } + +.teaser { + display: none; + padding: 0 15px; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +#absbot a { + text-decoration: underline; +} + +#absbot a:hover { + color: #DD0000; +} + +/***************************************************************/ + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + padding: 3px; + border: 1px solid rgba(0, 0, 0, 0.20); + background-color: #D6DAF0; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + line-height: 1; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + overflow: auto; + display: block; + max-height: 100vh; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; + border-top: 1px solid rgba(0, 0, 0, 0.20); +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/***************************************************************/ + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + color: #000; + background-color: #D6DAF0; + border: 2px solid #B7C5D9; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #B7C5D9 transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #D6DAF0 transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: #181f24; + border-radius: 3px; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: #dedede; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + color: #00A550; + font-weight: bold; +} + +.post-subject { + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +.verified-capcode { + color: #007FFF; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #98E; + color: #000; + font-weight: bold; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +table#postForm td { + margin: 0px; + padding: 0px; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +textarea[name="com"] { + width: 296px; +} + +#recaptcha_response_field { + border: 1px solid #aaa !important; + width: 300px !important; + font-size: 10pt !important; + padding: 0px; +} + +table.postForm input[type="submit"] { + margin-left: 5px; +} + +.postblock { + background-color: #98E; + color: #000; + font-weight: bold; + padding: 0 5px; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + width: 70px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 20px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +.txt-no a { text-decoration: none } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; + opacity: 0.5; +} + +.postMenuBtn:hover{ + opacity: 1; +} + +.postMenuBtn { + color: #000080; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 13px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #B7C5D9; +} +.dd-menu li:hover { + background-color: #EEF2FF; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#customMenu { + width: 400px; + left: 50%; + margin-left: -200px; + padding: 2px; +} +#customMenu .reply { border: 0 } +#customMenuBox { + margin: 0px auto 5px; + width: 385px; + padding: 2px 4px 3px; + font-size: 10pt; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } +.boardSelect .custom-menu-ctrl, +.boardSelect .customBoardList { margin-left: 10px; } +.boardSelect .custom-menu-ctrl { display: none; } +.boardSelect:hover .custom-menu-ctrl { display: inline; } +.persistentNav .boardList a, +.persistentNav .customBoardList a, +#boardNavMobile .boardSelect a { text-decoration: none; } + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} diff --git a/css/catalog_futaba_new.css b/css/catalog_futaba_new.css new file mode 100644 index 0000000..a2f8169 --- /dev/null +++ b/css/catalog_futaba_new.css @@ -0,0 +1,1775 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; + font-family: 'Times New Roman', serif; +} + +.nwsb { display: none; } + +div.reply { + background-color: #f0e0d6; +} +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.futaba_new .click-me { + color: #800000; + background-color: #F0E0D6; + border: 2px solid #D9BFB7; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.futaba_new .click-me:before { + border-color: #D9BFB7 transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.futaba_new .click-me:after { + border-color: #F0E0D6 transparent; + border-style: solid; +} + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #f0e0d6; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #d9bfb7; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #ede4dc; + overflow: hidden; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; +} + +div#boardNavMobile { + font-size: 12px; +} + +div#boardNavDesktop a { + padding: 1px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/* reCaptcha */ +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 102px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + background-color: #FFFFEE; + color: #800000; + font-size: 12pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +.button { + color: #0000EE; +} + +a:hover, +.button:hover { + color: red; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; +} + +p { + margin-bottom: 15px; +} + +#content textarea { + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +#content input[type="text"], +#content input[type="password"] { + font-size: 11px; + margin: 0px 2px; + padding: 1px; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +.button { + text-decoration: underline; + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +.clickbox { + margin-right: 3px; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: 1px solid #aaa; + text-decoration: none; + color: #000; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 100002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 100003; + font-size: 14px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + background-color: #F0E0D6; +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.extButton { + cursor: pointer; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #FFFFEE; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: red; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +div#boardNavDesktopFoot, +.boardnav { + font-size: 11pt; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +#bottomnav { + clear: both; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: red; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + color: #AF0A0F; + text-align: center; + clear: both; +} + +#bannerCnt { + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-size: 24pt; + font-weight: bold; + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: 10pt; +} + +div#boardNavDesktopFoot { + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: 8pt !important; + padding-bottom: 4px; + padding-top: 10px; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 100004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + margin-bottom: 20px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 180px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 11px; + line-height: 8px; + margin-top: 2px; + margin-bottom: 1px; +} + +.meta .postMenuBtn { + position: absolute; + margin-top: -1px; + display: none; +} + +.thread:hover .meta .postMenuBtn { display: inline-block; } + +.teaser { + display: none; + padding: 0 15px; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +/***************************************************************/ + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + padding: 3px; + border: 1px solid rgba(0, 0, 0, 0.20); + background-color: #f0e0d6; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + line-height: 1; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + overflow: auto; + display: block; + max-height: 100vh; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; + border-top: 1px solid rgba(0, 0, 0, 0.20); +} + +.yotsuba_b_new #watchList li:first-child { + border-top: 1px solid #b7c5d9; +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/***************************************************************/ + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; +} + +.yotsuba_new #first-run, +.futaba_new #first-run { + color: #800000; + background-color: #F0E0D6; + border: 2px solid #D9BFB7; +} + +.yotsuba_b_new #first-run, +.burichan_new #first-run { + color: #000; + background-color: #D6DAF0; + border: 2px solid #B7C5D9; +} + +.tomorrow #first-run { + color: #C5C8C6; + background-color: #282A2E; + border: 2px solid #111; +} + +.photon #first-run { + color: #333; + background-color: #ddd; + border: 2px solid #ccc; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #D9BFB7 transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #F0E0D6 transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: #181f24; + border-radius: 3px; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: #dedede; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + color: #00A550; + font-weight: bold; +} + +.post-subject { + color: #CC1105; + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +.verified-capcode { + color: #007FFF; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #EA8; + color: #800; + font-weight: bold; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +table#postForm td { + margin: 0px; + padding: 0px; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +textarea[name="com"] { + width: 296px; +} + +#recaptcha_response_field { + border: 1px solid #aaa !important; + width: 300px !important; + font-size: 10pt !important; + padding: 0px; +} + +table.postForm input[type="submit"] { + margin-left: 5px; +} + +.postblock { + background-color: #EA8; + color: #800; + font-weight: bold; + padding: 0 5px; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + width: 70px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 20px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +.txt-no a { text-decoration: none } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; + opacity: 0.5; +} + +.postMenuBtn:hover{ + opacity: 1; +} + +.postMenuBtn { + color: #000080; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 13px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #D9BFB7; +} +.dd-menu li:hover { + background-color: #FFFFEE; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#customMenu { + width: 400px; + left: 50%; + margin-left: -200px; + padding: 2px; +} +#customMenu .reply { border: 0 } +#customMenuBox { + margin: 0px auto 5px; + width: 385px; + padding: 2px 4px 3px; + font-size: 10pt; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } +.boardSelect .custom-menu-ctrl, +.boardSelect .customBoardList { margin-left: 10px; } +.boardSelect .custom-menu-ctrl { display: none; } +.boardSelect:hover .custom-menu-ctrl { display: inline; } +.persistentNav .boardList a, +.persistentNav .customBoardList a, +#boardNavMobile .boardSelect a { text-decoration: none; } + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} diff --git a/css/catalog_mobile.css b/css/catalog_mobile.css new file mode 100644 index 0000000..5abbcbe --- /dev/null +++ b/css/catalog_mobile.css @@ -0,0 +1,396 @@ +@media only screen and (max-width: 480px) { + body { + padding: 0 !important; + } + + .party-hat { display: none; } + + div.mobile { + display: block !important; + } + + .ad-plea, + embed { + display: none !important; + } + + .small .thread, + .extended-small .thread { + width: 155px !important; + } + + div.boardBanner > img { + height: 50px !important; + margin: 5px 0 !important; + max-width: 100% !important; + width: 150px !important; + } + + .boardBanner { + margin-top: 40px; + } + + form[name="post"] { + margin: auto; + max-width: 100%; + } + + #threadWatcher { + max-width: none; + padding: 3px 0; + top: 30px; + left: 0; + width: 100%; + border-left: none; + border-right: none; + } + + #twClose { + position: relative; + top: 0; + margin-top: -1px; + float: left; + right: 0; + } + + #watchList { + padding: 0 10px; + } + + #postForm { + width: auto; + } + + #postForm .mobile { + display: table-row !important; + } + + #postForm textarea, + #postForm input[type="text"] { + margin-right: 0; + width: 220px; + } + + #postForm input[name="sub"] { + width: 160px; + } + + #postForm input[type="password"] { + margin-right: 0; + width: 70px; + } + + #postForm input[type="submit"] { + margin: 0; + padding: 2px 4px 3px; + width: 60px; + } + + .rules { + display: none; + } + + .recaptcha_image_cell { + width: auto !important; + padding: 0 !important; + } + + #captchaFormPart > td > div { + margin-left: 8px; + } + + #recaptcha_table tr > td:last-child { + display: none; + } + + #recaptcha_table tr[height="73"] { + height: auto !important; + } + + #recaptcha_table tr > td { + padding: 0 !important; + } + + #recaptcha_image { + width: 280px !important; + } + + #recaptcha_response_field { + width: 272px !important; + margin: -1px 2px 0 3px !important; + font-size: 10pt !important; + } + + #recaptcha_image > img { + width: 280px !important; + } + + table#recaptcha_table > tbody > tr:first-child > td:nth-child(2) { + display: none; + } + + table#recaptcha_table > tbody > tr:first-child > td:nth-child(2), + #recaptcha_table tr > td:last-child { + display: none; + } + + #toggleMsgBtn .mobile, + #boardNavMobile { + display: block !important; + } + + div#boardNavMobile, + div#boardNavMobile select, + div#boardNavMobile option { + font-size: x-small; + } + + .absbot .mobile, + .desktop { + display: none !important; + } + + #boardNavMobile { + font: 10px arial,helvetica,sans-serif; + overflow: hidden; + padding: 2px 4px; + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 2; + } + + .burichan_new #boardNavMobile, + .yotsuba_b_new #boardNavMobile { + background-color: #D6DAF0; + border-bottom: 2px solid #B7C5D9; + } + + .futaba_new #boardNavMobile, + .yotsuba_new #boardNavMobile { + background-color: #F0E0D6; + border-bottom: 2px solid #D9BFB7; + } + + .tomorrow #boardNavMobile { + background-color: #1D1F21; + border-bottom: 2px solid #282a2e; + } + + .photon #boardNavMobile { + background-color: #ddd; + border-bottom: 2px solid #ccc; + } + + .boardSelect { + float: left; + } + + .boardSelect strong { + padding-right: 5px; + } + + .pageJump { + float: right; + font-size: 7.5pt; + padding-right: 5px; + padding-top: 3px; + } + + .boardnav .pageJump a { + padding: 0 5px 0 0; + text-decoration: none; + } + + #filters { + width: 310px !important; + margin-left: -160px !important; + } + + #filters-search { display: none; } + + .filter-boards { + width: 30px !important; + } + + .filter-pattern { + width: 90px !important; + } + + #theme { + width: 310px !important; + margin-left: -160px !important; + } + + #theme-css { + width: 300px !important; + min-width: 300px !important; + } + + #globalToggle { + background-color: #FFADAD; + background-image: url('/image/buttonfade-red.png'); + border: 1px solid #C45858; + border-radius: 3px 3px 3px 3px; + color: #880000; + font-weight: bold; + padding: 6px 10px 5px; + text-align: center; + font-size: 10pt; + margin: 0 auto 10px; + width: 200px; + display: block !important; + text-decoration: none; + } + + #absbot, + #styleSwitcher { + display: none; + } + + #bottomnav { + padding-bottom: 40px; + } + + #toggleMsgBtn { + display: none !important; + } + + #togglePostForm { + margin: 11px 0; + font-size: inherit; + display: block; + } + + #ctrl { + text-align: center; + } + + #info { + float: none !important; + line-height: 30px; + } + + #settings { + line-height: 40px; + text-align: center !important; + } + + #ctrl hr.mobile { + display: block !important; + margin-top: 10px; + } + + .ctrl-wrap { + white-space: nowrap; + } + + .navLinks { + display: block; + margin-top: 25px; + text-align: center; + } + + .navLinksBottom { + margin-bottom: 25px; + } + + .bottomad { + margin-bottom: 10px; + } + + .hideMobile { + display: none; + } + + #info .navLinks { + margin-top: 10px; + } + + #search-label-bottom { display: none !important; } + + input[type="text"], input[type="password"], textarea { + font-size: 16px !important; + } + + img.topad, .topad > div, .topad a img { + width: 300px !important; + height: 250px !important; + } + + img.middlead, .middlead > div, .middlead a img { + width: 234px !important; + height: 30px !important; + max-width: 100%; + overflow: hidden; + margin: auto; + } + + img.bottomad, .bottomad > div, .bottomad a img { + width: 320px !important; + height: 40px !important; + max-width: 100%; + overflow: hidden; + margin: auto; + } + + .mobilebtn .btn-wrap { + border-radius: 3px 3px 3px 3px; + font-weight: bold; + padding: 6px 10px 5px; + background-repeat: repeat-x; + cursor: pointer; + } + + .mobilebtn .btn-wrap .button { + text-decoration: none; + color: inherit; + } + + .mobilebtn .btn-wrap:before, + .mobilebtn .btn-wrap:after { + content: ''; + } + + .yotsuba_b_new .mobilebtn .btn-wrap, + .burichan_new .mobilebtn .btn-wrap { + background-color: #D6DAF0; + background-image: url("/image/buttonfade-blue.png"); + border: 1px solid #B7C5D9; + color: #34345C; + } + + .yotsuba_new .mobilebtn .btn-wrap, + .futaba_new .mobilebtn .btn-wrap { + background-color: #F0E0D6; + background-image: url("/image/buttonfade.png"); + border: 1px solid #C0A69D; + color: #880000; + } + + .burichan_new .boardSelect strong, + .yotsuba_b_new .boardSelect strong { + color: #000; + } + + .tomorrow .mobilebtn .btn-wrap { + background-color: rgb(27,28,30); + background-image: url("/image/buttonfade-dark.png"); + background-repeat: repeat-x; + border: 1px solid #282A2E; + color: #707070; + } + + .photon .mobilebtn .btn-wrap { + background: rgb(238,238,238); + background: -moz-linear-gradient(top, rgba(238,238,238,1) 0%, rgba(224,224,224,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(238,238,238,1)), color-stop(100%,rgba(224,224,224,1))); + background: -webkit-linear-gradient(top, rgba(238,238,238,1) 0%,rgba(224,224,224,1) 100%); + background: -o-linear-gradient(top, rgba(238,238,238,1) 0%,rgba(224,224,224,1) 100%); + background: -ms-linear-gradient(top, rgba(238,238,238,1) 0%,rgba(224,224,224,1) 100%); + background: linear-gradient(to bottom, rgba(238,238,238,1) 0%,rgba(224,224,224,1) 100%); + border: 1px solid #CCCCCC; + color: #333; + } +} diff --git a/css/catalog_photon.css b/css/catalog_photon.css new file mode 100644 index 0000000..c5102f9 --- /dev/null +++ b/css/catalog_photon.css @@ -0,0 +1,1785 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, textarea, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; +} + +.nwsb { display: none; } + +a { + text-decoration: none; +} + +div.reply { + background-color: #DDD; + border: 1px solid #CCC; +} +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.photon .click-me { + color: #333; + background-color: #ddd; + border: 2px solid #ccc; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.photon .click-me:before { + border-color: #ccc transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.photon .click-me:after { + border-color: #DDD transparent; + border-style: solid; +} + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #ddd; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +.oe-r-cb { + vertical-align: middle; +} + +.painter-ctrl label { + margin-right: 4px; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #DDD; + overflow: hidden; + border-bottom: 2px solid #CCC; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; + font-size: 12px; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/* reCaptcha */ +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + background-color: #EEE; + color: #333; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +div#boardNavDesktopFoot a, +.boardnav a, +.button { + color: #FF6600; +} + +a:hover, +.boardnav a:hover, +div#boardNavDesktopFoot a:hover, +.button:hover { + color: #FF3300; +} + +hr { + clear: both; + border: none; + border-top: 1px solid #DDD; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; + color: #000; +} + +p { + margin-bottom: 15px; +} + +input[type="text"], +input[type="password"], +textarea { + border: 1px solid #AAA; + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +input[type=text], +textarea { + outline: none; +} + +input[type="text"], +input[type="password"] { + margin: 0px 2px; + padding: 1px; +} + +input:focus, +textarea:focus { + border: 1px solid #EEAA88 !important; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +.button { + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 100002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 100003; + font-size: 14px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + background-color: #DDD; +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.extButton { + cursor: pointer; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/photon/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #EEEEEE; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: #333; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +div#boardNavDesktopFoot, +.boardnav { + color: #333; + font-size: 9pt; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +#bottomnav { + clear: both; +} + +div#boardNavDesktopFoot a, +#boardNavDesktop a { + padding: 1px; + text-decoration: none; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: #333; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #000; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + margin-top: 0px; + color: #004A99; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktopFoot { + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + color: #333; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 100004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + margin-bottom: 20px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 180px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 11px; + line-height: 8px; + margin-top: 2px; + margin-bottom: 1px; +} + +.meta .postMenuBtn { + position: absolute; + margin-top: -1px; + display: none; +} + +.thread:hover .meta .postMenuBtn { display: inline-block; } + +.teaser { + display: none; + padding: 0 15px; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +#absbot a { + text-decoration: underline !important; +} + +/***************************************************************/ + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + padding: 3px; + border: 1px solid #ccc; + background-color: #ddd; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + overflow: auto; + display: block; + max-height: 100vh; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; +} + +#watchList li:first-child { + border-top: 1px solid #ccc; +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/***************************************************************/ + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + color: #333; + background-color: #ddd; + border: 2px solid #ccc; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #ccc transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #DDD transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: #181f24; + border-radius: 3px; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: #dedede; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + font-weight: bold; +} + +.post-subject { + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +.verified-capcode { + color: #007FFF; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #DDD; + color: #333; + font-weight: bold; + border: 1px solid #CCC; + padding: 0 5px; + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +#postForm input[type="text"], +#postForm input[type="password"], +table.postForm > tbody textarea, +#recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + border: 1px solid #AAA; + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + color: #333; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + text-align: center; + width: 50px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 20px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; + opacity: 0.5; +} + +.postMenuBtn:hover{ + opacity: 1; +} + +.postMenuBtn { + color: #FF6600; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #DDDDDD; + border: 1px solid #CCCCCC; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #CCCCCC; +} +.dd-menu li:hover { + background-color: #EEEEEE; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#customMenu { + width: 400px; + left: 50%; + margin-left: -200px; + padding: 2px; +} +#customMenu .reply { border: 0 } +#customMenuBox { + margin: 0px auto 5px; + width: 385px; + padding: 2px 4px 3px; + font-size: 10pt; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl, +.boardSelect .customBoardList { margin-left: 10px; } +.boardSelect .custom-menu-ctrl { display: none; } +.boardSelect:hover .custom-menu-ctrl { display: inline; } +.persistentNav .boardList a, +.persistentNav .customBoardList a, +#boardNavMobile .boardSelect a { text-decoration: none; } + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} diff --git a/css/catalog_spooky.css b/css/catalog_spooky.css new file mode 100644 index 0000000..0954bc0 --- /dev/null +++ b/css/catalog_spooky.css @@ -0,0 +1,1888 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, textarea, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; +} + +.nwsb { display: none; } + +a { + text-decoration: none; +} + +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.burichan_new .click-me { + color: #800000; + background-color: #F0E0D6; + border: 2px solid #D9BFB7; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.burichan_new .click-me:before { + border-color: #D9BFB7 transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.burichan_new .click-me:after { + border-color: #F0E0D6 transparent; + border-style: solid; +} + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #f0e0d6; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #d9bfb7; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#bottomnav { + clear: both; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #F0E0D6; + overflow: hidden; + border-bottom: 2px solid #D9BFB7; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; + font-size: 12px; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; + color: #000080 !important; +} + +/* reCaptcha */ +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +input[type="text"], input[type="password"], textarea { + border-color: #140814 !important; +} + +input[type="text"]:hover, input[type="text"]:focus, +textarea:hover, textarea:focus { border-color: 1px solid #dfbaba !important } + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +#absbot a, +.button { + color: #d98518; +} + +a:hover, +#absbot a:hover, +.button:hover { + color: #ff9d1c !important; + text-shadow: 0px 0px 12px rgba(217,133,24,1); +} + +hr { + clear: both; + height: 1px; + border: 0; + border-bottom: 1px solid #96b4aa; + opacity: 0.1; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; + color: #000; +} + +p { + margin-bottom: 15px; +} + +input[type="text"], +input[type="password"], +textarea { + border: 1px solid #AAA; + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +input[type=text], +textarea { + outline: none; +} + +input[type="text"], +input[type="password"] { + margin: 0px 2px; + padding: 1px; +} + +input:focus, +textarea:focus { + border: 1px solid #EEAA88 !important; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +/* Clickables */ +.button { + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +.clickbox { + margin-right: 3px; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: 1px solid #aaa; + text-decoration: none; + color: #000; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 100002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 100003; + font-size: 14px; + box-shadow: 0 0 6px rgba(0,0,0,1); + background-color: #261e37; +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid rgba(150, 180, 170, 0.1); + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; + filter: invert(50%); +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #FFFFEE; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: red; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +#boardNavDesktop { + color: #321f28; + font-size: 9pt; +} + +#boardNavDesktop a { + padding: 1px; + text-decoration: none; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: red; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #880000; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #321f28; + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #800000; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 100004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + margin-bottom: 20px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 180px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; + filter: contrast(90%) brightness(90%); +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 11px; + line-height: 8px; + margin-top: 2px; + margin-bottom: 1px; +} + +.meta .postMenuBtn { + position: absolute; + margin-top: -1px; + display: none; +} + +.thread:hover .meta .postMenuBtn { display: inline-block; } + +.teaser { + display: none; + padding: 0 15px; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +#absbot a { + text-decoration: underline; +} + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + background-color: #F0E0D6; + border-color: #800000 #D9BFB7 #D9BFB7 #800000; + border-image: none; + border-right: 1px solid #D9BFB7; + border-style: none solid solid none; + border-width: medium 1px 1px medium; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + overflow: auto; + display: block; + max-height: 100vh; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; + border-top: 1px solid #d9bfb7; +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/***************************************************************/ + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + color: #800000; + background-color: #F0E0D6; + border: 2px solid #D9BFB7; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #D9BFB7 transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #F0E0D6 transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: #181f24; + border-radius: 3px; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: #dedede; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + color: #00A550; + font-weight: bold; +} + +.post-subject { + color: #CC1105; + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +.verified-capcode { + color: #007FFF; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #EA8; + color: #800; + font-weight: bold; + border: 1px solid #800; + padding: 0 5px; + + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +#postForm input[type="text"], +#postForm input[type="password"], +table.postForm > tbody textarea, +#recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #EA8; + color: #800; + font-weight: bold; + padding: 0 5px; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + text-align: center; + width: 50px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 20px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; + border: 1px solid; + border-color: #FF7518CF; + border-bottom: 0; + border-right-width: 1px; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #FF7518CF; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#customMenu { + width: 400px; + left: 50%; + margin-left: -200px; + padding: 2px; +} +#customMenu .reply { border: 0 } +#customMenuBox { + margin: 0px auto 5px; + width: 385px; + padding: 2px 4px 3px; + font-size: 10pt; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } +.boardSelect .custom-menu-ctrl, +.boardSelect .customBoardList { margin-left: 10px; } +.boardSelect .custom-menu-ctrl { display: none; } +.boardSelect:hover .custom-menu-ctrl { display: inline; } +.persistentNav .boardList a, +.persistentNav .customBoardList a, +#boardNavMobile .boardSelect a { text-decoration: none; } + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; + color: #321f28; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + color: #321f28; + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} + +#postForm tr > td > .desktop, +#ctrl, +.navLinks, +.postNum.desktop > span { color: #321f28 } +.open-qr-wrap { color: #140814 !important } +.navLinks.desktop > * { color: #321f28 } +td { border-color: #140814 !important } + +@media (max-width: 480px) { + .ws .dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-right-width: 2px; + } + .ws .dd-menu li { + border-bottom: 1px solid #B7C5D9; + } + .ws .dd-menu li:hover { + background-color: #EEF2FF; + } + .nws .dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-right-width: 2px; + } + .nws .dd-menu li { + border-bottom: 1px solid #D9BFB7; + } + .nws .dd-menu li:hover { + background-color: #FFFFEE; + } + + body.ws { + background: url('/image/fade-blue.png') repeat-x scroll center top #EEF2FF; + color: #000; + } + + body.nws { + background: url('/image/fade.png') repeat-x scroll center top #FFFFEE; + color: #800000; + } +} + +@media (min-width: 481px) { + body { + background: #120c20; + color: #F0F8FFD1; + } + + .ctrl-wrap { color: #F0F8FFD1; } + + .ws div.boardBanner { + color: #5995b3; + } + + .nws div.boardBanner { + color: #b35968; + } + + .quoteLink, .quotelink, .deadlink, + a, a.visited, .button, .dd-menu li, + .backlink span a.quotelink, + #arc-list .quotelink, s .sjis { + color: #FF7518CF !important; + } + + .middlead a img, + #bannerCnt { + filter: contrast(90%) brightness(90%); + } + + select { + border: 1px solid #FF7518CF; + background-color: #261e37; + color: #FF7518CF; + } + + table.postForm > tbody > tr > td:first-child { + font-weight: bold; + padding: 0 5px; + font-size: 10pt; + background-color: #261e37; + color: #dfbaba; + } + + .dd-menu ul { + background-color: #241023; + } + + input::-moz-placeholder, + textarea::-moz-placeholder, + input::placeholder, + textarea::placeholder { + color: #8f95b3 !important; + opacity: 0.6 !important; + } + + input[type=text]:focus, input[type=password]:focus, input:not([type]):focus, textarea:focus { + border: 1px solid #96b4aa !important; + } + + input[type="checkbox"] { + opacity: 0.7; + } + + input[type=text], input[type=password], + table.postForm > tbody textarea, + textarea + #recaptcha_response_field { + background-color: #261e37; + color: #F0F8FFD1; + } + + .postblock { + background-color: #241023; + border: 1px solid rgba(150, 180, 170, 0.3); + } +} diff --git a/css/catalog_spooky2017.css b/css/catalog_spooky2017.css new file mode 100644 index 0000000..af847aa --- /dev/null +++ b/css/catalog_spooky2017.css @@ -0,0 +1,1894 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, textarea, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; +} + +.nwsb { display: none; } + +a { + text-decoration: none; +} + +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.yotsuba_new .click-me { + color: #800000; + background-color: #F0E0D6; + border: 2px solid #D9BFB7; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.yotsuba_new .click-me:before { + border-color: #D9BFB7 transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.yotsuba_new .click-me:after { + border-color: #F0E0D6 transparent; + border-style: solid; +} + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #f0e0d6; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #d9bfb7; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input, +#qr-painter-ctrl input { + width: 30px !important; + text-align: center; +} + +#bottomnav { + clear: both; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #F0E0D6; + overflow: hidden; + border-bottom: 2px solid #D9BFB7; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; + font-size: 12px; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; + color: #000080 !important; +} + +/* reCaptcha */ +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +input[type="text"], input[type="password"], textarea { + border-color: #140814 !important; +} + +input[type="text"]:hover, input[type="text"]:focus, +textarea:hover, textarea:focus { border-color: 1px solid rgba(150, 180, 170, 0.3) !important } + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +#absbot a, +.button { + color: #d98518; + text-shadow: 0px 0px 8px rgba(217,133,24,1); +} + +a:hover, +#absbot a:hover, +.button:hover { + color: #ff9d1c; + transition-duration: 1s; + transition-property: color; +} + +hr { + clear: both; + height: 1px; + border: 0; + border-bottom: 1px solid #96b4aa; + opacity: 0.1; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; + color: #000; +} + +p { + margin-bottom: 15px; +} + +input[type="text"], +input[type="password"], +textarea { + border: 1px solid #AAA; + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +input[type=text], +textarea { + outline: none; +} + +input[type="text"], +input[type="password"] { + margin: 0px 2px; + padding: 1px; +} + +input:focus, +textarea:focus { + border: 1px solid #EEAA88 !important; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +/* Clickables */ +.button { + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +.clickbox { + margin-right: 3px; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: 1px solid #aaa; + text-decoration: none; + color: #000; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 100002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 100003; + font-size: 14px; + box-shadow: 0 0 6px rgba(0,0,0,1); + background-color: #241023; +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid rgba(150, 180, 170, 0.1); + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; + filter: invert(50%); +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #FFFFEE; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: red; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +#boardNavDesktop { + color: #140814; + font-size: 9pt; +} + +#boardNavDesktop a { + padding: 1px; + text-decoration: none; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: red; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #880000; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #140814; + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #800000; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + color:#96b4aa; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 100004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + margin-bottom: 20px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 180px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 11px; + line-height: 8px; + margin-top: 2px; + margin-bottom: 1px; +} + +.meta .postMenuBtn { + position: absolute; + margin-top: -1px; + display: none; +} + +.thread:hover .meta .postMenuBtn { display: inline-block; } + +.teaser { + display: none; + padding: 0 15px; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +#absbot a { + text-decoration: underline; +} + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + background-color: #F0E0D6; + border-color: #800000 #D9BFB7 #D9BFB7 #800000; + border-image: none; + border-right: 1px solid #D9BFB7; + border-style: none solid solid none; + border-width: medium 1px 1px medium; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + overflow: auto; + display: block; + max-height: 100vh; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; + border-top: 1px solid #d9bfb7; +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/***************************************************************/ + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + color: #800000; + background-color: #F0E0D6; + border: 2px solid #D9BFB7; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #D9BFB7 transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #F0E0D6 transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: #181f24; + border-radius: 3px; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: #dedede; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + color: #00A550; + font-weight: bold; +} + +.post-subject { + color: #CC1105; + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +.verified-capcode { + color: #007FFF; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #EA8; + color: #800; + font-weight: bold; + border: 1px solid #800; + padding: 0 5px; + + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +#postForm input[type="text"], +#postForm input[type="password"], +table.postForm > tbody textarea, +#recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #EA8; + color: #800; + font-weight: bold; + padding: 0 5px; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + text-align: center; + width: 50px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 20px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; + border: 1px solid; + border-color: #333; + border-bottom: 0; + border-right-width: 1px; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #333; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#customMenu { + width: 400px; + left: 50%; + margin-left: -200px; + padding: 2px; +} +#customMenu .reply { border: 0 } +#customMenuBox { + margin: 0px auto 5px; + width: 385px; + padding: 2px 4px 3px; + font-size: 10pt; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } +.boardSelect .custom-menu-ctrl, +.boardSelect .customBoardList { margin-left: 10px; } +.boardSelect .custom-menu-ctrl { display: none; } +.boardSelect:hover .custom-menu-ctrl { display: inline; } +.persistentNav .boardList a, +.persistentNav .customBoardList a, +#boardNavMobile .boardSelect a { text-decoration: none; } + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; + color: #140814; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + color: #140814; + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} + +#postForm tr > td > .desktop, +#ctrl, +.navLinks, +.postNum.desktop > span { color: #140814 } +.open-qr-wrap { color: #140814 !important } +.navLinks.desktop > * { color: #96b4aa } +td { border-color: #140814 !important } +.ctrl-wrap { color: #96b4aa; } + +@media (max-width: 480px) { + .ws .dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-right-width: 2px; + } + .ws .dd-menu li { + border-bottom: 1px solid #B7C5D9; + } + .ws .dd-menu li:hover { + background-color: #EEF2FF; + } + .nws .dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-right-width: 2px; + } + .nws .dd-menu li { + border-bottom: 1px solid #D9BFB7; + } + .nws .dd-menu li:hover { + background-color: #FFFFEE; + } + + body.ws { + background: url('/image/fade-blue.png') repeat-x scroll center top #EEF2FF; + color: #000; + } + + body.nws { + background: url('/image/fade.png') repeat-x scroll center top #FFFFEE; + color: #800000; + } +} + +@media (min-width: 481px) { + body { + background: #140814; + color: #96b4aa; + } + + .ws div.boardBanner { + color: #5995b3; + text-shadow: 0px 0px 5px rgba(89, 149, 179, 0.75); + } + + .nws div.boardBanner { + color: #b35968; + text-shadow: 0px 0px 5px rgba(179, 89, 104, 0.75); + } + + .quoteLink, .quotelink, .deadlink, + a, a.visited, .button, .dd-menu li, + .backlink span a.quotelink, + #arc-list .quotelink, s .sjis { + color: #d98518 !important; + text-shadow: 0px 0px 8px rgba(217, 133, 24, 1); + } + + .flag { + opacity: 0.5; + } + + .middlead a img, + #bannerCnt { + opacity: 0.5; + } + + select { + background-color: #241023; + border: 1px solid rgba(150, 180, 170, 0.3); + color: #96b4aa; + } + + table.postForm > tbody > tr > td:first-child { + color: inherit; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; + background-color: #241023; + border: 1px solid rgba(150, 180, 170, 0.3); + } + + .dd-menu ul { + background-color: #241023; + } + + input::-moz-placeholder, + textarea::-moz-placeholder, + input::placeholder, + textarea::placeholder { + color: #96b4aa !important; + opacity: 0.6 !important; + } + + input[type=text]:focus, input[type=password]:focus, input:not([type]):focus, textarea:focus { + border: 1px solid #96b4aa !important; + } + + input[type="checkbox"] { + opacity: 0.25; + } + + input[type=text], input[type=password], + table.postForm > tbody textarea, + textarea + #recaptcha_response_field { + background-color: #241023; + color: #96b4aa; + border: 1px solid rgba(150,180,170,0.3); + } + + div#absbot a:not(:hover) { + color: #d98518 !important; + text-shadow: 0px 0px 8px rgba(217, 133, 24, 1); + } + + .postblock { + background-color: #241023; + border: 1px solid rgba(150, 180, 170, 0.3); + } +} diff --git a/css/catalog_tomorrow.css b/css/catalog_tomorrow.css new file mode 100644 index 0000000..251b9ad --- /dev/null +++ b/css/catalog_tomorrow.css @@ -0,0 +1,1791 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, textarea, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; +} + +.nwsb { display: none; } + +a { + text-decoration: none; +} + +div.reply { + background-color: #282a2e; + border: 1px solid #282a2e; +} + +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.tomorrow .click-me { + color: #C5C8C6; + background-color: #282A2E; + border: 2px solid #111; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.tomorrow .click-me:before { + border-color: #111 transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.tomorrow .click-me:after { + border-color: #282A2E transparent; + border-style: solid; +} + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #282a2e; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #111; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #282a2e; + overflow: hidden; + border-bottom: 2px solid #111; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; + font-size: 12px; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/* reCaptcha */ +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + background-color: #1D1F21; + color: #C5C8C6; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +div#boardNavDesktopFoot a, +.boardnav a, +.button { + color: #81A2BE; +} + +a:hover, +.boardnav a:hover, +div#boardNavDesktopFoot a:hover, +.button:hover { + color: #5F89AC; +} + +hr { + clear: both; + border: none; + border-top: 1px solid #282A2E; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; + color: #000; +} + +p { + margin-bottom: 15px; +} + +input[type="text"], +input[type="password"], +textarea { + border: 1px solid #AAA; + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +input[type=text], +textarea { + outline: none; +} + +input[type="text"], +input[type="password"] { + margin: 0px 2px; + padding: 1px; +} + +input:focus, +textarea:focus { + border: 1px solid #000 !important; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +/* Clickables */ +.button { + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +.clickbox { + margin-right: 3px; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: 1px solid #aaa; + text-decoration: none; + color: #000; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 100002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 100003; + font-size: 14px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + background-color: #282A2E; +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid #111111; + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.extButton { + cursor: pointer; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/tomorrow/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #282A2E; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: #c5c8c6; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +div#boardNavDesktopFoot, +.boardnav { + color: #C5C8C6; + font-size: 9pt; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +#bottomnav { + clear: both; +} + +div#boardNavDesktopFoot a, +#boardNavDesktop a { + padding: 1px; + text-decoration: none; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: #C5C8C6; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #000; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktopFoot { + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + color: #C5C8C6; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 100004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + margin-bottom: 20px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 180px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 11px; + line-height: 8px; + margin-top: 2px; + margin-bottom: 1px; +} + +.meta .postMenuBtn { + position: absolute; + margin-top: -1px; + display: none; +} + +.thread:hover .meta .postMenuBtn { display: inline-block; } + +.teaser { + display: none; + padding: 0 15px; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +#absbot a { + text-decoration: underline !important; +} + +/***************************************************************/ + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + border: 1px solid #282A2E; + background-color: #282A2E; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + overflow: auto; + display: block; + max-height: 100vh; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; + border-top: 1px solid #111; +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/***************************************************************/ + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + color: #C5C8C6; + background-color: #282A2E; + border: 2px solid #111; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #111 transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #282A2E transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: #000; + border-radius: 3px; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: #dedede; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + color: #00A550; + font-weight: bold; +} + +.post-subject { + color: #B294BB; + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +.verified-capcode { + color: #007FFF; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #282a2e; + color: #c5c8c6; + font-weight: bold; + border: 1px solid #111; + padding: 0 5px; + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +#postForm input[type="text"], +#postForm input[type="password"], +table.postForm > tbody textarea, +#recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + border: 1px solid #AAA; + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + color: #c5c8c6; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + text-align: center; + width: 50px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 20px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; + opacity: 0.5; +} + +.postMenuBtn:hover{ + opacity: 1; +} + +.postMenuBtn { + color: #5F89AC; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #282A2E; + border: 1px solid #000; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #000; +} +.dd-menu li:hover { + background-color: #1D1F21; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#customMenu { + width: 400px; + left: 50%; + margin-left: -200px; + padding: 2px; +} +#customMenu .reply { border: 0 } +#customMenuBox { + margin: 0px auto 5px; + width: 385px; + padding: 2px 4px 3px; + font-size: 10pt; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl, +.boardSelect .customBoardList { margin-left: 10px; } +.boardSelect .custom-menu-ctrl { display: none; } +.boardSelect:hover .custom-menu-ctrl { display: inline; } +.persistentNav .boardList a, +.persistentNav .customBoardList a, +#boardNavMobile .boardSelect a { text-decoration: none; } + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} diff --git a/css/catalog_yotsuba_b_new.css b/css/catalog_yotsuba_b_new.css new file mode 100644 index 0000000..966c363 --- /dev/null +++ b/css/catalog_yotsuba_b_new.css @@ -0,0 +1,1816 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, textarea, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; +} + +.nwsb { display: none; } + +a { + text-decoration: none; +} + +.aplnk { + text-decoration: underline; +} + +div.reply { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-left: none; + border-top: none; +} + +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.yotsuba_b_new .click-me { + color: #000; + background-color: #D6DAF0; + border: 2px solid #B7C5D9; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.yotsuba_b_new .click-me:before { + border-color: #B7C5D9 transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.yotsuba_b_new .click-me:after { + border-color: #D6DAF0 transparent; + border-style: solid; +} + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer a { + color: #0000ff; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #d6daf0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #b7c5d9; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #D6DAF0; + overflow: hidden; + border-bottom: 2px solid #B7C5D9; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; + font-size: 12px; +} + +#bottomnav { + clear: both; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div.boardSelect { + float: left; + color: #000; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/* reCaptcha */ +#recaptcha_table { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + background: url('/image/fade-blue.png') repeat-x scroll center top #EEF2FF; + color: #000; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +.button { + color: #34345C; +} + +a:hover, +#absbot a:hover, +.button:hover { + color: #DD0000; +} + +hr { + clear: both; + border: none; + border-top: 1px solid #B7C5D9; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; +} + +p { + margin-bottom: 15px; +} + +input[type="text"], +input[type="password"], +textarea { + border: 1px solid #AAA; + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +input[type=text], +textarea { + outline: none; +} + +input[type="text"], +input[type="password"] { + margin: 0px 2px; + padding: 1px; +} + +input:focus, +textarea:focus { + border: 1px solid #9988EE !important; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +/* Clickables */ +.button { + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +.clickbox { + margin-right: 3px; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: 1px solid #aaa; + text-decoration: none; + color: #000; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 100002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 100003; + font-size: 14px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + background-color: #D6DAF0; +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid #B7C5D9; + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.extButton { + cursor: pointer; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/burichan/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #EEF2FF; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: red; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +.boardnav { + color: #8899AA; + font-size: 9pt; +} + +.pageJump a { + color: #34345C; + padding-right: 5px; + text-decoration: none; +} + +#boardNavDesktop a { + color: #34345C; + padding: 1px; + text-decoration: none; +} + +#boardNavDesktopFoot a:hover, +#boardNavDesktop a:hover { + color: #DD0000; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: red; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + color: #AF0A0F; + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #34345C; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #8899AA; + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #34345C; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + color: #000; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 100004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + margin-bottom: 20px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 180px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 11px; + line-height: 8px; + margin-top: 2px; + margin-bottom: 1px; +} + +.meta .postMenuBtn { + position: absolute; + margin-top: -1px; + display: none; +} + +.thread:hover .meta .postMenuBtn { display: inline-block; } + +.teaser { + display: none; + padding: 0 15px; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +#absbot a { + text-decoration: underline; +} + +/***************************************************************/ + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-left: none; + border-top: none; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + overflow: auto; + display: block; + max-height: 100vh; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; + border-top: 1px solid #b7c5d9; +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + color: #000; + background-color: #D6DAF0; + border: 2px solid #B7C5D9; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #B7C5D9 transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #D6DAF0 transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: #181f24; + border-radius: 3px; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: #dedede; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + color: #00A550; + font-weight: bold; +} + +.post-subject { + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +.verified-capcode { + color: #007FFF; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #98E; + color: #000; + font-weight: bold; + border: 1px solid #000; + padding: 0 5px; + + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +#postForm input[type="text"], +#postForm input[type="password"], +table.postForm > tbody textarea, +#recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + border: 1px solid #AAA; + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #98E; + color: #000; + font-weight: bold; + border: 1px solid #000; + padding: 0 5px; + font-size: 10pt; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + text-align: center; + width: 50px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 20px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; + opacity: 0.5; +} + +.postMenuBtn:hover{ + opacity: 1; +} + +.postMenuBtn { + color: #000080; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 13px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #B7C5D9; +} +.dd-menu li:hover { + background-color: #EEF2FF; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#customMenu { + width: 400px; + left: 50%; + margin-left: -200px; + padding: 2px; +} +#customMenu .reply { border: 0 } +#customMenuBox { + margin: 0px auto 5px; + width: 385px; + padding: 2px 4px 3px; + font-size: 10pt; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } +.boardSelect .custom-menu-ctrl, +.boardSelect .customBoardList { margin-left: 10px; } +.boardSelect .custom-menu-ctrl { display: none; } +.boardSelect:hover .custom-menu-ctrl { display: inline; } +.persistentNav .boardList a, +.persistentNav .customBoardList a, +#boardNavMobile .boardSelect a { text-decoration: none; } + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} \ No newline at end of file diff --git a/css/catalog_yotsuba_new.css b/css/catalog_yotsuba_new.css new file mode 100644 index 0000000..34e6dc0 --- /dev/null +++ b/css/catalog_yotsuba_new.css @@ -0,0 +1,1804 @@ +/* YUI Reset */ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + color: #000; +} + +body, div, dl, dt, dd, ul, ol, li, pre, code, form, fieldset, legend, textarea, p, blockquote, th, td { + margin: 0; + padding: 0; +} + +body { + margin-bottom: 8px; +} + +.nwsb { display: none; } + +a { + text-decoration: none; +} + +div.reply { + background-color: #f0e0d6; + border: 1px solid #D9BFB7; + border-left: none; + border-top: none; +} + +.click-me { + border-radius: 5px; + margin-top: 5px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + z-index: 2; + white-space: nowrap; +} +.yotsuba_new .click-me { + color: #800000; + background-color: #F0E0D6; + border: 2px solid #D9BFB7; +} +.click-me:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; +} +.yotsuba_new .click-me:before { + border-color: #D9BFB7 transparent; +} +.click-me:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; +} +.yotsuba_new .click-me:after { + border-color: #F0E0D6 transparent; + border-style: solid; +} + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } +} + +.party-cnt { + position: relative; + display: inline-block; +} + +.party-hat { + position: absolute; + pointer-events: none; +} + +.small .party-hat, +.extended-small .party-hat { + width: 100px; + left: -10px; + margin-top: -55px; +} + +.large .party-hat, +.extended-large .party-hat { + left: -15px; + margin-top: -95px; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #f0e0d6; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #d9bfb7; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +body.hasDropDownNav { + margin-top: 45px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#bottomnav { + clear: both; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #F0E0D6; + overflow: hidden; + border-bottom: 2px solid #D9BFB7; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9001; + font-size: 12px; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +.autohide-nav { transition: top 0.2s ease-in-out } + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; + color: #000080 !important; +} + +/* reCaptcha */ +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +#content table { + border-collapse: collapse; + border-spacing: 0; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +fieldset, img { + border: 0; +} + +address, caption, cite, code, dfn, em, strong, th, var { + font-style: normal; + font-weight: normal; +} + +li { + list-style: none; +} + +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +abbr, acronym { + border: 0; + font-variant: normal; +} + +sup { + vertical-align: text-top; +} + +sub { + vertical-align: text-bottom; +} + +legend { + color: #000; +} + +ins { + text-decoration: none; +} + +body { + background: url('/image/fade.png') repeat-x scroll center top #FFFFEE; + color: #800000; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a, +#absbot a, +.button { + color: #0000EE; +} + +a:hover, +#absbot a:hover, +.button:hover { + color: red; +} + +hr { + clear: both; + border: none; + border-top: 1px solid #D9BFB7; +} + +code { + padding: 1px 5px 1px 5px; + background-color: #EEE; + color: #000; +} + +kbd { + background-color: #f7f7f7; + border: 1px solid #ccc; + border-radius: 3px 3px 3px 3px; + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; + font-family: monospace; + font-size: 11px; + line-height: 1.4; + padding: 0 5px; + color: #000; +} + +p { + margin-bottom: 15px; +} + +input[type="text"], +input[type="password"], +textarea { + border: 1px solid #AAA; + font-size: 11px; + margin: 0 2px 0 0; + padding: 2px 4px 3px; +} + +input[type=text], +textarea { + outline: none; +} + +input[type="text"], +input[type="password"] { + margin: 0px 2px; + padding: 1px; +} + +input:focus, +textarea:focus { + border: 1px solid #EEAA88 !important; +} + +#content input[type="checkbox"] { + vertical-align: middle; +} + +#filters input[type="checkbox"] { + display: inline-block; + margin: auto; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +/* Clickables */ +.button { + cursor: pointer; + border: none; + white-space: nowrap; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.btn-wrap:before { + content: '['; +} + +.btn-wrap:after { + content: ']'; +} + +.clickbox { + margin-right: 3px; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: 1px solid #aaa; + text-decoration: none; + color: #000; +} + +/* Input box for custom colors */ +.custom-rgb { + width: 45px; +} + +.abovePostForm { + width: 90%; +} + +/* UI panels */ +.hidden { + display: none; +} + +#backdrop { + background-color: rgba(0, 0, 0, 0.25); + text-align: center; + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 100002; +} + +.panel { + position: absolute; + padding: 2px 5px 5px 5px; + z-index: 100003; + font-size: 14px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + background-color: #F0E0D6; +} + +.panel h4 { + font-size: 14px; + padding: 0; + margin: 10px 0px 5px 0px; +} + +.panelHeader { + border-bottom: 1px solid #D9BFB7; + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; + margin-top: 5px; + padding-bottom: 5px; + text-align: center; + line-height: 14px; +} + +.extButton { + cursor: pointer; +} + +.icon { + width: 18px; + height: 18px; + display: block; + background-size: 100%; + cursor: pointer; + position: absolute; + top: 5px; +} + +.closeIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/cross.png'); + right: 5px; +} + +.helpIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/question.png'); + right: 23px; +} + +.expandIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_plus.png'); + right: 23px; +} + +.collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_minus.png'); + right: 23px; +} + +.watchIcon, +.unwatchIcon { + height: 18px; + position: absolute; + width: 18px; + cursor: pointer; + visibility: hidden; +} + +.watchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_off.png'); +} + +.unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_on.png'); +} + +.thread:hover .watchIcon, +.thread:hover .unwatchIcon { + visibility: visible; +} + +.threadIcons { + display: inline; + height: 16px; + margin: 2px 0 0 -101px; + position: absolute; + width: 100px; + text-align: right; +} + +.threadIcon { + width: 16px; + height: 16px; + display: inline-block; +} + +.stickyIcon { + background-image: url('//s.4cdn.org/image/sticky.gif'); +} + +.closedIcon { + background-image: url('//s.4cdn.org/image/closed.gif'); +} + +.adminIcon { + background-image: url('//s.4cdn.org/image/adminicon.gif'); +} + +.founderIcon { + background-image: url('//s.4cdn.org/image/foundericon.gif'); +} + +.modIcon { + background-image: url('//s.4cdn.org/image/modicon.gif'); +} + +.developerIcon { + background-image: url('//s.4cdn.org/image/developericon.gif'); +} + +.managerIcon { + background-image: url('//s.4cdn.org/image/managericon.gif'); +} + +.imgdel { + padding: 20px 14px; + min-height: 0 !important; + width: 127px; + height: 13px; +} + +.nofile { + padding: 20px 36px; + min-height: 0 !important; +} + +.refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/refresh.png'); +} + +.rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_rotate.gif'); +} + +@media (-webkit-min-device-pixel-ratio: 2.0), + (min--moz-device-pixel-ratio: 2), + (min-resolution: 2dppx) + { + .closeIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/cross@2x.png'); + } + + .helpIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/question@2x.png'); + } + + .expandIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_plus@2x.png'); + background-size: 100%; + } + + .collapseIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_minus@2x.png'); + background-size: 100%; + } + + .stickyIcon { + background-image: url('//s.4cdn.org/image/sticky@2x.gif'); + background-size: 100%; + } + + .closedIcon { + background-image: url('//s.4cdn.org/image/closed@2x.gif'); + background-size: 100%; + } + + .adminIcon { + background-image: url('//s.4cdn.org/image/adminicon@2x.gif'); + background-size: 100%; + } + + .modIcon { + background-image: url('//s.4cdn.org/image/modicon@2x.gif'); + background-size: 100%; + } + + .developerIcon { + background-image: url('//s.4cdn.org/image/developericon@2x.gif'); + background-size: 100%; + } + + .managerIcon { + background-image: url('//s.4cdn.org/image/managericon@2x.gif'); + background-size: 100%; + } + + .refreshIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/refresh@2x.png'); + background-size: 100%; + } + + .rotateIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/post_expand_rotate@2x.gif'); + background-size: 100%; + } + + .watchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_off@2x.png'); + background-size: 100%; + } + + .unwatchIcon { + background-image: url('//s.4cdn.org/image/buttons/futaba/watch_thread_on@2x.png'); + background-size: 100%; + } + +} + +.clickset { + margin-bottom: 8px; +} + +.clickset li { + clear: both; + margin-bottom: 2px; + line-height: 20px; +} + +.inputset { + margin-bottom: 3px; +} + +/* Notifications */ +.msg-error, +.msg-ok { + border-radius: 3px; + padding: 1px 5px 1px 5px; +} + +.msg-ok { + background-color: #FFFFEE; + color: #000; +} + +.msg-error { + background-color: #E62020; + color: #fff; +} + +.error { + color: red; + font-size: x-large; + font-weight: bold +} + +/***************************************************************/ +/* Info bar: update time, order, etc on the left */ +/* Settings bar: buttons on the right */ +#info { + float: left; +} + +#settings { + float: right; + text-align: right; +} + +#filtered-count, +#hidden-count, +#filtered-count-bottom, +#hidden-count-bottom, +#search-term, +#search-term-bottom, +#ordered-by { + font-weight: bold; +} + +#filtered-label, +#hidden-label, +#filtered-label-bottom, +#hidden-label-bottom, +#search-label, +#search-label-bottom { + display: none; +} + +/* Search "Quickfilter" Bar related elements */ +#qf-cnt { + display: none; +} + +#qf-clear { + text-decoration: none; +} + +#qf-box { + width: 75px; + margin-right: 5px; +} + +/* Top and bottom navigation menus */ +#boardNavDesktop { + color: #BB8866; + font-size: 9pt; +} + +#boardNavDesktop a { + color: #800000; + padding: 1px; + text-decoration: none; +} + +#boardNavDesktopFoot a:hover, +#boardNavDesktop a:hover { + color: red; +} + +.navLinks .btn-wrap { + display: inline; +} + +#navtopright, +#navbotright { + float: right; +} + +.navSmall { + font-size: 90%; +} + +#styleSwitcher { + float: right; +} + +#globalMessage { + color: red; + text-align: center; +} + +#toggleMsgBtn { + width: 18px; + height: 18px; + display: block; + cursor: pointer; + top: 5px; + float: left; + margin-bottom: 6px; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #880000; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #B86; + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #800000; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + color: #800; + clear: both; +} + +/***************************************************************/ + +/* Theme editor panel */ +#theme { + width: 330px; + top: 60px; + left: 50%; + margin-left: -178px; +} + +.settings-field { + margin-top: 5px; + width: 100%; +} + +#theme-css { + font-family: 'DejaVu Sans Mono', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; + width: 320px; + min-width: 320px; +} + +#theme-msg { + position: absolute; + margin-top: 4px; +} + +#theme-btns { + margin-top: 5px; + margin-bottom: 3px; +} + +/***************************************************************/ + +/* Filters panel */ +#filters { + width: 380px; + top: 60px; + left: 50%; + margin-left: -190px; +} + +#filters-search { + position: absolute; + left: 4px; + width: 50px; + font-size: 11px; + padding: 0px 2px; +} + +#filters-search:focus { + width: 85px; +} + +#filters .clickbox { + margin: auto; +} + +/* Filters help panel */ +#filters-protip { + width: 570px; + top: 50px; + left: 50%; + margin-left: -295px; + padding-left: 10px; + padding-right: 10px; +} + +#filters-protip h4:before { + content: "»"; + margin-right: 3px; +} + +#filters-protip h4 { + font-size: 15px; +} + +@media all and (max-width: 605px) { + #filters-protip { + left: 0; + margin-left: 0; + } +} + +#filters-protip ul { + margin-bottom: 10px; +} + +#filters-protip li { + margin: 5px 0px; +} + +/* Filters color palette */ +#filter-palette { + position: fixed; + width: 100%; + height: 100%; + z-index: 100004; + top: 0; + left: 0; +} + +#colorpicker { + position: fixed; + padding: 4px; +} + +#colorpicker table td { + padding: 2px; + text-align: center; +} + +#filter-rgb-ok { + float: right; + background-color: transparent; +} + +#filter-table { + width: 100%; +} + +#filter-table th { + font-weight: bold; + font-size: 11px; + min-width: 20px; +} + +#filter-table th, +#filter-table td { + text-align: center; +} + +#filter-table tbody td { + padding: 8px 0px 0px 0px; +} + +#filter-table tfoot td { + padding-top: 20px; +} + +#filter-color-table .clickbox { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; +} + +.filter-pattern { + width: 140px; +} + +.filter-color:hover { + color: #000; +} + +.filter-hits { + font-size: 12px; +} + +.filter-boards { + width: 30px; +} + +#theme-msg, +#filters-msg { + display: none; +} + +/***************************************************************/ + +/* Threads main container */ +#threads { + padding: 20px 0px; + text-align: center; +} + +/* Thread wrapper */ +.thread { + vertical-align: top; + display: inline-block; + word-wrap: break-word; + overflow: hidden; + margin-top: 5px; + margin-bottom: 20px; + padding: 5px 0 3px 0; + position: relative; +} + +.thread a { + border: none; +} + +.thread img { + display: inline; +} + +/* Thread in small mode, teaser off */ +.small .thread { + width: 165px; +} + +/* Thread in large mode, teaser off */ +.large .thread { + width: 270px; +} + +/* Thread in small mode with teaser displayed */ +.extended-small .thread { + width: 180px; + max-height: 320px; +} + +/* Thread in large mode with teaser displayed */ +.extended-large .thread { + width: 270px; + max-height: 410px; +} + +/* "On top" (highlighted) thumbnails */ +.hl { + border-style: solid; + border-width: 3px; +} + +/* Watched (pinned) thumbnails */ +.pinned { + border: 3px dashed #34345C; +} + +.pinned:hover { + border-color: red; +} + +/* Thumbnails */ +.thumb { + display: block; + margin: auto; + z-index: 2; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + min-height: 50px; + min-width: 50px; +} + +/* Replies / images / page indicators */ +.meta { + cursor: help; + font-size: 11px; + line-height: 8px; + margin-top: 2px; + margin-bottom: 1px; +} + +.meta .postMenuBtn { + position: absolute; + margin-top: -1px; + display: none; +} + +.thread:hover .meta .postMenuBtn { display: inline-block; } + +.teaser { + display: none; + padding: 0 15px; +} + +/* Teaser, displayed */ +.extended-small .teaser, +.extended-large .teaser { + display: block; +} + +/* spoilers */ +.teaser s { + background-color: #000; + color: #000; + text-decoration: none; +} + +.teaser s:focus, +.teaser s:hover { + color: #fff; +} + +/***************************************************************/ + +.left { + float: left; +} + +.right { + float: right; +} + +.clear { + clear: both; +} + +.close { + margin-left: 3px; +} + +.center { + text-align: center; + margin: auto; +} + +.mobile { + display: none; +} + +#absbot a { + text-decoration: underline; +} + +/* Thread Watcher */ +#threadWatcher { + max-width: 265px; + display: block; + position: absolute; + background-color: #F0E0D6; + border-color: #800000 #D9BFB7 #D9BFB7 #800000; + border-image: none; + border-right: 1px solid #D9BFB7; + border-style: none solid solid none; + border-width: medium 1px 1px medium; + padding: 3px; +} + +#twHeader { + font-weight: bold; + text-align: center; + height: 17px; + cursor: move; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#twPrune { + position: relative; + top: 0; + margin-left: 3px; + margin-top: -1px; + float: right; +} + +#watchList { + margin: 0; + padding: 0; + overflow: auto; + display: block; + max-height: 100vh; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +#watchList li:first-child { + margin-top: 3px; + padding-top: 2px; + border-top: 1px solid #d9bfb7; +} + +#watchList a { + text-decoration: none; +} + +#watchList li { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hasNewReplies { font-weight: bold; } +.hasYouReplies { font-style: italic; } +.archivelink { opacity: 0.5; } + +.deadlink { + text-decoration: line-through; +} + +.pointer { + cursor: pointer; +} + +/***************************************************************/ + +/* The Click Me */ +#first-run { + border-radius: 5px; + margin-top: 5px; + margin-left: -7px; + padding: 2px 5px; + position: absolute; + font-weight: bold; + color: #800000; + background-color: #F0E0D6; + border: 2px solid #D9BFB7; +} + +#first-run:before { + content: ""; + border-width: 0 6px 6px; + border-style: solid; + left: 50%; + margin-left: -6px; + position: absolute; + width: 0; + height: 0; + top: -6px; + border-color: #D9BFB7 transparent; +} + +#first-run:after { + content: ""; + border-width: 0 4px 4px; + top: -4px; + display: block; + left: 50%; + margin-left: -4px; + position: absolute; + width: 0; + height: 0; + border-color: #F0E0D6 transparent; + border-style: solid; +} + +/* Tooltips */ +#post-preview { + position: absolute; + background-color: #181f24; + border-radius: 3px; + padding: 5px 8px 4px 8px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 500px; + color: #dedede; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +.post-author { + color: #00A550; + font-weight: bold; +} + +.post-subject { + color: #CC1105; + font-weight: bold; +} + +.post-tripcode { + color: #00A550; + font-weight: normal; +} + +.post-teaser { + margin: 3px 0 0 0; + padding: 0; +} + +.post-page { + font-size: 90%; + color: #7d807e; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-top; + margin-left: 3px; +} + +.post-page:before { + content: '('; +} + +.post-page:after { + content: ')'; +} + +.post-last { + color: #bbbfbd; + font-size: 90%; + margin-top: 3px; +} + +.admin-capcode { + color: #FF0000; +} + +.founder-capcode { + color: #117743; +} + +.mod-capcode { + color: #800080; +} + +.developer-capcode { + color: #0000F0; +} + +.manager-capcode { + color: #FF0080; +} + +.verified-capcode { + color: #007FFF; +} + +#enable-mobile { + font-size: small !important; + } + +#disable-mobile { + font-size: small !important; + display: none !important; +} + +/* Post form */ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #EA8; + color: #800; + font-weight: bold; + border: 1px solid #800; + padding: 0 5px; + + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +#postForm input[type="text"], +#postForm input[type="password"], +table.postForm > tbody textarea, +#recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + border: 1px solid #AAA; + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #EA8; + color: #800; + font-weight: bold; + padding: 0 5px; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.password { + font-size: smaller; +} + +input[type="password"] { + text-align: center; + width: 50px; +} + +.rules { + margin-top: 5px; +} + +.rules li { + font-size: 11px; + list-style: none outside none; +} + +.rules li:before { + content: "• "; +} + +#togglePostForm { + text-align: center; + margin-top: 5px; + font-size: 24px; + font-weight: bold; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.text_only #threads table { + width: 80%; + border-collapse: separate; + border-spacing: 2px; +} + +.text_only #threads th { + font-weight: bold; + padding: 4px 2px; + text-align: center; +} + +.text_only #threads td { + padding: 4px 2px; +} + +.text_only #threads .txt-sub { + text-align: left; +} + +.text_only .hl { + border: none; +} + +.text_only .pinned td:first-child { + box-shadow: -1px 0 #000; +} + +.txt-ctrl { width: 25px; } +.txt-no { width: 20px; } +.txt-rep { width: 100px; } +.txt-date { width: 200px; } + +@media only screen and (max-width: 480px) { + .txt-ctrl, + .txt-date { display: none } + .text_only #threads table { + width: 100%; + } +} + +.postMenuBtn { + margin-left: 5px; + text-decoration: none; + line-height: 1em; + display: inline-block; + -webkit-transition: -webkit-transform 0.1s; + -moz-transition: -moz-transform 0.1s; + transition: transform 0.1s; + width: 1em; + height: 1em; + text-align: center; + outline: none; + opacity: 0.5; +} + +.postMenuBtn:hover{ + opacity: 1; +} + +.postMenuBtn { + color: #000080; +} + +.menuOpen { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-right-width: 2px; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #D9BFB7; +} +.dd-menu li:hover { + background-color: #FFFFEE; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#customMenu { + width: 400px; + left: 50%; + margin-left: -200px; + padding: 2px; +} +#customMenu .reply { border: 0 } +#customMenuBox { + margin: 0px auto 5px; + width: 385px; + padding: 2px 4px 3px; + font-size: 10pt; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } +.boardSelect .custom-menu-ctrl, +.boardSelect .customBoardList { margin-left: 10px; } +.boardSelect .custom-menu-ctrl { display: none; } +.boardSelect:hover .custom-menu-ctrl { display: inline; } +.persistentNav .boardList a, +.persistentNav .customBoardList a, +#boardNavMobile .boardSelect a { text-decoration: none; } + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} +#postForm textarea { + width: 292px; +} diff --git a/css/flags.css b/css/flags.css new file mode 100644 index 0000000..8a96da7 --- /dev/null +++ b/css/flags.css @@ -0,0 +1,268 @@ +.flag { + display: inline-block; + width: 16px; + height: 11px; + position: relative; + top: 1px; + background: url('//s.4cdn.org/image/flags.8.png') no-repeat; +} + +.flag-ad { background-position: 0 0 } +.flag-ae { background-position: -16px 0 } +.flag-af { background-position: -32px 0 } +.flag-ag { background-position: -48px 0 } +.flag-ai { background-position: -64px 0 } +.flag-al { background-position: -80px 0 } +.flag-am { background-position: -96px 0 } +.flag-an { background-position: -112px 0 } +.flag-ao { background-position: -128px 0 } +.flag-aq { background-position: -144px 0 } +.flag-ar { background-position: -160px 0 } +.flag-as { background-position: -176px 0 } +.flag-at { background-position: -192px 0 } +.flag-au { background-position: -208px 0 } +.flag-aw { background-position: -224px 0 } +.flag-ax { background-position: -240px 0 } +.flag-az { background-position: 0 -11px } +.flag-ba { background-position: -16px -11px } +.flag-bb { background-position: -32px -11px } +.flag-bd { background-position: -48px -11px } +.flag-be { background-position: -64px -11px } +.flag-bf { background-position: -80px -11px } +.flag-bg { background-position: -96px -11px } +.flag-bh { background-position: -112px -11px } +.flag-bi { background-position: -128px -11px } +.flag-bj { background-position: -144px -11px } +.flag-bl { background-position: -160px -11px } +.flag-bm { background-position: -176px -11px } +.flag-bn { background-position: -192px -11px } +.flag-bo { background-position: -208px -11px } +.flag-bq { background-position: -224px -11px } +.flag-br { background-position: -240px -11px } +.flag-bs { background-position: 0 -22px } +.flag-bt { background-position: -16px -22px } +.flag-bv { background-position: -32px -22px } +.flag-bw { background-position: -48px -22px } +.flag-by { background-position: -64px -22px } +.flag-bz { background-position: -80px -22px } +.flag-ca { background-position: -96px -22px } +.flag-catalonia { background-position: -112px -22px } +.flag-cc { background-position: -128px -22px } +.flag-cd { background-position: -144px -22px } +.flag-cf { background-position: -160px -22px } +.flag-cg { background-position: -176px -22px } +.flag-ch { background-position: -192px -22px } +.flag-ci { background-position: -208px -22px } +.flag-ck { background-position: -224px -22px } +.flag-cl { background-position: -240px -22px } +.flag-cm { background-position: 0 -33px } +.flag-cn { background-position: -16px -33px } +.flag-co { background-position: -32px -33px } +.flag-cr { background-position: -48px -33px } +.flag-cs { background-position: -64px -33px } +.flag-cu { background-position: -80px -33px } +.flag-cv { background-position: -96px -33px } +.flag-cw { background-position: -112px -33px } +.flag-cx { background-position: -128px -33px } +.flag-cy { background-position: -144px -33px } +.flag-cz { background-position: -160px -33px } +.flag-de { background-position: -176px -33px } +.flag-dj { background-position: -192px -33px } +.flag-dk { background-position: -208px -33px } +.flag-dm { background-position: -224px -33px } +.flag-do { background-position: -240px -33px } +.flag-dz { background-position: 0 -44px } +.flag-ec { background-position: -16px -44px } +.flag-ee { background-position: -32px -44px } +.flag-eg { background-position: -48px -44px } +.flag-eh { background-position: -64px -44px } +.flag-xe { background-position: -80px -44px } +.flag-er { background-position: -96px -44px } +.flag-es { background-position: -112px -44px } +.flag-et { background-position: -128px -44px } +.flag-eu { background-position: -144px -44px } +.flag-fam { background-position: -160px -44px } +.flag-fi { background-position: -176px -44px } +.flag-fj { background-position: -192px -44px } +.flag-fk { background-position: -208px -44px } +.flag-fm { background-position: -224px -44px } +.flag-fo { background-position: -240px -44px } +.flag-fr { background-position: 0 -55px } +.flag-ga { background-position: -16px -55px } +.flag-gb { background-position: -32px -55px } +.flag-gd { background-position: -48px -55px } +.flag-ge { background-position: -64px -55px } +.flag-gf { background-position: -80px -55px } +.flag-gg { background-position: -96px -55px } +.flag-gh { background-position: -112px -55px } +.flag-gi { background-position: -128px -55px } +.flag-gl { background-position: -144px -55px } +.flag-gm { background-position: -160px -55px } +.flag-gn { background-position: -176px -55px } +.flag-gp { background-position: -192px -55px } +.flag-gq { background-position: -208px -55px } +.flag-gr { background-position: -224px -55px } +.flag-gs { background-position: -240px -55px } +.flag-gt { background-position: 0 -66px } +.flag-gu { background-position: -16px -66px } +.flag-gw { background-position: -32px -66px } +.flag-gy { background-position: -48px -66px } +.flag-hk { background-position: -64px -66px } +.flag-hm { background-position: -80px -66px } +.flag-hn { background-position: -96px -66px } +.flag-hr { background-position: -112px -66px } +.flag-ht { background-position: -128px -66px } +.flag-hu { background-position: -144px -66px } +.flag-id { background-position: -160px -66px } +.flag-ie { background-position: -176px -66px } +.flag-il { background-position: -192px -66px } +.flag-im { background-position: -208px -66px } +.flag-in { background-position: -224px -66px } +.flag-io { background-position: -240px -66px } +.flag-iq { background-position: 0 -77px } +.flag-ir { background-position: -16px -77px } +.flag-is { background-position: -32px -77px } +.flag-it { background-position: -48px -77px } +.flag-je { background-position: -64px -77px } +.flag-jm { background-position: -80px -77px } +.flag-jo { background-position: -96px -77px } +.flag-jp { background-position: -112px -77px } +.flag-ke { background-position: -128px -77px } +.flag-kg { background-position: -144px -77px } +.flag-kh { background-position: -160px -77px } +.flag-ki { background-position: -176px -77px } +.flag-km { background-position: -192px -77px } +.flag-kn { background-position: -208px -77px } +.flag-kp { background-position: -224px -77px } +.flag-kr { background-position: -240px -77px } +.flag-kw { background-position: 0 -88px } +.flag-ky { background-position: -16px -88px } +.flag-kz { background-position: -32px -88px } +.flag-la { background-position: -48px -88px } +.flag-lb { background-position: -64px -88px } +.flag-lc { background-position: -80px -88px } +.flag-li { background-position: -96px -88px } +.flag-lk { background-position: -112px -88px } +.flag-lr { background-position: -128px -88px } +.flag-ls { background-position: -144px -88px } +.flag-lt { background-position: -160px -88px } +.flag-lu { background-position: -176px -88px } +.flag-lv { background-position: -192px -88px } +.flag-ly { background-position: -208px -88px } +.flag-ma { background-position: -224px -88px } +.flag-mc { background-position: -240px -88px } +.flag-md { background-position: 0 -99px } +.flag-me { background-position: -16px -99px } +.flag-mf { background-position: -32px -99px } +.flag-mg { background-position: -48px -99px } +.flag-mh { background-position: -64px -99px } +.flag-mk { background-position: -80px -99px } +.flag-ml { background-position: -96px -99px } +.flag-mm { background-position: -112px -99px } +.flag-mn { background-position: -128px -99px } +.flag-mo { background-position: -144px -99px } +.flag-mp { background-position: -160px -99px } +.flag-mq { background-position: -176px -99px } +.flag-mr { background-position: -192px -99px } +.flag-ms { background-position: -208px -99px } +.flag-mt { background-position: -224px -99px } +.flag-mu { background-position: -240px -99px } +.flag-mv { background-position: 0 -110px } +.flag-mw { background-position: -16px -110px } +.flag-mx { background-position: -32px -110px } +.flag-my { background-position: -48px -110px } +.flag-mz { background-position: -64px -110px } +.flag-na { background-position: -80px -110px } +.flag-nc { background-position: -96px -110px } +.flag-ne { background-position: -112px -110px } +.flag-nf { background-position: -128px -110px } +.flag-ng { background-position: -144px -110px } +.flag-ni { background-position: -160px -110px } +.flag-nl { background-position: -176px -110px } +.flag-no { background-position: -192px -110px } +.flag-np { background-position: -208px -110px } +.flag-nr { background-position: -224px -110px } +.flag-nu { background-position: -240px -110px } +.flag-nz { background-position: 0 -121px } +.flag-om { background-position: -16px -121px } +.flag-pa { background-position: -32px -121px } +.flag-pe { background-position: -48px -121px } +.flag-pf { background-position: -64px -121px } +.flag-pg { background-position: -80px -121px } +.flag-ph { background-position: -96px -121px } +.flag-pk { background-position: -112px -121px } +.flag-pl { background-position: -128px -121px } +.flag-pm { background-position: -144px -121px } +.flag-pn { background-position: -160px -121px } +.flag-pr { background-position: -176px -121px } +.flag-ps { background-position: -192px -121px } +.flag-pt { background-position: -208px -121px } +.flag-pw { background-position: -224px -121px } +.flag-py { background-position: -240px -121px } +.flag-qa { background-position: 0 -132px } +.flag-re { background-position: -16px -132px } +.flag-ro { background-position: -32px -132px } +.flag-rs { background-position: -48px -132px } +.flag-ru { background-position: -64px -132px } +.flag-rw { background-position: -80px -132px } +.flag-sa { background-position: -96px -132px } +.flag-sb { background-position: -112px -132px } +.flag-sc { background-position: -128px -132px } +.flag-xs { background-position: -144px -132px } +.flag-sd { background-position: -160px -132px } +.flag-se { background-position: -176px -132px } +.flag-sg { background-position: -192px -132px } +.flag-sh { background-position: -208px -132px } +.flag-si { background-position: -224px -132px } +.flag-sj { background-position: -240px -132px } +.flag-sk { background-position: 0 -143px } +.flag-sl { background-position: -16px -143px } +.flag-sm { background-position: -32px -143px } +.flag-sn { background-position: -48px -143px } +.flag-so { background-position: -64px -143px } +.flag-sr { background-position: -80px -143px } +.flag-ss { background-position: -96px -143px } +.flag-st { background-position: -112px -143px } +.flag-sv { background-position: -128px -143px } +.flag-sx { background-position: -144px -143px } +.flag-sy { background-position: -160px -143px } +.flag-sz { background-position: -176px -143px } +.flag-tc { background-position: -192px -143px } +.flag-td { background-position: -208px -143px } +.flag-tf { background-position: -224px -143px } +.flag-tg { background-position: -240px -143px } +.flag-th { background-position: 0 -154px } +.flag-tj { background-position: -16px -154px } +.flag-tk { background-position: -32px -154px } +.flag-tl { background-position: -48px -154px } +.flag-tm { background-position: -64px -154px } +.flag-tn { background-position: -80px -154px } +.flag-to { background-position: -96px -154px } +.flag-tr { background-position: -112px -154px } +.flag-tt { background-position: -128px -154px } +.flag-tv { background-position: -144px -154px } +.flag-tw { background-position: -160px -154px } +.flag-tz { background-position: -176px -154px } +.flag-ua { background-position: -192px -154px } +.flag-ug { background-position: -208px -154px } +.flag-um { background-position: -224px -154px } +.flag-us { background-position: -240px -154px } +.flag-uy { background-position: 0 -165px } +.flag-uz { background-position: -16px -165px } +.flag-va { background-position: -32px -165px } +.flag-vc { background-position: -48px -165px } +.flag-ve { background-position: -64px -165px } +.flag-vg { background-position: -80px -165px } +.flag-vi { background-position: -96px -165px } +.flag-vn { background-position: -112px -165px } +.flag-vu { background-position: -128px -165px } +.flag-xw { background-position: -144px -165px } +.flag-wf { background-position: -160px -165px } +.flag-ws { background-position: -176px -165px } +.flag-xk { background-position: -192px -165px } +.flag-xx { background-position: -208px -165px } +.flag-ye { background-position: -224px -165px } +.flag-yt { background-position: -240px -165px } +.flag-za { background-position: 0 -176px } +.flag-zm { background-position: -16px -176px } +.flag-zw { background-position: -32px -176px } diff --git a/css/futaba.css b/css/futaba.css new file mode 100644 index 0000000..9ff1a7f --- /dev/null +++ b/css/futaba.css @@ -0,0 +1,320 @@ +body { + font-size: 12pt; + background: #FFFFEE; + color: #800000; + padding-left: 5px; + padding-right: 5px; + margin-right: 0px; + margin-left: 0px; + margin-top: 5px; +} + +center iframe { + border: 1px solid black; +} + +a, a:visited { + background: inherit; + color: #0000EE; + font-family: serif; +} + +a:hover { + color: #DD0000; + background: inherit; + font-family: serif; +} + +a.quotelink { + background: inherit; + color: #000080; + font-family: serif; +} + +.logo { + clear: both; + text-align: center; + background: inherit; + font-size: 24pt; + color: #800000; + width: 100%; +} + +.postarea { + background: inherit; +} + +form { + margin-top: 0px; +} + +.rules { + width: 468px; + font-size: 10px; + font-family: sans-serif; +} + +.rules a, .rules a:visited, .rules a:link { + font-family: sans-serif; +} + +.rules li { + margin-left: 1em; + text-indent: -1em; +} + +.postblock { + background: #EEAA88; + color: #800000; + font-weight: 800; +} + +.footer { + text-align: center; + font-size: 12px; + font-family: serif; +} + +.unkfunc { + color: #789922; +} + +.filesize { + font-size: 16px; + font-family: serif; + text-decoration: none; +} + +.filesize span, span.postername, span.filetitle, span.commentpostername { + unicode-bidi: embed; +} + +.filetitle, .replytitle { + background: inherit; + font-size: 18px; + font-family: serif; + color: #CC1105; + font-weight: 800; +} + +.postername, .commentpostername { + background: inherit; + font-size: 16px; + font-family: serif; + color: #117743; + font-weight: 800; +} + +.postertrip { + background: inherit; + font-size: 16px; + font-family: serif; + color: #228854; +} + +.oldpost { + background: inherit; + font-family: serif; + color: #f00000; + font-weight: 800; +} + +.omittedposts, .abbr { + background: inherit; + color: #707070; +} + +.reply { + background: #F0E0D6; + color: #800000; + font-family: serif; +} + +.replyhl { + background: #F0C0B0; + color: #800000; + font-family: serif; +} + +.doubledash { + vertical-align: top; + clear: both; + float: left; +} + +a.quotejs:active, a.quotejs:link, a.quotejs:visited { + color: #800000; + text-decoration: none; +} + +a.quotejs:hover { + font-weight: bold; +} + +.tn_thread { + width: 200px; + height: 100px; + margin: 0px 20px 20px 20px; + float: left; + background: #eed; + border: #ea8 1px solid; + text-align: center; +} + +.tn_reply { + width: 100px; + height: 100px; + margin: 0px 20px 20px 20px; + float: left; + background: #eed; + border: #ea8 1px solid; + text-align: center; +} + +.adHeadline { + font: bold 10pt serif; + text-decoration: underline; + color: #00e +} + +.adText { + font: normal 10pt serif; + text-decoration: none; + color: #800 +} + +#ad { + width: 120px; + + margin: 0; + padding: 0; + + position: absolute; + right: 150px; + + border: 1px solid #EEAA88; + + font-family: arial, helvetica, sans-serif; + font-size: 11px; + +} + +#ad div { + margin: 0; + padding: 0.4em; + +} + +#ad div.ad-title { + padding: 0em; + background: #EEAA88; + color: #800000; + font-size: 11px; +} + +#ad div.ad-title a { + font-family: arial, helvetica, sans-serif; + color: #800000; +} + +#ad div.ad-text { +} + +#ad div.ad-text a { + font-family: arial, helvetica, sans-serif; +} + +#ad div.ad-text a:visited { +} + +.bottomAdTitle { + font-family: arial, helvetica, sans-serif; + background: #EEAA88; + color: #800000; +} + +#bottomAd { + height: 52px; + font-size: 11px; +} + +#bottomAdOuter { + width: 600px; + border: 1px solid #EEAA88; + font-size: 11px; +} + +.spoiler a.quotelink, .spoiler .unkfunc { + color: inherit; +} + +.exif { + display: none; + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td { + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td b { + font-weight: 800; + text-decoration: underline; + font-size: x-small; +} + +#header { + position: absolute; + left: 5px; + right: 5px; +} + +* html #header { + width: 100%; +} + +#navtop, #navbot { + left: 0px; + float: left; + font-size: 11pt; +} + +#navtopr, #navbotr { + right: 0px; + display: block; + text-align: right; + font-size: 11pt; +} + +#footer { + clear: both; +} + +.fstitle { + float: left; + width: 25px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#recaptcha_response_field { + border: 1px solid #AAA !important; +} + +#recaptcha_div { + height: 107px; + width: 442px; +} + +#recaptcha_challenge_field { + width: 400px +} \ No newline at end of file diff --git a/css/futabanew.css b/css/futabanew.css new file mode 100644 index 0000000..3ee5190 --- /dev/null +++ b/css/futabanew.css @@ -0,0 +1,1676 @@ +/** GENERIC / ELEMENT STYLING **/ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #FFE; + + color: #800000; + font-size: 12pt; + font-family: 'Times New Roman', serif; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +.adc-resp-bg { + margin: auto; + width: 728px; + height: 112px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } + + #quickReply { + z-index: 9000 !important; + } + + .adc-resp-bg { + width: 300px; + height: 250px; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.n-atb { + border-radius: 6px; + padding: 1px 4px; + color: #353839; +} + +.atsb2018 table { + margin: auto; + font-size: 90%; +} +.atsb2018 td { + white-space: nowrap; + padding: 0px 4px; +} +.atsb2018 .atsgc { + width: 280px; +} +.atsb2018 .atgg { + height: 20px; +} + +.n-atb-0 { background-color: #F56FA1; } +.n-atb-1 { background-color: #7B3F00; color: #F2F3F4; } +.n-atb-2 { background-color: #FFFDD0; } +.n-atb-3 { background-color: #E4D00A; } +.n-atb-4 { background-color: #50C878; } + +.n-atb-0::after { content: 'Team Peep'; } +.n-atb-1::after { content: 'Team Chocolate'; } +.n-atb-2::after { content: 'Team Creme'; } +.n-atb-3::after { content: 'Team Peanut Butter'; } +.n-atb-4::after { content: 'Team Mini'; } + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #f0e0d6; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #d9bfb7; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 102px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline { + display: none !important; +} + +a, a:visited { + color: #00E; +} + +a.replylink, div#absbot a { + text-decoration: underline !important; +} + +a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #00E !important; +} + +a:hover { + color: red !important; +} + +div.board > hr { + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img { + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +div.container { + margin: 0px !important; + padding: 0px !important; + + display: block; + + /** This fixes annoying margins and makes it a real container **/ + line-height: 0em; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#qr-painter-ctrl .oe-r-cb { + vertical-align: sub; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/** MOBILE ONLY DISABLES **/ +.mobile { + display: none; +} + +/** HEADER **/ +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules > li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-size: 24pt; + font-weight: bold; + + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: 10pt; +} + +div#boardNavDesktop { + + font-size: 11pt; + display: block; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +/** General Containers **/ +div.pContainer { + +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + + float: left; + + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +/** Thread Container **/ +div.thread { + + margin: 0px; + + clear: both; + +} + +/** Post Container **/ +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread > div:nth-of-type(2) > div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #f0e0d6; + + display: table; + + padding: 2px; +} + +div.reply input { + float: none; +} + +/** Post Information **/ +div.post div.postInfo { + display: block; + width: 100%; +} + +.fileText { + max-width: 600px; + white-space: nowrap; +} + +div.post div.postInfo span.postNum { + +} + +div.post div.postInfo span.postNum a { + text-decoration: none; + color: #800000; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: red !important; +} + +/* Name */ +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + color: #117743; + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + color: #117743; + font-weight: normal !important; +} + +/* Date/Time */ +div.post div.postInfo span.date { + +} + +div.post div.postInfo span.time { + +} + +/* Subject */ +div.post div.postInfo span.subject { + color: #cc1105; + font-weight: bold; +} + +/** Message **/ +div.post blockquote.postMessage { + display: block; +} + +blockquote > span.quote { + color: #789922; +} + +.quoteLink, .quotelink, .deadlink { + color: #000080 !important; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-list .quotelink { + color: #34345C !important; +} + +#arc-list .quotelink:hover { + color: #D00 !important; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +/** File Information **/ +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + + margin-bottom: 5px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + + float: left; +} + +/** Summary **/ +span.summary { + margin-top: 10px; +} + +/** POST FORM **/ +div.postingMode { + background-color: #e04000; + padding: 1px; + text-align: center; + + color: #fff; + font-weight: bold; + font-size: larger; + + margin-top: 8px; +} + +div.navLinks { + margin-bottom: 10px; +} + +div.navLinksBot { + margin-bottom: 0px; +} + +#verification table { + border: none !important; + margin: 0px; +} + +/** FOOTER **/ +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; + +} + +div.pagelist { + overflow: hidden; + + border: 1px solid gray; + background: #FFE; + border: 1px solid black; + border-left-color: gray; + border-top-color: gray; + padding: 1px; + + display: inline-block; +} + +div.pagelist > div { + float: left; + margin: 1px; + + border: 1px solid gray; + border-left-color: black; + border-top-color: black; +} + +div.pagelist div.pages, div.pagelist div span { + padding-top: 3px; + padding-bottom: 3px; + display: inline-block; +} + +div.pagelist form { + display: inline; +} + +div.pagelist strong { + color: #800000; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; +} + +div#boardNavDesktopFoot { + + clear: both; + font-size: 11pt; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: 8pt !important; + + padding-bottom: 4px; + padding-top: 10px; + clear: both; + + color: #800000; +} + +#recaptcha_response_field { + padding: 0px; +} + +/** POST FORM **/ +table#postForm { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +#postForm { + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #EA8; + color: #800; + font-weight: bold; + +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +table#postForm td { + margin: 0px; + padding: 0px; +} + +/* table.postForm > tbody input[type=text], table.postForm > tbody textarea { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + outline: none; +} */ + +table.postForm > tbody > tr > td > input[type=text] { + width: 260px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +input[type=password] { + width: 70px; +} + +table.postForm input[type="submit"] { + margin-left: 5px; +} + +.postblock { + background-color: #EA8; + color: #800; + font-weight: bold; + padding: 0 5px; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.reply:target, .reply.highlight { + background: #F0C0B0 !important; +} + +.hand { + cursor: pointer; +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: red; + text-align: center; +} + +.highlightPost:not(.op) { + background: #f0d6d6 !important; +} + +span.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +span.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; +} + +span.capcodeDeveloper span.name, span.capcodeDeveloper span.name a, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +span.capcodeManager span.name, span.capcodeManager span.name a, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +span.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +.omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #707070; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +#navtopright, #navbotright { + float: right; + font-size: 11pt; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #ede4dc; + overflow: hidden; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/** QUICK REPLY **/ +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; + +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; + +} + +div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; + +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail .name, .useremail .postertrip { + color: #0000EE !important; +} + +.useremail:hover * { + color: red !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; + + + margin: 0; + font-size: 14px; +} + +.preview { + background-color: #F0E0D6; + border: 1px solid rgba(0, 0, 0, 0.20); + border-bottom: 2px solid rgba(0, 0, 0, 0.20); + border-right: 2px solid rgba(0, 0, 0, 0.20); +} + +/** this is not important **/ +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing { + margin: 0 auto; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #117743; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #117743; +} + +table.flashListing .subject { + color: #cc1105; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: #ede2d4; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +textarea[name="com"] { + width: 296px; +} + +#recaptcha_response_field { + border: 1px solid #aaa !important; + width: 300px !important; + font-size: 10pt !important; +} + +table.postForm > tbody > tr > td > input[type="text"] { + width: 244px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + background: inherit; + font-family: serif; + color: #f00000; + font-weight: 800; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm { + width: 468px; +} +#postForm textarea { + width: 292px; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 13px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #D9BFB7; +} +.dd-menu li:hover { + background-color: #FFFFEE; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +#g-search-form { + text-align: center; +} +.g-search-ctrl { + vertical-align: top; + margin: 0 3px; +} +#js-sf-qf { + width: 185px; + padding: 2px 4px 3px 4px; +} +#js-sf-bf { + padding: 2px 4px 3px 4px; + width: 125px; +} +#js-sf-status { + text-align: center; + font-size: 24px; +} +.js-sf-err { + color: #C41E3A; +} +.boardBlock { + font-weight: bold; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.ogv-cnt { + position: relative; + display: inline-block; + text-align: center; + min-height: 16px; +} + +.ogv-cnt:not(.ogv-detached)::before { + content: 'Loading…'; + position: absolute; + top: 8px; + left: 0; + font-weight: bold; +} + +.ogv-cnt.ogv-loaded::before { + display: none; +} + +.ogv-cnt > ogvjs { + position: initial !important; +} + +.ogv-ctrl { + position: absolute; + bottom: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.65); + width: 100%; + height: 32px; + gap: 0; + display: none; +} + +.ogv-btn { + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + color: white; +} + +.ogv-btn svg { + vertical-align: middle; +} + +.ogv-btn svg:last-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:first-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:last-child { + display: inline; +} + +.ogv-ts { + font-family: sans-serif; + font-size: 10px; + text-align: center; + line-height: 32px; + width: 70px; + color: white; + overflow: hidden; +} + +.ogv-vol { + width: 50px; + margin: 0; +} + +.ogv-seek { + width: 0; + margin: 0; + flex-grow: 1; +} diff --git a/css/janichan.css b/css/janichan.css new file mode 100644 index 0000000..0fd042a --- /dev/null +++ b/css/janichan.css @@ -0,0 +1,1400 @@ +/*body { + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + background: #FFF2EE url(/image/fade-pink.png) top center repeat-x; + color: #000; + padding-left: 5px; + padding-right: 5px; + margin-right: 0; + margin-left: 0; + margin-top: 5px; +} + +td { + font-size: 10pt; + padding: 0; + margin: 0; +} + +td.reply { + border: 1px solid #d9b9b7; + border-left: none; + border-top: none; + padding: 2px; +} + +blockquote { + font-size: 10pt; +} + +.doubledash { + color: #000; +} + +.inputtext { + margin: 0; + margin-right: 2px; + padding: 1px 4px; + border: 1px solid #aaa; + outline: none; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +input.inputtext { + height: 1.75em; +} + +td>.inputtext { + height: auto; + padding-top: 2px; + padding-bottom: 3px; +} + +.inputtext:focus { + border: 1px solid #E89; +} + +.logo img { + border: 1px solid #000; +} + +.logo b span { + font-family: tahoma; +} + +.logo span { + font-size: 28px; + letter-spacing: -2px; +} + +hr { + border: none; + border-top: 1px solid #d9b9b7; + height: 0; +} + +.postblock { + border: 1px solid #000; + font-size: 10pt; + padding: 0 5px; + line-height: 20px; +} + +.rules { + font-size: 10pt; +} + +.rules a { + text-decoration: none; +} + +form a img { + margin-top: 3px; +} + +iframe,.rotating { + background: #F0DAD6; + border-right: 1px solid #d9b9b7; + border-bottom: 1px solid #d9b9b7; +} + +.pages { + border: none; + background: #F0DAD6; + border-right: 1px solid #d9b9b7; + border-bottom: 1px solid #d9b9b7; +} + +.pages td { + border: none; + padding: 1px 5px; +} + +.pages td { + color: #d9b9b7; +} + +.pages td b { + color: #000; +} + +.deletebuttons { + text-align: right; +} + +.deletebuttons br { + display: none; +} + +a,a:visited { + color: #5C3434; +} + +a:hover { + color: #00D; +} + +a.quotelink { + color: #229978; +} + +.logo { + clear: both; + text-align: center; + font-size: 24pt; + color: #0F0AAF; + width: 100%; +} + +form { + margin-top: 0; +} + +.rules { + width: 468px; + font-size: 10px; +} + +.rules>li { + list-style: none; +} + +.rules>li:before { + content: "\2022 \20"; +} + +.postblock { + background: #E89; + color: #000; + font-weight: 800; +} + +.footer { + text-align: center; + font-size: 12px; +} + +.unkfunc { + color: #229978; +} + +.filesize { + text-decoration: none; +} + +td .filesize { + display: inline; + background: none; +} + +.filesize span,span.postername,span.filetitle,span.commentpostername { + unicode-bidi: embed; +} + +.filetitle,.replytitle { + background: inherit; + color: #5D0C0F; + font-weight: 800; +} + +.postername,.commentpostername { + background: inherit; + color: #437711; + font-weight: 800; +} + +.postertrip { + background: inherit; + color: #548822; +} + +.oldpost { + background: inherit; + color: #5D0C0F; + font-weight: 800; +} + +.omittedposts,.abbr { + color: #070707; +} + +.reply { + background: #F0DAD6; + color: #000; +} + +.replyhl { + background: #D0BAD6; + color: #000; +} + +.doubledash { + vertical-align: top; + clear: both; + float: left; +} + +a.quotejs:active,a.quotejs:link,a.quotejs:visited { + color: #000; + text-decoration: none; +} + +a.quotejs:hover { + color: #d00; +} + +.tn_thread { + width: 200px; + height: 100px; + margin: 0 20px 20px 20px; + float: left; + background: #fff2ee; + border: #98e 1px solid; + text-align: center; +} + +.tn_reply { + width: 100px; + height: 100px; + margin: 0 20px 20px 20px; + float: left; + background: #fff2ee; + border: #98e 1px solid; + text-align: center; +} + +.adHeadline { + font: bold 10pt serif; + text-decoration: underline; + color: #00e; +} + +.adText { + font: normal 10pt serif; + text-decoration: none; + color: #800; +} + +#ad { + width: 120px; + margin: 0; + padding: 0; + position: absolute; + right: 150px; + border: 1px solid #98E; + font-family: arial, helvetica, sans-serif; + font-size: 11px; +} + +#ad div { + margin: 0; + padding: .4em; +} + +#ad div.ad-title { + padding: 0; + background: #98E; + color: #000; + font-size: 11px; +} + +#ad div.ad-title a { + font-family: arial, helvetica, sans-serif; + color: #000; +} + +#ad div.ad-text a { + font-family: arial, helvetica, sans-serif; +} + +.bottomAdTitle { + font-family: arial, helvetica, sans-serif; + background: #98E; + color: #000; +} + +#bottomAd { + height: 52px; + font-size: 11px; +} + +#bottomAdOuter { + width: 600px; + border: 1px solid #98E; + font-size: 11px; +} + +.spoiler a.quotelink,.spoiler .unkfunc { + color: inherit; +} + +.exif { + display: none; + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td { + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td b { + font-weight: 800; + text-decoration: underline; + font-size: x-small; +} + +#header { + position: absolute; + top: 2px; + left: 5px; + right: 5px; +} + +* html #header { + width: 100%; +} + +#navtop,#navbot { + left: 0; + float: left; +} + +#navtopright,#navbotright { + right: 0; + display: block; + float: right; + text-align: right; +} + +#header,#navbot,#navbotright { + font-size: 9pt; + color: #d9b9b7; +} + +#header a,#navbot a,#navbotr a,.pages td a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #5C3434; +} + +#header a:hover,#navbot a:hover,#navbotr a:hover,.pages td a:hover { + color: #00D; +} + +#footer { + clear: both; + padding-top: 10px; +} + +#footer center font { + font-size: 7pt; +} + +.pages td a { + color: #5C3434; +} + +td.replyhl { + border: 1px solid #D0BAD6; + border-left: none; + border-top: none; + padding: 2px; +} + +td.deletebuttons input.checkbox { + margin: 1px 2px 1px 2px; +} + +div.logo img { + margin: 5px 0 5px 0; +} + +.fstitle { + float: left; + width: 25px; +}*/ + +/** GENERIC / ELEMENT STYLING **/ +body { + background: #FFF2EE url(/image/fade-pink.png) top center repeat-x; + + color: #000; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.mobile, .mobileinline { + display: none !important; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +#ctrl-top { + display: none; +} + +a, a:visited { + color: #00E; +} + +a:hover { + color: red; +} + +a.replylink, div#absbot a { + color: #00E !important; + text-decoration: underline !important; +} + +div#absbot { + color: #800000; + clear: both; +} + +div.board > hr { + clear: both; +} + +img { + border: none; +} + +hr { + border: none; + border-top: 1px solid #d9b9b7; + + height: 0; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +/** MOBILE ONLY DISABLES **/ +.mobile { + display: none; +} + +/** HEADER **/ +ul.rules { + margin: 0px; + padding: 0px; + + margin-top: 1px; +} + +ul.rules > li { + list-style: none; + font-size: 10px; +} + +.rules > li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +div.boardBanner > img { + border: 1px solid #34345C; + margin: 5px 0px 5px 0px; + width: 300px; + height: 100px; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + + margin-top: 0px; + + color: #0F0AAF; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktop { + + font-size: 9pt; + color: #B86; + + display: block; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #800000; +} + +/** General Containers **/ +div.pContainer { + +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + color: #e0bfb7; + + float: left; + + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +/** Thread Container **/ +div.thread { + + margin: 0px; + clear: both; + +} + +/** Post Container **/ +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread > div:nth-of-type(2) > div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #F0DAD6; + + border: 1px solid #d9b9b7; + border-left: none; + border-top: none; + + display: table; + + padding: 2px; +} + +div.reply input { + float: none; +} + +/** Post Information **/ +div.post div.postInfo { + display: block; + + width: 100%; +} + +div.post div.postInfo span.postNum { + +} + +div.post div.postInfo span.postNum a { + text-decoration: none; + color: #000; +} + +div.post div.postInfo span.postNum a:hover { + color: d00 !important; +} + +/* Name */ +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + color: #437711; + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + color: #548822; + font-weight: normal !important; +} + +/* Date/Time */ +div.post div.postInfo span.date { + +} + +div.post div.postInfo span.time { + +} + +/* Subject */ +div.post div.postInfo span.subject { + color: #5D0C0F; + font-weight: bold; +} + +/** Message **/ +div.post blockquote.postMessage { + display: block; +} + +blockquote > span.quote { + color: #85ABA1; +} + +a.quoteLink, a.quotelink, .deadlink { + color: #229978 !important; +} + +/** File Information **/ +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + + float: left; + margin-bottom: 5px; +} + +/** Summary **/ +span.summary { + color: #707070; + margin-top: 10px; +} + +/** POST FORM **/ +div.postingMode { + background-color: #e04000; + padding: 1px; + text-align: center; + + color: #fff; + font-weight: bold; + font-size: larger; + + margin-to.p: 8px; +} + +div.navLinks { + margin-bottom: 10px; +} + +div.navLinksBot { + margin-bottom: 0px; +} + +#verification table { + border: none !important; + margin: 0px; +} + +/** FOOTER **/ +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; + +} + +div.pagelist { + font-size: 13px !important; + + margin: 0; + padding: 3px 7px; + + float: left; + + border: none; + background: #F0E0D6; + + border-right: 1px solid #d9b9b7; + border-bottom: 1px solid #d9b9b7; + + list-style: none; + overflow: hidden; + + color: #B86; +} + +div.pagelist > div { + float: left; +} + +div.pagelist > div span { + padding: 4px; + display: inline-block; +} + +div.pagelist div.pages { + padding: 4px; +} + +div.pagelist div.pages a { + text-decoration: none !important; + /* FINE MOOT GOSH JEEZ */ +} + +div.pagelist form { + display: inline; +} + +div.pagelist strong { + color: #800000; +} + +div.deleteform { + float: right; + margin-top: 2px; +} + +input[type=password] { + width: 50px; + text-align: center; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +div.stylechanger { + float: right; + + font-size: 10pt; + clear: both; +} + +div.stylechanger select { + display: inline; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #B86; + + clear: both; + + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #800000; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: xx-small !important; + + padding-bottom: 4px; + + color: #800; +} + +#recaptcha_response_field { + padding: 0px; +} + +/** POST FORM **/ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +#postForm { + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #E89; + color: #000; + font-weight: bold; + border: 1px solid #000; + padding: 0 5px; + + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +table.postForm > tbody input[type=text], input[type=password], table.postForm > tbody textarea { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + border: 1px solid #AAA; + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 232px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #E89; + color: #800; + font-weight: bold; + border: 1px solid #800; + padding: 0 5px; + font-size: 10pt; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + + padding-top: 100px; + padding-bottom: 100px; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + } +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: red; + text-align: center; +} + +.highlightPost { + background: #f0d6d6 !important; + border-color: #d69595 !important; +} + +.reply:target, +.replyContainer .highlight { + background: #F0C0B0 !important; + border: 1px solid #D99F91 !important; + border-left: none !important; + border-top: none !important; + padding: 2px; +} + +span.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; +} + +.omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +table.exif { + min-width: 450px; +} + +table.exif td { + color: #707070; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; + +} + +div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; + +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + + height: auto !important; + width: auto !important; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +#navtopright, #navbotright { + float: right; +} + +.preview { + background: #F0E0D6; + + border: 1px solid #d9b9b7 !important; + border-right: 2px solid #d9b9b7 !important; + border-bottom: 2px solid #d9b9b7 !important; +} + +#settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +div#boardNavMobile { + padding: 5px; + + background-color: #e7dad6; + + overflow: hidden; + + border-bottom: 1px solid #dcc5b8; + + position: fixed; + top: 0px; + left: 0px; + + width: 100%; +} + +div#boardNavMobile select, div#boardNavMobile option { +} + +div.boardSelect { + float: left; +} + +div.pageJump { + float: right; + padding-right: 10px; +} + +/** QUICK REPLY **/ +div.qrWindow { + position: absolute; + z-index: 10000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; + +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail .name, .useremail .postertrip { + color: #0000EE !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +/** this is not important **/ +#captchaContainer { + overflow: hidden; +} + +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; +} + +input:not([type=submit]):focus, textarea:focus { + border: 1px solid #E89 !important; +} + +input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; +} + +/** fix stupid google recaptcha padding in webkit **/ +iframe[src="about:blank"] { + display: none; +} + +textarea[name="com"] { + width: 292px; +} + +input[type=password] { + width: 50px; + text-align: center; +} + +.deadlink { + text-decoration: line-through; +} + +#search-cnt { + display: none; +} + +#search-ok { + text-decoration: none; + line-height: 1; + cursor: pointer; +} + +#search-box { + width: 100px; + font-size: 11px; + height: 12px; + margin-right: 5px; + padding: 0; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +.dd-menu { + position: absolute; + font-size: 13px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #D9BFB7; +} +.dd-menu li:hover { + background-color: #FFFFEE; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} diff --git a/css/md2016.css b/css/md2016.css new file mode 100644 index 0000000..b72069c --- /dev/null +++ b/css/md2016.css @@ -0,0 +1,435 @@ +body { + background: #eee; + color: #000; + font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 20px; + padding: 0; + margin: 0; +} + +.board { + background: #e5e5e5; + border-top: 1px solid #ccc; + margin-bottom: 50px; + padding-bottom: 25px; +} + +.is_index .board { + padding-bottom: 50px; +} + +a, +a:visited, +.quoteLink, +.quotelink, +.deadlink, +div#boardNavMobile .pageJump a, +.persistentNav .pageJump a, +.summary a.replylink, +div.post div.postInfo span.postNum a:visited, +div.post div.postInfo span.postNum a.replylink { + color: rgb(83,109,254) !important; + text-decoration: none !important; +} + +a:hover, +.quoteLink:hover, +.quotelink:hover, +.deadlink:hover, +.summary a.replylink:hover, +.persistentNav .pageJump a:hover, +div#boardNavMobile .pageJump a:hover, +div.post div.postInfo span.postNum a:hover, +.posteruid .hand:hover { + color: rgb(83,109,254) !important; + text-decoration: underline !important; +} + +.postInfo a.postMenuBtn, +.postInfo a.postMenuBtn:hover { + color: rgb(83,109,254) !important; + text-decoration: none !important; +} + +div.postContainer { + display: block; + width: 80%; + margin: 15px auto 0 auto; +} + +.thread { + margin-top: 30px !important; + padding-top: 15px; + border-top: 1px solid #ccc; +} + +.thread:first-child { + padding-top: 0; + border-top: none; +} + +.md-plus-btn { + border-radius: 50%; + font-size: 24px; + height: 56px; + margin: auto; + min-width: 56px; + width: 56px; + padding: 0; + overflow: hidden; + box-shadow: 0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24); + position: relative; + line-height: normal; + position: fixed; + right: 25px; + bottom: 25px; +} + +div.boardBanner > div.boardTitle { + margin-top: 40px; + font-size: 36px; + color: #676767; + margin-bottom: 40px; + font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; + font-weight: normal; + letter-spacing: 0; +} + +:-ms-input-placeholder { color: rgba(0,0,0,.35) } +::-webkit-input-placeholder { color: rgba(0,0,0,.35) } +:-moz-placeholder { color: rgba(0,0,0,.35) } +::-moz-placeholder { color: rgba(0,0,0,.35) } + +#togglePostFormLink, +.ad-plea, +#blotter tfoot td, +#ctrl-top, +div.post div.postInfo span.postNum, +div.pagelist div.pages { + color: transparent; +} + +table.postForm > tbody > tr > td:first-child, +.thread-stats { + color: #000; +} + +.postInfo .backlink a.quotelink, +.postInfo .backlink a.quotelink:hover { + color: rgb(83,109,254) !important; +} + +.ws input[type="text"], .nws input[type="text"] { + border: none !important; + border-bottom: 1px solid rgba(0,0,0,.12) !important; + font-size: 14px !important; + font-family: "Helvetica","Arial",sans-serif; + padding: 4px !important; + -webkit-transition: border-bottom-color 0.2s; + transition: border-bottom-color 0.2s; +} + +.ws input[type="text"]:focus, .ws #quickReply input[type="submit"]:focus { + border-bottom: 1px solid rgb(83,109,254) !important; +} + +.nws input[type="text"]:focus, .nws #quickReply input[type="submit"]:focus { + border-bottom: 1px solid rgb(244,67,54) !important; +} + +.tomorrow .extPanel, +.recaptcha_input_area #recaptcha_response_field { + border: 0 !important; +} + +.ws input[type="submit"], .nws input[type="submit"], button { + border: none; + border-radius: 2px; + position: relative; + height: 28px; + min-width: 64px; + display: inline-block; + text-transform: uppercase; + outline: none; + cursor: pointer; + background: rgb(83,109,254); + color: #fff; + font-size: 14px; + font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; + line-height: 28px; + margin-left: 8px; +} + +.nws input[type="submit"], .nws button { + background: rgb(244,67,54) +} + +#qrCaptchaContainerAlt { + margin-bottom: 8px; +} + +#postForm input[type="text"] { + margin: 4px 0; +} + +#postForm td:first-child { + text-align: right; + padding-right: 10px; +} + +.ws textarea, .nws textarea { border: 0 !important } +.ws textarea:focus, .nws textarea:focus { border: 0 !important } + +table#postForm td { + background: transparent; + border: 0; + font-weight: normal; +} + +#toggleMsgBtn { + margin-left: 5px !important; +} + +.rules { + text-align: center; +} + +#qrHeader { + background: #676767 !important; + color: #fff !important; +} + +#qrHeader a { + color: #fff !important; +} + +#search-box { + height: inherit; + line-height: inherit; + margin: 0; + padding: inherit; +} + +#ctrl-top { + padding-bottom: 10px; + border-top: 1px solid #ddd; + padding-top: 10px; + text-align: center; +} + +.reply:target, .reply.highlight { + background: #eee !important; + padding: 10px !important; + border: 0 !important; +} + +.navLinks + hr, +.open-qr-wrap, +.board hr, +#bannerCnt, +#ctrl-top + hr, +#ctrl-top > hr { + display: none; +} + +.navLinks { + padding-bottom: 8px; + color: transparent; + width: 80%; + margin: auto; +} + +.navLinks label { + color: rgb(83,109,254); +} + +.navLinks label + span { + color: #000; +} + +.navLinksBot { + margin-top: 30px; + text-align: left; +} + +div#boardNavDesktop, div#boardNavDesktopFoot, div#boardNavMobile { + background: #fff; + padding: 5px 10px; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.08),0 3px 1px -2px rgba(0,0,0,.06),0 1px 5px 0 rgba(0,0,0,.04); + margin-bottom: 25px; + color: transparent; + font-size: 14px; +} + +div#boardNavMobile { + color: #000; + border: 0; +} + +div#boardNavDesktopFoot { + margin-bottom: 0; + margin-top: 25px; + padding-top: 25px; + padding-bottom: 25px; +} + +div#boardNavDesktop a, div#boardNavDesktopFoot a { + margin-left: -3px; + margin-right: -3px; + padding: 0; +} + +.persistentNav .pageJump a { + margin: 0; + padding-right: 5px; +} + +div.pagelist { + background: #fff; + margin-top: 25px; + margin-bottom: 25px; + border: 0; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); + margin-left: 25px; + padding: 8px; + border-radius: 2px; +} + +div#absbot { + background: #fff; + color: #999; +} + +div#absbot #footer-links a { + text-decoration: none !important; + color: rgb(83,109,254) !important; +} + +div#absbot #footer-links a:hover { + text-decoration: underline !important; +} + +.bottomCtrl { display: none } + +.ad-plea-bottom + hr { + display: none; +} + +div#boardNavDesktopFoot { + margin-bottom: 0; +} + +div.post, .extPanel, div.reply, .dd-menu ul { + background: #fff; + border-radius: 2px; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); + border: none; +} + +div.pagelist div.cataloglink { + border: 0; +} + +div.post { + max-width: 75%; + margin: 0; + padding: 10px; +} + +.dd-menu li { + padding: 6px 10px !important; + border: 0 !important; +} + +.dd-menu li:hover { + background-color: #eee !important; +} + +#quote-preview { + border: 0 !important; + padding: 10px !important; +} + +div.op { + display: block; + max-width: none; +} + +.fileText { + color: #999; + font-size: 12px; + margin-top: 2px; +} + +.op .fileText { + margin-top: -5px; + margin-bottom: 5px; +} + +.fileText a { + color: #999 !important; + text-decoration: none; +} + +.posteruid, +.dateTime { + color: #999; +} + +.postInfo input[type="checkbox"] { + display: none; +} + +div.sideArrows { display: none; } + +hr { + border: 0; + border-bottom: 1px solid #ddd; +} + +div.post:after { + display: block; + content: ' '; + clear: both; +} + +div.post div.file .fileThumb img { + object-fit: cover; +} + +div.reply div.file .fileThumb img:not(.expanded-thumb) { + border-radius: 75px; + width: 75px !important; + height: 75px !important; +} + +div.op div.file .fileThumb img:not(.expanded-thumb) { + border-radius: 150px; + width: 150px !important; + height: 150px !important; +} + +.postMessage { + margin-left: 20px; + margin-top: 5px; +} + +#boardNavDesktop::after { + content: ' '; + display: block; + clear: both; +} + +span.summary { + border-radius: 0 0 2px 2px; + background-color: #f5f5f5; + width: 80%; + display: block; + margin: -1px auto 0 auto; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); + border-top: 1px solid rgba(0,0,0,0.1); + text-indent: 10px; + padding: 10px 0; +} diff --git a/css/photon.css b/css/photon.css new file mode 100644 index 0000000..6764d6b --- /dev/null +++ b/css/photon.css @@ -0,0 +1,1706 @@ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #EEEEEE none; + color: #333; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +.adc-resp-bg { + margin: auto; + width: 728px; + height: 112px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } + + #quickReply { + z-index: 9000 !important; + } + + .adc-resp-bg { + width: 300px; + height: 250px; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.n-atb { + border-radius: 6px; + padding: 1px 4px; + color: #353839; +} + +.atsb2018 table { + margin: auto; + font-size: 90%; +} +.atsb2018 td { + white-space: nowrap; + padding: 0px 4px; +} +.atsb2018 .atsgc { + width: 280px; +} +.atsb2018 .atgg { + height: 20px; +} + +.n-atb-0 { background-color: #F56FA1; } +.n-atb-1 { background-color: #7B3F00; color: #F2F3F4; } +.n-atb-2 { background-color: #FFFDD0; } +.n-atb-3 { background-color: #E4D00A; } +.n-atb-4 { background-color: #50C878; } + +.n-atb-0::after { content: 'Team Peep'; } +.n-atb-1::after { content: 'Team Chocolate'; } +.n-atb-2::after { content: 'Team Creme'; } +.n-atb-3::after { content: 'Team Peanut Butter'; } +.n-atb-4::after { content: 'Team Mini'; } + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #ddd; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; +} + +.aboveMidAd { + width: 468px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#qr-painter-ctrl .oe-r-cb { + vertical-align: sub; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline, .mobileib { + display: none !important; +} + +a, a:visited { + color: #FF6600 !important; + text-decoration: none; +} + +a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #FF6600 !important; +} + +a:hover { + color: #FF3300 !important; +} + +div#absbot { + color: #333; + clear: both; +} + +div.board>hr { + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img { + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +hr { + border: none; + border-top: 1px solid #DDD; + height: 0; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +.mobile { + display: none; +} + +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules>li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #000; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner>div.boardTitle { + color: #004A99; + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + margin-top: 0px; +} + +div.boardBanner>div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktop { + font-size: 9pt; + color: #333; + display: block; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #FF6600; +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + color: #333; + float: left; + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +div.thread { + margin: 0px; + clear: both; +} + +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread>div:nth-of-type(2)>div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #DDD; + border: 1px solid #CCC; + display: table; + padding: 2px; +} + +div.reply input { + float: none; +} + +div.post div.postInfo { + display: block; + width: 100%; +} + +.fileText { + max-width: 600px; + white-space: nowrap; +} + +div.post div.postInfo span.postNum { +} + +div.post div.postInfo span.postNum > a { + text-decoration: none; + color: #333333 !important; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: #FF3300 !important; +} + +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + color: #004A99; + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + color: #FF3300; + font-weight: normal !important; +} + +div.post div.postInfo span.date { +} + +div.post div.postInfo span.time { +} + +div.post div.postInfo span.subject { + color: #111; + font-weight: bold; +} + +div.post blockquote.postMessage { + display: block; +} + +blockquote>span.quote { + color: #789922; +} + +.quoteLink, .quotelink, .deadlink { + color: #FF6600 !important; + text-decoration: underline; +} + +a.quoteLink:hover, a.quotelink:hover { + color: #FF3300 !important; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-list .quotelink { + color: #34345C !important; +} + +#arc-list .quotelink:hover { + color: #D00 !important; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + + margin-bottom: 5px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + float: left; +} + +span.summary { + color: #707070; + margin-top: 10px; +} + +div.postingMode { + background-color: #DDD; + padding: 1px; + text-align: center; + color: #333; + font-weight: bold; + font-size: larger; + margin-top: 8px; + border: 1px solid #CCC; +} + +#verification table { + border: none !important; + margin: 0px; +} + +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; +} + +div.pagelist { + font-size: 13px !important; + margin: 0; + padding: 3px 7px; + float: left; + border: none; + background: #DDD; + border: 1px solid #CCC; + list-style: none; + overflow: hidden; + color: #333; +} + +div.pagelist>div { + float: left; +} + +div.pagelist>div span { + padding: 4px; + display: inline-block; +} + +div.pagelist div.pages { + padding: 4px; +} + +div.pagelist div.pages a { + text-decoration: none !important; +} + +div.pagelist form { + display: inline; +} + +div.pagelist div.cataloglink { + border-left: 1px solid #CCC; + padding-left: 12px; + margin-left: 7px; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +input[type=password] { + width: 50px; + text-align: center; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; + font-size: 10pt; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #333; + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #FF6600; +} + +div.homelink { + float: right; +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + color: #333; +} + +#recaptcha_response_field { + padding: 0px; +} + +table { + border-spacing: 1px; + margin-left: auto; + margin-right: auto; +} + +table.postForm>tbody>tr>td:first-child { + background-color: #DDD; + color: #333; + font-weight: bold; + border: 1px solid #CCC; + padding: 0 5px; + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + font-size: 10pt; +} + +input[type=text], table.postForm>tbody textarea, input[type="password"], #recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + border: 1px solid #AAA; + outline: none; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +input[type=text]:focus, input[type=password]:focus, input:not([type]):focus, textarea:focus { + border: 1px solid #ea8 !important; +} + +table.postForm>tbody>tr>td>input[type=text] { + width: 244px; +} + +table.postForm>tbody>tr>td>input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #DDD; + color: #333; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + width: 292px; + } +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: #333; + text-align: center; +} + +.highlightPost:not(.op) { + background: #f0d6d6 !important; + border-color: #d69595 !important; +} + +.reply:target, .reply.highlight { + background: #CCC !important; + border: 1px solid #CCC !important; + padding: 2px; +} + +span.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +span.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #FF6600 !important; +} + +span.capcodeDeveloper span.name, span.capcodeDeveloper span.name a, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +span.capcodeManager span.name, span.capcodeManager span.name a, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +span.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +.hand { + cursor: pointer; +} + +#reportTypes a, +.useremail { + text-decoration: underline; +} + +.omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #707070; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; +} + +div.posthover { + padding: 5px; + padding-left: 10px; + padding-right: 10px; +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + height: auto !important; + width: auto !important; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +#navtopright, #navbotright { + float: right; +} + +.preview { + background: #DDD; + border: 1px solid #CCC !important; + +} + +.qrWindow table.postForm>tbody>tr>td:first-child { + background-color: #EEE; + border-color: #CCC; +} + +#settingsBox { + position: absolute; + right: 10px; + margin-top: 10px; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +.backlinkHr hr { + border-top-color: #CCC; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #DDD; + overflow: hidden; + border-bottom: 2px solid #CCC; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + background: #EEE; + border: 1px solid #CCC; + padding: 2px; + font-size: small; + text-align: center; +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail:not(:hover) .name, .useremail:not(:hover) .postertrip { + color: #FF3300 !important; +} + +.useremail .name:hover, .useremail .postertrip:hover { + color: #FF6600 !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +#captchaContainer>img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; + + margin: 0; +} + +.prettyprint { + background-color: rgba(150, 150, 150, 0.2); + border-color: rgba(130, 130, 130, 0.1); +} + +span.tag { + color: #003369; +} + +span.pun { + color: #61663A; +} + +span.com { + color: #FF6600; +} + +span.str { + color: #7FA61B; +} + +span.kwd { + color: #004A99; +} + +span.typ { + color: #6F4E9C; +} + +span.lit { + color: #FF3300; +} + +span.pln { + color: #333; +} + +/** this is not important **/ +#captchaContainer { + overflow: hidden; +} + +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; + font-size: 9pt; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #004A99; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #004A99; +} + +table.flashListing .subject { + color: #111; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: #888; +} + +input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + color: #333; + font-weight: bold; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +.ad-plea a { + text-decoration: none; +} + +.fileText a { + text-decoration: underline; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm textarea { + width: 292px; +} + +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #DDDDDD; + border: 1px solid #CCCCCC; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #CCCCCC; +} +.dd-menu li:hover { + background-color: #EEEEEE; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +#g-search-form { + text-align: center; +} +.g-search-ctrl { + vertical-align: top; + margin: 0 3px; +} +#js-sf-qf { + width: 185px; + padding: 2px 4px 3px 4px; +} +#js-sf-bf { + padding: 2px 4px 3px 4px; + width: 125px; +} +#js-sf-status { + text-align: center; + font-size: 24px; +} +.js-sf-err { + color: #C41E3A; +} +.boardBlock { + font-weight: bold; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.ogv-cnt { + position: relative; + display: inline-block; + text-align: center; + min-height: 16px; +} + +.ogv-cnt:not(.ogv-detached)::before { + content: 'Loading…'; + position: absolute; + top: 8px; + left: 0; + font-weight: bold; +} + +.ogv-cnt.ogv-loaded::before { + display: none; +} + +.ogv-cnt > ogvjs { + position: initial !important; +} + +.ogv-ctrl { + position: absolute; + bottom: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.65); + width: 100%; + height: 32px; + gap: 0; + display: none; +} + +.ogv-btn { + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + color: white; +} + +.ogv-btn svg { + vertical-align: middle; +} + +.ogv-btn svg:last-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:first-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:last-child { + display: inline; +} + +.ogv-ts { + font-family: sans-serif; + font-size: 10px; + text-align: center; + line-height: 32px; + width: 70px; + color: white; + overflow: hidden; +} + +.ogv-vol { + width: 50px; + margin: 0; +} + +.ogv-seek { + width: 0; + margin: 0; + flex-grow: 1; +} diff --git a/css/spooky.css b/css/spooky.css new file mode 100644 index 0000000..59f3a66 --- /dev/null +++ b/css/spooky.css @@ -0,0 +1,1821 @@ +/** GENERIC / ELEMENT STYLING **/ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #120c20; + color: #F0F8FFD1; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu, +.n-jol { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; +} + +.n-pu { + /* Pumpkin icon by twemoji is licensed under CC BY 4.0 (https://twitter.github.io/twemoji/) */ + background: url('//s.4cdn.org/image/1f383.png'); + -webkit-filter: drop-shadow(0px 0px 6px rgba(217,133,24,1)) brightness(125%); + filter: drop-shadow(0px 0px 6px rgba(217,133,24,1)) brightness(125%); +} + +.n-jol { + background: url('//s.4cdn.org/image/1f383.png'); +} + +.n-jol-1 { + -webkit-filter: drop-shadow(0px 0px 4px rgba(217,133,24,1)); + filter: drop-shadow(0px 0px 4px rgba(217,133,24,1)); +} + +.n-jol-2 { + -webkit-filter: drop-shadow(0px 0px 4px rgba(217,133,24,1)) brightness(115%); + filter: drop-shadow(0px 0px 4px rgba(217,133,24,1)) brightness(115%); +} + +.n-jol-3 { + -webkit-filter: drop-shadow(0px 0px 6px rgba(217,133,24,1)) brightness(120%); + filter: drop-shadow(0px 0px 6px rgba(217,133,24,1)) brightness(120%); +} + +.n-jol-4 { + -webkit-filter: drop-shadow(0px 0px 8px rgba(217,133,24,1)) brightness(130%); + filter: drop-shadow(0px 0px 8px rgba(217,133,24,1)) brightness(130%); +} + +.n-jol-5 { + -webkit-filter: drop-shadow(0px 0px 8px rgba(217,133,24,1)) brightness(150%); + filter: drop-shadow(0px 0px 8px rgba(217,133,24,1)) brightness(150%); +} + +.n-jol-6 { + -webkit-filter: drop-shadow(0px 0px 10px rgba(217,133,24,1)) brightness(175%); + filter: drop-shadow(0px 0px 10px rgba(217,133,24,1)) brightness(175%); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.twtdisc { + display: block !important; +} + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #f0e0d6; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #d9bfb7; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline, .mobileib { + display: none !important; +} + +a, a:visited { + color: #d98518; + text-decoration: none; +} + +div.board > hr { + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +hr { + height: 1px; + border: 0; + border-bottom: 1px solid #96b4aa; + opacity: 0.1; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#qr-painter-ctrl .oe-r-cb { + vertical-align: sub; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/** MOBILE ONLY DISABLES **/ +.mobile { + display: none; +} + +/** HEADER **/ +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules > li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #800; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktop { + color: transparent; + font-size: 9pt; + display: block; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; + text-decoration: none; +} + +/** General Containers **/ +div.pContainer { + +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + color: #707070; + + float: left; + + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +/** Thread Container **/ +div.thread { + + margin: 0px; + clear: both; + +} + +/** Post Container **/ +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread > div:nth-of-type(2) > div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #261e37; + border: none; + display: table; + padding: 2px; +} + +div.reply input { + float: none; +} + +/** Post Information **/ +div.post div.postInfo { + display: block; + width: 100%; +} + +div.post div.postInfo span.postNum { + +} + +div.post div.postInfo span.postNum a { + text-decoration: none; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: red !important; +} + +/* Name */ +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + font-weight: normal !important; +} + +/* Date/Time */ +div.post div.postInfo span.date { + +} + +div.post div.postInfo span.time { + +} + +/* Subject */ +div.post div.postInfo span.subject { + font-weight: bold; +} + +/** Message **/ +div.post blockquote.postMessage { + display: block; +} + +blockquote > span.quote { + color: #b76d5e; +} + +.quoteLink, .quotelink, .deadlink, .pageJump a { + text-decoration: underline; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +/** File Information **/ +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + margin-bottom: 5px; + filter: contrast(90%) brightness(90%); +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + + float: left; +} + +/** Summary **/ +span.summary { + color: #707070; + margin-top: 10px; +} + +/** POST FORM **/ +div.postingMode { + background-color: #e04000; + padding: 1px; + text-align: center; + + color: #fff; + font-weight: bold; + font-size: larger; + + margin-top: 8px; +} + +#verification table { + border: none !important; + margin: 0px; +} + +/** FOOTER **/ +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; + +} + +div.pagelist { + font-size: 13px !important; + + margin: 0; + padding: 3px 7px; + + float: left; + + border: none; + background: #F0E0D6; + + border-right: 1px solid #D9BFB7; + border-bottom: 1px solid #D9BFB7; + + list-style: none; + overflow: hidden; + + color:#96b4aa; +} + +div.pagelist > div { + float: left; +} + +div.pagelist > div span { + padding: 4px; + display: inline-block; +} + +div.pagelist div.pages { + padding: 4px; +} + +div.pagelist div.pages a { + text-decoration: none !important; + /* FINE MOOT GOSH JEEZ */ +} + +div.pagelist form { + display: inline; +} + +div.pagelist div.cataloglink { + border-left: 1px solid #D9BFB7; + padding-left: 12px; + margin-left: 7px; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +input[type=password] { + width: 50px; + text-align: center; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; + font-size: 10pt; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: transparent; + + clear: both; + + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + clear: both; +} + +#recaptcha_response_field { + padding: 0px; +} + +/** POST FORM **/ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + color: inherit; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +input[type="checkbox"] { + opacity: 0.75; +} + +input[type=text], input[type=password], +table.postForm > tbody textarea, +#recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + border: 1px solid rgba(150, 180, 170, 0.3); + outline: none; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + color: inherit; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + width: 292px + } +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: red; + text-align: center; +} + +.highlightPost:not(.op) { + background: #f0d6d6 !important; + border-color: #d69595 !important; +} + +.reply:target, .reply.highlight { + box-shadow: 0 0 4px 1px #e79e4f; + border: none; + padding: 2px; +} + +.hand { + cursor: pointer; +} + +.nameBlock.capcodeAdmin span.name, span.capcodeAdmin a span.name, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +.nameBlock.capcodeMod span.name, span.capcodeMod a span.name, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; +} + +.nameBlock.capcodeDeveloper span.name, span.capcodeDeveloper a span.name, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +.nameBlock.capcodeManager span.name, span.capcodeManager a span.name, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +.nameBlock.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +#reportTypes a, +.useremail { + text-decoration: underline; +} + +.omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #707070; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; + +} + +div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; + +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + + height: auto !important; + width: auto !important; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +#navtopright, #navbotright { + float: right; +} + +.preview { + background: #F0E0D6; + border-color: #707070 !important; + border-width: 1px !important; +} + +#settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #F0E0D6; + overflow: hidden; + border-bottom: 2px solid #D9BFB7; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/** QUICK REPLY **/ +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; + +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail:not(:hover) .name, .useremail:not(:hover) .postertrip { + color: #0000EE !important; +} + +.useremail:hover * { + color: red !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +/** this is not important **/ +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; + + margin: 0; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; + font-size: 9pt; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #117743; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #117743; +} + +table.flashListing .subject { + color: #cc1105; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: #ede2d4; +} + +input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; + border-color: #140814 !important; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + background: inherit; + color: #f00000; + font-weight: 800; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +.ad-plea a { + text-decoration: none; +} + +.fileText a { + text-decoration: underline; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; + color: #140814; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm textarea { + width: 292px; +} + +#postForm tr > td > .desktop, +#ctrl-top, +.navLinks.desktop, +.postNum.desktop > span { color: transparent } +.open-qr-wrap { color: transparent !important } +.navLinks.desktop > * { color: #FF7518CF } +td { border-color: #140814 !important } + +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + color: transparent; + font-size: 22px; + font-weight: bold; + text-align: center; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + border: 1px solid; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; + border-color: #FF7518CF; + border-bottom: 0; + border-right-width: 1px; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #FF7518CF; +} +.dd-menu li:hover { + background-color: transparent; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.post.reply.highlight-anti { + background-color: #1a224d !important; +} + +#quote-preview { + border: 1px solid rgba(150, 180, 170, 0.3) !important; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover, +.backlink span a.quotelink:hover, +a:hover, .button:hover, .dd-menu li:hover, #arc-list .quotelink:hover, s .sjis:hover { + color: #ff9d1c !important; + text-shadow: 0px 0px 12px rgba(217, 133, 24, 1); +} + +.ws div.post div.postInfo span.subject { + color: #5995b3; +} + +.nws div.post div.postInfo span.subject { + color: #b35968; +} + +.flashListing .highlightPost, +table.flashListing tr:nth-of-type(2n+1), +div.pagelist { + background-color: transparent !important; + border: none !important; +} + +div.pagelist div.cataloglink { + border: none; +} + +.panel, +.preview { + background-color: #241023; +} + +#skellington { + position: fixed; + right: 0; + bottom: 0; + pointer-events: none; + -webkit-animation: spookyhover 3s linear; + -moz-animation: spookyhover 3s linear; + animation: spookyhover 3s linear; +} + +.topskel { + transform: rotate(180deg); + bottom: auto; + top: 0px; +} + +div.sideArrows, hr { visibility: hidden; } + +@-webkit-keyframes spookyhover { from { opacity: 0; } to { opacity: 1; } } +@-moz-keyframes spookyhover { from { opacity: 0; } to { opacity: 1; } } +@keyframes spookyhover { from { opacity: 0; } to { opacity: 1; } } + +#image-hover { + -webkit-animation: spookyhover 0.2s ease-in; + -moz-animation: spookyhover 0.2s ease-in; + animation: spookyhover 0.2s ease-in; +} + +@media (max-width: 480px) { + .ws .dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-right-width: 2px; + } + .ws .dd-menu li { + border-bottom: 1px solid #B7C5D9; + } + .ws .dd-menu li:hover { + background-color: #EEF2FF; + } + .nws .dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-right-width: 2px; + } + .nws .dd-menu li { + border-bottom: 1px solid #D9BFB7; + } + .nws .dd-menu li:hover { + background-color: #FFFFEE; + } +} + +@media (min-width: 481px) { + .ws div.boardBanner { + color: #5995b3; + } + + .nws div.boardBanner { + color: #b35968; + } + + .quoteLink, .quotelink, .deadlink, + a, a.visited, .button, .dd-menu li, + .backlink span a.quotelink, + #arc-list .quotelink, s .sjis { + color: #FF7518CF !important; + } + + .middlead a img, + #bannerCnt { + filter: contrast(90%) brightness(90%); + } + + select { + border: 1px solid #FF7518CF; + background-color: #261e37; + color: #FF7518CF; + } + + #qrForm textarea, + .extPanel input[type="text"], + .extPanel textarea { + background-color: #261e37 !important; + } + + .extPanel { + box-shadow: 0 0 6px #000; + } + + table.postForm > tbody > tr > td:first-child { + color: inherit; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; + background-color: #261e37; + border: 1px solid rgba(150, 180, 170, 0.3); + } + + .dd-menu ul { + background-color: #261e37; + } + + input::-moz-placeholder, + textarea::-moz-placeholder, + input::placeholder, + textarea::placeholder, + #quickReply #qrForm input::-moz-placeholder, + #quickReply #qrForm input::placeholder, + #quickReply #qrForm textarea::-moz-placeholder, + #quickReply #qrForm textarea::placeholder { + color: #8f95b3 !important; + opacity: 0.6 !important; + } + + input[type=text]:focus, input[type=password]:focus, input:not([type]):focus, + textarea:focus, .extPanel #qrForm textarea:focus { + border: 1px solid #dfbaba !important; + } + + #quickReply textarea { + color: #96b4aa; + } + + input[type=text], input[type=password], + table.postForm > tbody textarea, + #recaptcha_response_field { + background-color: #261e37; + color: #F0F8FFD1; + } + + a.replylink:not(:hover) { + /*color: #d98518 !important;*/ + } + + .postblock { + border: 1px solid rgba(150, 180, 170, 0.3); + } +} + +.ogv-cnt { + position: relative; + display: inline-block; + text-align: center; + min-height: 16px; +} + +.ogv-cnt:not(.ogv-detached)::before { + content: 'Loading…'; + position: absolute; + top: 8px; + left: 0; + font-weight: bold; +} + +.ogv-cnt.ogv-loaded::before { + display: none; +} + +.ogv-cnt > ogvjs { + position: initial !important; +} + +.ogv-ctrl { + position: absolute; + bottom: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.65); + width: 100%; + height: 32px; + gap: 0; + display: none; +} + +.ogv-btn { + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + color: white; +} + +.ogv-btn svg { + vertical-align: middle; +} + +.ogv-btn svg:last-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:first-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:last-child { + display: inline; +} + +.ogv-ts { + font-family: sans-serif; + font-size: 10px; + text-align: center; + line-height: 32px; + width: 70px; + color: white; + overflow: hidden; +} + +.ogv-vol { + width: 50px; + margin: 0; +} + +.ogv-seek { + width: 0; + margin: 0; + flex-grow: 1; +} diff --git a/css/spooky2017.css b/css/spooky2017.css new file mode 100644 index 0000000..4813fe0 --- /dev/null +++ b/css/spooky2017.css @@ -0,0 +1,1744 @@ +/** GENERIC / ELEMENT STYLING **/ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #140814; + color: #96b4aa; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu, +.n-jol { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; +} + +.n-pu { + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-jol { + background: url('//s.4cdn.org/image/1f383.png'); +} + +.n-jol-1 { + -webkit-filter: drop-shadow(0px 0px 4px rgba(217,133,24,1)); + filter: drop-shadow(0px 0px 4px rgba(217,133,24,1)); +} + +.n-jol-2 { + -webkit-filter: drop-shadow(0px 0px 4px rgba(217,133,24,1)) brightness(115%); + filter: drop-shadow(0px 0px 4px rgba(217,133,24,1)) brightness(115%); +} + +.n-jol-3 { + -webkit-filter: drop-shadow(0px 0px 6px rgba(217,133,24,1)) brightness(120%); + filter: drop-shadow(0px 0px 6px rgba(217,133,24,1)) brightness(120%); +} + +.n-jol-4 { + -webkit-filter: drop-shadow(0px 0px 8px rgba(217,133,24,1)) brightness(130%); + filter: drop-shadow(0px 0px 8px rgba(217,133,24,1)) brightness(130%); +} + +.n-jol-5 { + -webkit-filter: drop-shadow(0px 0px 8px rgba(217,133,24,1)) brightness(150%); + filter: drop-shadow(0px 0px 8px rgba(217,133,24,1)) brightness(150%); +} + +.n-jol-6 { + -webkit-filter: drop-shadow(0px 0px 10px rgba(217,133,24,1)) brightness(175%); + filter: drop-shadow(0px 0px 10px rgba(217,133,24,1)) brightness(175%); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.twtdisc { + display: block !important; +} + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #f0e0d6; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #d9bfb7; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline, .mobileib { + display: none !important; +} + +a, a:visited { + color: #d98518; + text-decoration: none; +} + +div.board > hr { + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +hr { + height: 1px; + border: 0; + border-bottom: 1px solid #96b4aa; + opacity: 0.1; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input, +#qr-painter-ctrl input { + width: 30px !important; + text-align: center; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/** MOBILE ONLY DISABLES **/ +.mobile { + display: none; +} + +/** HEADER **/ +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules > li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #800; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktop { + color: #140814; + font-size: 9pt; + display: block; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; + text-decoration: none; +} + +/** General Containers **/ +div.pContainer { + +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + color: #707070; + + float: left; + + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +/** Thread Container **/ +div.thread { + + margin: 0px; + clear: both; + +} + +/** Post Container **/ +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread > div:nth-of-type(2) > div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #241023; + border: none; + display: table; + padding: 2px; +} + +div.reply input { + float: none; +} + +/** Post Information **/ +div.post div.postInfo { + display: block; + width: 100%; +} + +div.post div.postInfo span.postNum { + +} + +div.post div.postInfo span.postNum a { + text-decoration: none; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: red !important; +} + +/* Name */ +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + font-weight: normal !important; +} + +/* Date/Time */ +div.post div.postInfo span.date { + +} + +div.post div.postInfo span.time { + +} + +/* Subject */ +div.post div.postInfo span.subject { + font-weight: bold; +} + +/** Message **/ +div.post blockquote.postMessage { + display: block; +} + +blockquote > span.quote { + color: #789922; +} + +.quoteLink, .quotelink, .deadlink, .pageJump a { + text-decoration: underline; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +/** File Information **/ +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + margin-bottom: 5px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + + float: left; +} + +/** Summary **/ +span.summary { + color: #707070; + margin-top: 10px; +} + +/** POST FORM **/ +div.postingMode { + background-color: #e04000; + padding: 1px; + text-align: center; + + color: #fff; + font-weight: bold; + font-size: larger; + + margin-top: 8px; +} + +#verification table { + border: none !important; + margin: 0px; +} + +/** FOOTER **/ +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; + +} + +div.pagelist { + font-size: 13px !important; + + margin: 0; + padding: 3px 7px; + + float: left; + + border: none; + background: #F0E0D6; + + border-right: 1px solid #D9BFB7; + border-bottom: 1px solid #D9BFB7; + + list-style: none; + overflow: hidden; + + color:#96b4aa; +} + +div.pagelist > div { + float: left; +} + +div.pagelist > div span { + padding: 4px; + display: inline-block; +} + +div.pagelist div.pages { + padding: 4px; +} + +div.pagelist div.pages a { + text-decoration: none !important; + /* FINE MOOT GOSH JEEZ */ +} + +div.pagelist form { + display: inline; +} + +div.pagelist div.cataloglink { + border-left: 1px solid #D9BFB7; + padding-left: 12px; + margin-left: 7px; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +input[type=password] { + width: 50px; + text-align: center; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; + font-size: 10pt; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #140814; + + clear: both; + + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + color:#96b4aa; + clear: both; +} + +#recaptcha_response_field { + padding: 0px; +} + +/** POST FORM **/ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + color: inherit; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +input[type="checkbox"] { + opacity: 0.25; +} + +input[type=text], input[type=password], +table.postForm > tbody textarea, +#recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + border: 1px solid rgba(150, 180, 170, 0.3); + outline: none; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + color: inherit; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + width: 292px + } +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: red; + text-align: center; +} + +.highlightPost:not(.op) { + background: #f0d6d6 !important; + border-color: #d69595 !important; +} + +.reply:target, .reply.highlight { + background-color: #331433; + border: none; + padding: 2px; +} + +.hand { + cursor: pointer; +} + +.nameBlock.capcodeAdmin span.name, span.capcodeAdmin a span.name, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +.nameBlock.capcodeMod span.name, span.capcodeMod a span.name, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; +} + +.nameBlock.capcodeDeveloper span.name, span.capcodeDeveloper a span.name, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +.nameBlock.capcodeManager span.name, span.capcodeManager a span.name, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +.nameBlock.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +#reportTypes a, +.useremail { + text-decoration: underline; +} + +.omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #707070; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; + +} + +div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; + +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + + height: auto !important; + width: auto !important; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +#navtopright, #navbotright { + float: right; +} + +.preview { + background: #F0E0D6; + border-color: #707070 !important; + border-width: 1px !important; +} + +#settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #F0E0D6; + overflow: hidden; + border-bottom: 2px solid #D9BFB7; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/** QUICK REPLY **/ +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; + +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail:not(:hover) .name, .useremail:not(:hover) .postertrip { + color: #0000EE !important; +} + +.useremail:hover * { + color: red !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +/** this is not important **/ +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; + + margin: 0; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; + font-size: 9pt; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #117743; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #117743; +} + +table.flashListing .subject { + color: #cc1105; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: #ede2d4; +} + +input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; + border-color: #140814 !important; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + background: inherit; + color: #f00000; + font-weight: 800; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +.ad-plea a { + text-decoration: none; +} + +.fileText a { + text-decoration: underline; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; + color: #140814; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm textarea { + width: 292px; +} + +#postForm tr > td > .desktop, +#ctrl-top, +.navLinks.desktop, +.postNum.desktop > span { color: #140814 } +.open-qr-wrap { color: #140814 !important } +.navLinks.desktop > * { color: #96b4aa } +td { border-color: #140814 !important } + +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + color: #140814; + font-size: 22px; + font-weight: bold; + text-align: center; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + border: 1px solid; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; + border-color: #333; + border-bottom: 0; + border-right-width: 1px; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #333; +} +.dd-menu li:hover { + background-color: transparent; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.post.reply.highlight-anti { + background-color: #3b1839 !important; +} + +#quote-preview { + border: 1px solid rgba(150, 180, 170, 0.3) !important; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover, +.backlink span a.quotelink:hover, +a:hover, .button:hover, .dd-menu li:hover, #arc-list .quotelink:hover, s .sjis:hover { + color: #ff9d1c !important; + transition-duration: 1s !important; + transition-property: color !important; +} + +.ws div.post div.postInfo span.subject { + color: #5995b3; +} + +.nws div.post div.postInfo span.subject { + color: #b35968; +} + +div.post div.postInfo span.nameBlock span.postertrip, +div.post div.postInfo span.nameBlock span.name { + color: #d98518; +} + +.flashListing .highlightPost, +table.flashListing tr:nth-of-type(2n+1), +div.pagelist { + background-color: transparent !important; + border: none !important; +} + +div.pagelist div.cataloglink { + border: none; +} + +.panel, +.preview { + background-color: #241023; +} + +#skellington { + position: fixed; + right: 0; + bottom: 0; + pointer-events: none; + -webkit-animation: spookyhover 3s linear; + -moz-animation: spookyhover 3s linear; + animation: spookyhover 3s linear; +} + +.topskel { + transform: rotate(180deg); + bottom: auto; + top: 0px; +} + +div.sideArrows, hr { visibility: hidden; } + +@-webkit-keyframes spookyhover { from { opacity: 0; } to { opacity: 1; } } +@-moz-keyframes spookyhover { from { opacity: 0; } to { opacity: 1; } } +@keyframes spookyhover { from { opacity: 0; } to { opacity: 1; } } + +#image-hover { + -webkit-animation: spookyhover 0.2s ease-in; + -moz-animation: spookyhover 0.2s ease-in; + animation: spookyhover 0.2s ease-in; +} + +@media (max-width: 480px) { + .ws .dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-right-width: 2px; + } + .ws .dd-menu li { + border-bottom: 1px solid #B7C5D9; + } + .ws .dd-menu li:hover { + background-color: #EEF2FF; + } + .nws .dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-right-width: 2px; + } + .nws .dd-menu li { + border-bottom: 1px solid #D9BFB7; + } + .nws .dd-menu li:hover { + background-color: #FFFFEE; + } +} + +@media (min-width: 481px) { + body { + background: #140814; + color: #96b4aa; + } + + .ws div.boardBanner { + color: #5995b3; + text-shadow: 0px 0px 5px rgba(89, 149, 179, 0.75); + } + + .nws div.boardBanner { + color: #b35968; + text-shadow: 0px 0px 5px rgba(179, 89, 104, 0.75); + } + + .quoteLink, .quotelink, .deadlink, + a, a.visited, .button, .dd-menu li, + .backlink span a.quotelink, + #arc-list .quotelink, s .sjis { + color: #d98518 !important; + text-shadow: 0px 0px 8px rgba(217, 133, 24, 1); + } + + .flag { + opacity: 0.5; + } + + .middlead a img, + #bannerCnt { + opacity: 0.5; + } + + select { + background-color: #241023; + border: 1px solid rgba(150, 180, 170, 0.3); + color: #96b4aa; + } + + #qrForm textarea, + .extPanel input[type="text"], + .extPanel textarea { + background-color: #241023 !important; + border: 1px solid rgba(150, 180, 170, 0.3) !important; + } + + .extPanel { + box-shadow: 0 0 6px #000; + } + + table.postForm > tbody > tr > td:first-child { + color: inherit; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; + background-color: #241023; + border: 1px solid rgba(150, 180, 170, 0.3); + } + + .dd-menu ul { + background-color: #241023; + } + + input::-moz-placeholder, + textarea::-moz-placeholder, + input::placeholder, + textarea::placeholder, + #quickReply #qrForm input::-moz-placeholder, + #quickReply #qrForm input::placeholder, + #quickReply #qrForm textarea::-moz-placeholder, + #quickReply #qrForm textarea::placeholder { + color: #96b4aa !important; + opacity: 0.6 !important; + } + + input[type=text]:focus, input[type=password]:focus, input:not([type]):focus, + textarea:focus, .extPanel #qrForm textarea:focus { + border: 1px solid #96b4aa !important; + } + + #quickReply textarea { + color: #96b4aa; + } + + input[type=text], input[type=password], + table.postForm > tbody textarea, + #recaptcha_response_field { + background-color: #241023; + color: #96b4aa; + } + + a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #d98518 !important; + text-shadow: 0px 0px 8px rgba(217, 133, 24, 1); + } + + .postblock { + background-color: #241023; + border: 1px solid rgba(150, 180, 170, 0.3); + } +} diff --git a/css/tegaki-test.css b/css/tegaki-test.css new file mode 100644 index 0000000..d40013e --- /dev/null +++ b/css/tegaki-test.css @@ -0,0 +1,666 @@ +/*! Contains fonts from Font Awesome (Copyright (C) 2016 by Dave Gandy), Entypo (Copyright (C) 2012 by Daniel Bruce) */ +@font-face { + font-family: 'tegaki'; + src: url('data:application/octet-stream;base64,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') format('woff'); + font-weight: normal; + font-style: normal; +} + +.tegaki-icon:before { + font-family: 'tegaki'; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-align: center; + font-variant: normal; + text-transform: none; + line-height: 1em; +} + +.tegaki-cancel:before { content: '\e800'; } /* '' */ +.tegaki-plus:before { content: '\e801'; } /* '' */ +.tegaki-minus:before { content: '\e802'; } /* '' */ +.tegaki-pen:before { content: '\e803'; } /* '' */ +.tegaki-down-open:before { content: '\e804'; } /* '' */ +.tegaki-up-open:before { content: '\e805'; } /* '' */ +.tegaki-level-down:before { content: '\e806'; } /* '' */ +.tegaki-pencil:before { content: '\e807'; } /* '' */ +.tegaki-play:before { content: '\e808'; } /* '' */ +.tegaki-bucket:before { content: '\e809'; } /* '' */ +.tegaki-pause:before { content: '\e80a'; } /* '' */ +.tegaki-blur:before { content: '\e80b'; } /* '' */ +.tegaki-to-start:before { content: '\e80c'; } /* '' */ +.tegaki-watercolor:before { content: '\e80d'; } /* '' */ +.tegaki-tone:before { content: '\e80e'; } /* '' */ +.tegaki-airbrush:before { content: '\e80f'; } /* '' */ +.tegaki-fast-fw:before { content: '\e810'; } /* '' */ +.tegaki-fast-bw:before { content: '\e811'; } /* '' */ +.tegaki-left-open:before { content: '\e812'; } /* '' */ +.tegaki-right-open:before { content: '\e813'; } /* '' */ +.tegaki-eraser:before { content: '\f12d'; } /* '' */ +.tegaki-pipette:before { content: '\f1fb'; } /* '' */ + +.tegaki-disabled, +.tegaki-disabled::after, +.tegaki-disabled::before { + opacity: 0.35; +} + +.tegaki-hidden { + display: none !important; +} + +.tegaki-invis { + visibility: hidden !important; +} + +.tegaki-replay-mode #tegaki-tools-cnt, +.tegaki-replay-mode #tegaki-toolmode-bar, +.tegaki-replay-mode .tegaki-ctrlgrp, +.tegaki-replay-mode .tegaki-layers-cell, +.tegaki-replay-mode #tegaki-layers-ctrl { + pointer-events: none; +} + +.tegaki-replay-mode #tegaki-ctrlgrp-zoom, +.tegaki-replay-mode #tegaki-ctrlgrp-layers { + pointer-events: auto; +} + +#tegaki { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: #a3b1bf; + color: #222; + font-family: arial, sans-serif; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + z-index: 9999; + display: grid; + grid-template-columns: 40px 1fr 160px; + grid-template-rows: 24px 1fr 18px; + grid-gap: 2px; +} + +#tegaki input { + color: inherit; +} + +#tegaki > div { + background-color: #8d99a6; +} + +#tegaki-menu-cnt { + grid-area: 1 / 1 / 2 / 4; + white-space: nowrap; + overflow: hidden; + display: flex; +} + +#tegaki-tools-cnt { + grid-area: 2 / 1 / 4 / 2; + padding: 4px; +} + +#tegaki-canvas-cnt { + grid-area: 2 / 2 / 3 / 3; + overflow: auto; + display: flex; + touch-action: none; +} + +#tegaki-ctrl-cnt { + grid-area: 2 / 3 / 4 / 4; + padding: 6px; + overflow: hidden auto; +} + +#tegaki-status-cnt { + grid-area: 3 / 2 / 4 / 3; + line-height: 18px; + display: flex; +} + +#tegaki-status-cnt > div { + padding: 0 4px; +} + +#tegaki-status-replay { + color: #a61930; +} + +#tegaki-status-output { + font-size: 11px; + font-weight: bold; +} + +#tegaki-status-version { + color: #a3b1bf; + font-size: 11px; + margin-left: auto; +} + +#tegaki-menu-bar { + font-size: 12px; + padding-left: 4px; + padding-right: 18px; + border-right: 2px solid #a3b1bf; +} + +.tegaki-replay-mode #tegaki-menu-bar { + padding-right: 4px; +} + +.tegaki-menu-lbl { + margin: 0 2px; + vertical-align: middle; +} + +#tegaki-replay-controls { + padding-right: 10px; + padding-left: 10px; + border-right: 2px solid #a3b1bf; + font-size: 11px; +} + +#tegaki-replay-timeline { + display: inline-block; + width: 100px; + height: 24px; + margin: 0 4px; + border-left: 1px solid #a3b1bf; + border-right: 1px solid #a3b1bf; + /*background-color: rgba(0, 0, 0, 0.25);*/ +} + +#tegaki-replay-timeline-fill { + display: inline-block; + width: 36px; + height: 100%; + background-color: #a3b1bf; +} + +#tegaki-replay-controls > span { + vertical-align: middle; +} + +#tegaki-replay-controls .tegaki-ui-cb-w { + margin-right: 4px; +} + +#tegaki-replay-speed-lbl { + width: 24px; + display: inline-block; + text-align: center; +} + +#tegaki-replay-speed-lbl::before { + content: '×'; +} + +#tegaki-replay-now-lbl, +#tegaki-replay-end-lbl { + display: inline-block; + max-width: 50px; + min-width: 30px; + overflow: hidden; + text-align: center; + margin: 0 4px; +} + +#tegaki-toolmode-bar { + font-size: 11px; + margin-left: 4px; + line-height: 24px; +} + +.tegaki-toolmode-lbl { + margin-right: 6px; +} + +.tegaki-toolmode-lbl::after { + content: ':'; +} + +.tegaki-toolmode-grp { + border-left: 1px solid #a3b1bf; + padding: 0 18px; +} + +#tegaki canvas { + image-rendering: optimizeSpeed; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-optimize-contrast; + image-rendering: pixelated; + -ms-interpolation-mode: nearest-neighbor; +} + +#tegaki .tegaki-smooth-layers #tegaki-cursor-layer { + image-rendering: auto; +} + +.tegaki-tool-active { + color: #f2f3f4; +} + +.tegaki-tool-btn { + width: 32px; + height: 32px; + display: block; + margin: auto; +} + +.tegaki-tool-btn:hover { + background-color: rgba(0, 0, 0, 0.15); +} + +.tegaki-tool-btn:before { + font-size: 20px; + width: 32px; + height: 32px; + line-height: 32px; +} + +.tegaki-mb-btn { + cursor: default; + text-decoration: none; + display: inline-block; + padding: 0 6px; + word-spacing: -1px; + position: relative; + line-height: 24px; + height: 24px; +} + +.tegaki-mb-btn:hover:not(.tegaki-disabled), +.tegaki-ui-btn:hover:not(.tegaki-disabled) { + background-color: rgba(0, 0, 0, 0.10); +} + +.tegaki-sw-btn { + display: inline; + padding: 2px 6px; + margin: 0 2px; + box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.15), -1px -1px 0 rgba(255, 255, 255, 0.15); +} + +.tegaki-sw-btn:hover:not(.tegaki-sw-btn-a) { + background-color: rgba(0, 0, 0, 0.05); +} + +.tegaki-sw-btn-a { + background-color: rgba(0, 0, 0, 0.1); + box-shadow: -1px -1px 0 rgba(0, 0, 0, 0.15), 1px 1px 0 rgba(255, 255, 255, 0.15); +} + +#tegaki-toolmode-bar .tegaki-mb-btn-a { + color: inherit; + background-color: rgba(0, 0, 0, 0.10); +} + +#tegaki-toolmode-bar .tegaki-mb-btn.tegaki-mb-btn-a:hover { + color: inherit; +} + +#tegaki-debug { + position: absolute; + left: 0; + top: 0; +} + +#tegaki-debug canvas { + width: 75px; + height: 75px; + display: block; + border: 1px solid black; +} + +.tegaki-backdrop { + overflow: hidden; +} + +.tegaki-hidden { + display: none !important; +} + +.tegaki-strike { + text-decoration: line-through; +} + +#tegaki-layers { + position: relative; + font-size: 0; + box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.25); + contain: content; +} + +#tegaki-layers canvas { + width: 100%; + height: 100%; +} + +#tegaki-layers:empty { + display: none; +} + +#tegaki-layers-wrap { + margin: auto; + padding: 50px; +} + +#tegaki-layers canvas { + position: absolute; + left: 0; + top: 0; +} + +#tegaki-finish-btn { + font-weight: bold; +} + +/* generic ui */ +.tegaki-alpha-bg, +.tegaki-alpha-bg-xs { + background-color: #fefefe; + background-image: + linear-gradient(45deg, #cacaca 25%, transparent 25%, transparent 75%, #cacaca 75%, #cacaca), + linear-gradient(45deg, #cacaca 25%, transparent 25%, transparent 75%, #cacaca 75%, #cacaca); +} + +.tegaki-alpha-bg { + background-size: 16px 16px; + background-position: 0 0, 8px 8px; +} + +.tegaki-alpha-bg-xs { + background-size: 6px 6px; + background-position: 0 0, 3px 3px; +} + +.tegaki-ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.tegaki-ui-cb { + display: inline-block; + vertical-align: middle; + width: 10px; + height: 10px; + border: 1px solid #222; + cursor: default; + margin-right: 4px; +} + +.tegaki-ui-cb::after, +.tegaki-ui-cb-a::after { + display: block; + content: ' '; + width: 6px; + height: 6px; + margin-top: 2px; + margin-left: 2px; +} + +.tegaki-ui-cb-a::after { + background-color: #222; +} + +.tegaki-ui-cb-w:hover .tegaki-ui-cb::after, +.tegaki-ui-cb:hover::after { + background-color: #555; +} + +.tegaki-ui-ellipsis::after { + content: '...'; + letter-spacing: -1px; +} + +.tegaki-ui-borderless { + border: none; +} + +.tegaki-ui-btn { + display: inline-block; +} + +.tegaki-ui-btn:before { + height: 24px; + width: 24px; + line-height: 24px; + font-size: 14px; +} + +.tegaki-stealth-input { + border: 0; + margin: 0; + padding: 0; + background: none; +} + +.tegaki-stealth-input:hover:not(.tegaki-disabled) { + background-color: rgba(0, 0, 0, 0.1); +} + +.tegaki-range-lbl, +.tegaki-range-lbl-xs { + display: inline-block; + text-align: center; + vertical-align: top; +} + +.tegaki-range-lbl { + width: 28px; + font-size: 12px; + margin-left: 4px; +} + +.tegaki-range-lbl-xs { + width: 20px; + font-size: 10px; +} + +.tegaki-label-xs { + font-size: 10px; + vertical-align: top; +} + +.tegaki-lbl-c::after { + content: ':'; +} + +.tegaki-lbl-p::after { + content: '%'; + margin-left: 1px; +} + +.tegaki-drag-lbl:not(.tegaki-disabled) { + cursor: ew-resize; +} + +.tegaki-disabled .tegaki-drag-lbl { + cursor: auto; +} + +/* control groups */ +.tegaki-ctrlgrp { + margin-bottom: 10px; +} + +.tegaki-ctrlgrp:last-child { + margin-bottom: 0; +} + +.tegaki-ctrlgrp-title { + font-size: 12px; + font-weight: bold; + margin-bottom: 6px; + background-color: #a3b1bf; + padding: 1px 4px; +} + +.tegaki-ctrlrow { + font-size: 11px; +} + +.tegaki-ctrlrow:not(:last-child) { + margin-bottom: 6px; +} + +.tegaki-ctrl-range { + width: calc(100% - 34px); + padding: 0; + margin: 0; + height: 14px; +} + +/* zoom ctrl group */ +#tegaki-zoom-lbl { + display: inline-block; + font-size: 12px; + float: right; + height: 24px; + line-height: 24px; +} + +/* color ctrl group */ +#tegaki-color-ctrl { + display: flex; +} + +#tegaki-palette-switcher { + align-self: center; + margin-left: auto; +} + +.tegaki-color-grid { + display: grid; + grid-gap: 4px; + margin-top: 6px; +} + +.tegaki-color-grid-20 { + grid-template-columns: repeat(auto-fill, 20px); + grid-auto-rows: 20px; +} + +.tegaki-color-grid-15 { + grid-template-columns: repeat(auto-fill, 15px); + grid-auto-rows: 15px; +} + +.tegaki-color-btn { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5); +} + +#tegaki-color, +#tegaki-colorpicker { + padding: 0; + border: 0; + display: block; + width: 28px; + height: 28px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); +} + +/* layers ctrl group */ +#tegaki-ctrlgrp-layers { + position: relative; +} + +#tegaki-layers-opts { + height: 18px; + display: flex; +} + +#tegaki-layer-alpha-cell { + margin-left: auto; +} + +#tegaki-layers-ctrl { + margin-top: 4px; +} + +#tegaki-layers-grid { + height: 84px; + min-height: 84px; + overflow: auto; + background-color: #8d99a6; + display: flex; + flex-direction: column; + border: 1px solid #a3b1bf; + resize: vertical; +} + +.tegaki-layers-cell { + box-sizing: border-box; + box-shadow: 0 1px 0 0px #a3b1bf; + padding: 0; + height: 28px; + flex-shrink: 0; + overflow: hidden; + display: flex; + align-items: center; +} + +.tegaki-layers-cell-s, +.tegaki-layers-cell-a { + background-color: #a3b1bf7f; +} + +.tegaki-layers-cell-a { + font-weight: bold; +} + +.tegaki-layers-cell-v { + margin: 0 6px 0 4px; +} + +.tegaki-layers-cell-v .tegaki-ui-cb { + vertical-align: unset; + margin: 0; +} + +.tegaki-layers-cell-p { + margin-right: 6px; +} + +.tegaki-layers-cell-p canvas { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); + vertical-align: middle; +} + +.tegaki-layers-cell-s .tegaki-layers-cell-p canvas { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 1.0); +} + +.tegaki-layers-cell-n { + font-size: 11px; + margin-right: 1px; + min-width: 20px; +} + +.tegaki-layers-cell-d { + box-shadow: inset 0 -2px 0 0 #000; + z-index: 2; +} + +#tegaki-layers-grid.tegaki-layers-cell-d { + box-shadow: 0 -2px 0 0px #000; +} + +#tegaki-layers-cell-dx { + position: absolute; + background: transparent; + width: 100%; + height: 32px; + margin-top: -32px; +} + diff --git a/css/tegaki.css b/css/tegaki.css new file mode 100644 index 0000000..31b14a3 --- /dev/null +++ b/css/tegaki.css @@ -0,0 +1,670 @@ +/*! Contains fonts from Font Awesome (Copyright (C) 2016 by Dave Gandy), Entypo (Copyright (C) 2012 by Daniel Bruce) */ +@font-face { + font-family: 'tegaki'; + src: url('data:application/octet-stream;base64,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') format('woff'); + font-weight: normal; + font-style: normal; +} + +.tegaki-icon:before { + font-family: 'tegaki'; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-align: center; + font-variant: normal; + text-transform: none; + line-height: 1em; +} + +.tegaki-cancel:before { content: '\e800'; } /* '' */ +.tegaki-plus:before { content: '\e801'; } /* '' */ +.tegaki-minus:before { content: '\e802'; } /* '' */ +.tegaki-pen:before { content: '\e803'; } /* '' */ +.tegaki-down-open:before { content: '\e804'; } /* '' */ +.tegaki-up-open:before { content: '\e805'; } /* '' */ +.tegaki-level-down:before { content: '\e806'; } /* '' */ +.tegaki-pencil:before { content: '\e807'; } /* '' */ +.tegaki-play:before { content: '\e808'; } /* '' */ +.tegaki-bucket:before { content: '\e809'; } /* '' */ +.tegaki-pause:before { content: '\e80a'; } /* '' */ +.tegaki-blur:before { content: '\e80b'; } /* '' */ +.tegaki-to-start:before { content: '\e80c'; } /* '' */ +.tegaki-watercolor:before { content: '\e80d'; } /* '' */ +.tegaki-tone:before { content: '\e80e'; } /* '' */ +.tegaki-airbrush:before { content: '\e80f'; } /* '' */ +.tegaki-fast-fw:before { content: '\e810'; } /* '' */ +.tegaki-fast-bw:before { content: '\e811'; } /* '' */ +.tegaki-left-open:before { content: '\e812'; } /* '' */ +.tegaki-right-open:before { content: '\e813'; } /* '' */ +.tegaki-eraser:before { content: '\f12d'; } /* '' */ +.tegaki-pipette:before { content: '\f1fb'; } /* '' */ + +.tegaki-disabled, +.tegaki-disabled::after, +.tegaki-disabled::before { + opacity: 0.45; +} + +.tegaki-hidden { + display: none !important; +} + +.tegaki-invis { + visibility: hidden !important; + width: 0 !important; + height: 0 !important; +} + +.tegaki-replay-mode #tegaki-tools-cnt, +.tegaki-replay-mode #tegaki-toolmode-bar, +.tegaki-replay-mode .tegaki-ctrlgrp, +.tegaki-replay-mode .tegaki-layers-cell, +.tegaki-replay-mode #tegaki-layers-ctrl { + pointer-events: none; +} + +.tegaki-replay-mode #tegaki-ctrlgrp-zoom, +.tegaki-replay-mode #tegaki-ctrlgrp-layers { + pointer-events: auto; +} + +#tegaki { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: #a3b1bf; + color: #222; + font-family: arial, sans-serif; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + z-index: 9999; + display: grid; + grid-template-columns: 40px 1fr 160px; + grid-template-rows: 24px 1fr 18px; + grid-gap: 2px; +} + +#tegaki input { + color: inherit; +} + +#tegaki > div { + background-color: #8d99a6; +} + +#tegaki-menu-cnt { + grid-area: 1 / 1 / 2 / 4; + white-space: nowrap; + overflow: hidden; + display: flex; +} + +#tegaki-tools-cnt { + grid-area: 2 / 1 / 4 / 2; + padding: 4px; +} + +#tegaki-canvas-cnt { + grid-area: 2 / 2 / 3 / 3; + overflow: auto; + display: flex; +} + +#tegaki-ctrl-cnt { + grid-area: 2 / 3 / 4 / 4; + padding: 6px; + overflow: hidden auto; +} + +#tegaki-status-cnt { + grid-area: 3 / 2 / 4 / 3; + line-height: 18px; + display: flex; +} + +#tegaki-status-cnt > div { + padding: 0 4px; +} + +#tegaki-status-replay { + color: #a61930; +} + +#tegaki-status-output { + font-size: 11px; + font-weight: bold; +} + +#tegaki-status-version { + color: #adbdcc; + font-size: 11px; + margin-left: auto; +} + +#tegaki-menu-bar { + font-size: 12px; + padding-left: 4px; + padding-right: 18px; + border-right: 2px solid #a3b1bf; +} + +.tegaki-replay-mode #tegaki-menu-bar { + padding-right: 4px; +} + +.tegaki-menu-lbl { + margin: 0 2px; + vertical-align: middle; +} + +#tegaki-replay-controls { + padding-right: 10px; + padding-left: 10px; + border-right: 2px solid #a3b1bf; + font-size: 11px; +} + +#tegaki-replay-timeline { + display: inline-block; + width: 100px; + height: 24px; + margin: 0 4px; + border-left: 1px solid #a3b1bf; + border-right: 1px solid #a3b1bf; + /*background-color: rgba(0, 0, 0, 0.25);*/ +} + +#tegaki-replay-timeline-fill { + display: inline-block; + width: 36px; + height: 100%; + background-color: #a3b1bf; +} + +#tegaki-replay-controls > span { + vertical-align: middle; +} + +#tegaki-replay-controls .tegaki-ui-cb-w { + margin-right: 4px; +} + +#tegaki-replay-speed-lbl { + width: 28px; + display: inline-block; + text-align: center; +} + +#tegaki-replay-speed-lbl::before { + content: '×'; +} + +#tegaki-replay-now-lbl, +#tegaki-replay-end-lbl { + display: inline-block; + max-width: 50px; + min-width: 30px; + overflow: hidden; + text-align: center; + margin: 0 4px; +} + +#tegaki-toolmode-bar { + font-size: 11px; + margin-left: 4px; + line-height: 24px; +} + +.tegaki-toolmode-lbl { + margin-right: 6px; +} + +.tegaki-toolmode-lbl::after { + content: ':'; +} + +.tegaki-toolmode-grp { + border-left: 1px solid #a3b1bf; + padding: 0 18px; +} + +#tegaki canvas { + image-rendering: optimizeSpeed; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-optimize-contrast; + image-rendering: pixelated; + -ms-interpolation-mode: nearest-neighbor; +} + +.tegaki-tool-active { + color: #f2f3f4; +} + +.tegaki-tool-btn { + width: 32px; + height: 32px; + display: block; + margin: auto; +} + +.tegaki-tool-btn:hover { + background-color: rgba(0, 0, 0, 0.15); +} + +.tegaki-tool-btn:before { + font-size: 20px; + width: 32px; + height: 32px; + line-height: 32px; +} + +.tegaki-mb-btn { + cursor: default; + text-decoration: none; + display: inline-block; + padding: 0 6px; + word-spacing: -1px; + position: relative; + line-height: 24px; + height: 24px; +} + +.tegaki-mb-btn:hover:not(.tegaki-disabled), +.tegaki-ui-btn:hover:not(.tegaki-disabled) { + background-color: rgba(0, 0, 0, 0.10); +} + +.tegaki-sw-btn { + display: inline; + padding: 2px 6px; + margin: 0 2px; + box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.15), -1px -1px 0 rgba(255, 255, 255, 0.15); +} + +.tegaki-sw-btn:hover:not(.tegaki-sw-btn-a) { + background-color: rgba(0, 0, 0, 0.05); +} + +.tegaki-sw-btn-a { + background-color: rgba(0, 0, 0, 0.1); + box-shadow: -1px -1px 0 rgba(0, 0, 0, 0.15), 1px 1px 0 rgba(255, 255, 255, 0.15); +} + +#tegaki-toolmode-bar .tegaki-mb-btn-a { + color: inherit; + background-color: rgba(0, 0, 0, 0.10); +} + +#tegaki-toolmode-bar .tegaki-mb-btn.tegaki-mb-btn-a:hover { + color: inherit; +} + +#tegaki-debug { + position: absolute; + left: 0; + top: 0; +} + +#tegaki-debug canvas { + width: 75px; + height: 75px; + display: block; + border: 1px solid black; +} + +.tegaki-backdrop { + overflow: hidden; +} + +.tegaki-hidden { + display: none !important; +} + +.tegaki-strike { + text-decoration: line-through; +} + +#tegaki-layers { + position: relative; + font-size: 0; + box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.25); + contain: content; +} + +#tegaki-layers canvas { + width: 100%; + height: 100%; +} + +#tegaki-layers:empty { + display: none; +} + +#tegaki-layers-wrap { + margin: auto; + padding: 50px; + pointer-events: none; +} + +#tegaki-layers canvas { + position: absolute; + left: 0; + top: 0; +} + +#tegaki-finish-btn { + font-weight: bold; +} + +#tegaki-cursor-layer { + position: absolute; + mix-blend-mode: difference; + touch-action: none; +} + +/* generic ui */ +.tegaki-alpha-bg, +.tegaki-alpha-bg-xs { + background-color: #fefefe; + background-image: + linear-gradient(45deg, #cacaca 25%, transparent 25%, transparent 75%, #cacaca 75%, #cacaca), + linear-gradient(45deg, #cacaca 25%, transparent 25%, transparent 75%, #cacaca 75%, #cacaca); +} + +.tegaki-alpha-bg { + background-size: 16px 16px; + background-position: 0 0, 8px 8px; +} + +.tegaki-alpha-bg-xs { + background-size: 6px 6px; + background-position: 0 0, 3px 3px; +} + +.tegaki-ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.tegaki-ui-cb { + display: inline-block; + vertical-align: middle; + width: 10px; + height: 10px; + border: 1px solid #222; + cursor: default; + margin-right: 4px; +} + +.tegaki-ui-cb::after, +.tegaki-ui-cb-a::after { + display: block; + content: ' '; + width: 6px; + height: 6px; + margin-top: 2px; + margin-left: 2px; +} + +.tegaki-ui-cb-a::after { + background-color: #222; +} + +.tegaki-ui-cb-w:hover .tegaki-ui-cb::after, +.tegaki-ui-cb:hover::after { + background-color: #555; +} + +.tegaki-ui-ellipsis::after { + content: '...'; + letter-spacing: -1px; +} + +.tegaki-ui-borderless { + border: none; +} + +.tegaki-ui-btn { + display: inline-block; +} + +.tegaki-ui-btn:before { + height: 24px; + width: 24px; + line-height: 24px; + font-size: 14px; +} + +.tegaki-stealth-input { + border: 0; + margin: 0; + padding: 0; + background: none; +} + +.tegaki-stealth-input:hover:not(.tegaki-disabled) { + background-color: rgba(0, 0, 0, 0.1); +} + +.tegaki-range-lbl, +.tegaki-range-lbl-xs { + display: inline-block; + text-align: center; + vertical-align: top; +} + +.tegaki-range-lbl { + width: 28px; + font-size: 12px; + margin-left: 4px; +} + +.tegaki-range-lbl-xs { + width: 20px; + font-size: 10px; +} + +.tegaki-label-xs { + font-size: 10px; + vertical-align: top; +} + +.tegaki-lbl-c::after { + content: ':'; +} + +.tegaki-lbl-p::after { + content: '%'; + margin-left: 1px; +} + +.tegaki-drag-lbl:not(.tegaki-disabled) { + cursor: ew-resize; +} + +.tegaki-disabled .tegaki-drag-lbl { + cursor: auto; +} + +/* control groups */ +.tegaki-ctrlgrp { + margin-bottom: 10px; +} + +.tegaki-ctrlgrp:last-child { + margin-bottom: 0; +} + +.tegaki-ctrlgrp-title { + font-size: 12px; + font-weight: bold; + margin-bottom: 6px; + background-color: #a3b1bf; + padding: 1px 4px; +} + +.tegaki-ctrlrow { + font-size: 11px; +} + +.tegaki-ctrlrow:not(:last-child) { + margin-bottom: 6px; +} + +.tegaki-ctrl-range { + width: calc(100% - 34px); + padding: 0; + margin: 0; + height: 14px; +} + +/* zoom ctrl group */ +#tegaki-zoom-lbl { + display: inline-block; + font-size: 12px; + float: right; + height: 24px; + line-height: 24px; +} + +/* color ctrl group */ +#tegaki-color-ctrl { + display: flex; +} + +#tegaki-palette-switcher { + align-self: center; + margin-left: auto; +} + +.tegaki-color-grid { + display: grid; + grid-gap: 4px; + margin-top: 6px; +} + +.tegaki-color-grid-20 { + grid-template-columns: repeat(auto-fill, 20px); + grid-auto-rows: 20px; +} + +.tegaki-color-grid-15 { + grid-template-columns: repeat(auto-fill, 15px); + grid-auto-rows: 15px; +} + +.tegaki-color-btn { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5); +} + +#tegaki-color, +#tegaki-colorpicker { + padding: 0; + border: 0; + display: block; + width: 28px; + height: 28px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); +} + +/* layers ctrl group */ +#tegaki-ctrlgrp-layers { + position: relative; +} + +#tegaki-layers-opts { + height: 18px; + display: flex; +} + +#tegaki-layer-alpha-cell { + margin-left: auto; +} + +#tegaki-layers-ctrl { + margin-top: 4px; +} + +#tegaki-layers-grid { + height: 84px; + min-height: 84px; + overflow: auto; + background-color: #8d99a6; + display: flex; + flex-direction: column; + border: 1px solid #a3b1bf; + resize: vertical; +} + +.tegaki-layers-cell { + box-sizing: border-box; + box-shadow: 0 1px 0 0px #a3b1bf; + padding: 0; + height: 28px; + flex-shrink: 0; + overflow: hidden; + display: flex; + align-items: center; +} + +.tegaki-layers-cell-s, +.tegaki-layers-cell-a { + background-color: #a3b1bf7f; +} + +.tegaki-layers-cell-a { + font-weight: bold; +} + +.tegaki-layers-cell-v { + margin: 0 6px 0 4px; +} + +.tegaki-layers-cell-v .tegaki-ui-cb { + vertical-align: unset; + margin: 0; +} + +.tegaki-layers-cell-p { + margin-right: 6px; +} + +.tegaki-layers-cell-p canvas { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); + vertical-align: middle; +} + +.tegaki-layers-cell-s .tegaki-layers-cell-p canvas { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 1.0); +} + +.tegaki-layers-cell-n { + font-size: 11px; + margin-right: 1px; + min-width: 20px; +} + +.tegaki-layers-cell-d { + box-shadow: inset 0 -2px 0 0 #000; + z-index: 2; +} + +#tegaki-layers-grid.tegaki-layers-cell-d { + box-shadow: 0 -2px 0 0px #000; +} + +#tegaki-layers-cell-dx { + position: absolute; + background: transparent; + width: 100%; + height: 32px; + margin-top: -32px; +} + diff --git a/css/tomorrow.css b/css/tomorrow.css new file mode 100644 index 0000000..d17fcd1 --- /dev/null +++ b/css/tomorrow.css @@ -0,0 +1,1734 @@ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #1d1f21 none; + color: #c5c8c6; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + margin-left: 0; + margin-right: 0; + margin-top: 5px; + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #2d2d2d; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +#t-root #t-cnt { + filter: invert(85%); +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +.adc-resp-bg { + margin: auto; + width: 728px; + height: 112px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } + + #quickReply { + z-index: 9000 !important; + } + + .adc-resp-bg { + width: 300px; + height: 250px; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.n-atb { + border-radius: 6px; + padding: 1px 4px; + color: #353839; +} + +.atsb2018 table { + margin: auto; + font-size: 90%; +} +.atsb2018 td { + white-space: nowrap; + padding: 0px 4px; +} +.atsb2018 .atsgc { + width: 280px; +} +.atsb2018 .atgg { + height: 20px; +} + +.n-atb-0 { background-color: #F56FA1; } +.n-atb-1 { background-color: #7B3F00; color: #F2F3F4; } +.n-atb-2 { background-color: #FFFDD0; } +.n-atb-3 { background-color: #E4D00A; } +.n-atb-4 { background-color: #50C878; } + +.n-atb-0::after { content: 'Team Peep'; } +.n-atb-1::after { content: 'Team Chocolate'; } +.n-atb-2::after { content: 'Team Creme'; } +.n-atb-3::after { content: 'Team Peanut Butter'; } +.n-atb-4::after { content: 'Team Mini'; } + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #282a2e; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #111; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#qr-painter-ctrl .oe-r-cb { + vertical-align: sub; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline, .mobileib { + display: none !important; +} + +a, a:visited { + color: #81a2be !important; + text-decoration: none; +} + +a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #81a2be !important; +} + +a:hover { + color: #5F89AC !important; +} + +div#absbot { + color: #c5c8c6; + clear: both; +} + +div.board>hr { + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img { + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +hr { + border: none; + border-top: 1px solid #282a2e; + height: 0; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +.mobile { + display: none; +} + +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules>li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #000; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner>div.boardTitle { + + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + margin-top: 0px; +} + +div.boardBanner>div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktop { + font-size: 9pt; + color: #c5c8c6; + display: block; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #81a2be; +} + +div.pContainer { +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + color: #c5c8c6; + float: left; + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +div.thread { + margin: 0px; + clear: both; +} + +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread>div:nth-of-type(2)>div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #282a2e; + border: 1px solid #282a2e; + display: table; + padding: 2px; +} + +div.reply input { + float: none; +} + +div.post div.postInfo { + display: block; + width: 100%; +} + +.fileText { + max-width: 600px; + white-space: nowrap; +} + +div.post div.postInfo span.postNum { +} + +div.post div.postInfo span.postNum > a { + text-decoration: none; + color: #C5C8C6 !important; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: #5F89AC !important; +} + +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + color: #c5c8c6; + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + color: #c5c8c6; + font-weight: normal !important; +} + +div.post div.postInfo span.date { +} + +div.post div.postInfo span.time { +} + +div.post div.postInfo span.subject { + color: #b294bb; + font-weight: bold; +} + +div.post blockquote.postMessage { + display: block; +} + +blockquote>span.quote { + color: #b5bd68; +} + +.quoteLink, .quotelink, .deadlink { + color: #5F89AC !important; + text-decoration: underline; +} + +a.quoteLink:hover, a.quotelink:hover { + color: #81a2be !important; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-list .quotelink { + color: #34345C !important; +} + +#arc-list .quotelink:hover { + color: #D00 !important; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + margin-bottom: 5px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + float: left; +} + +span.summary { + color: #707070; + margin-top: 10px; +} + +div.postingMode { + background-color: #282a2e; + padding: 1px; + text-align: center; + color: #c5c8c6; + font-weight: bold; + font-size: larger; + margin-top: 8px; + border: 1px solid #111; +} + +#verification table { + border: none !important; + margin: 0px; +} + +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; +} + +div.pagelist { + font-size: 13px !important; + margin: 0; + padding: 3px 7px; + float: left; + border: none; + background: #282a2e; + border: 1px solid #111; + list-style: none; + overflow: hidden; + color: #c5c8c6; +} + +div.pagelist>div { + float: left; +} + +div.pagelist>div span { + padding: 4px; + display: inline-block; +} + +div.pagelist div.pages { + padding: 4px; +} + +div.pagelist div.pages a { + text-decoration: none !important; + +} + +div.pagelist form { + display: inline; +} + +div.pagelist div.cataloglink { + border-left: 1px solid #111; + padding-left: 12px; + margin-left: 7px; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +input[type=password] { + width: 50px; + text-align: center; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; + font-size: 10pt; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #c5c8c6; + clear: both; + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #81a2be; +} + +div.homelink { + float: right; +} + +div#absbot { + text-align: center; + font-size: x-small !important; + padding-bottom: 4px; + padding-top: 10px; + color: #c5c8c6; +} + +#recaptcha_response_field { + padding: 0px; +} + +table { + border-spacing: 1px; + margin-left: auto; + margin-right: auto; +} + +table.postForm>tbody>tr>td:first-child { + background-color: #282a2e; + color: #c5c8c6; + font-weight: bold; + border: 1px solid #111; + padding: 0 5px; + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + font-size: 10pt; +} + +input[type=text], table.postForm>tbody textarea, input[type="password"], #recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + border: 1px solid #000; + background-color: #282a2e; + color: #c5c8c6; + outline: none; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +input[type="checkbox"], input[type="submit"], input[type="file"], button { + filter: brightness(80%); +} + +input::placeholder { + color: #919191; +} + +select { + border: 1px solid #575a60; + background-color: #282a2e; + color: #c5c8c6; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +input[type=text]:focus, input[type=password]:focus, input:not([type]):focus, textarea:focus { + border: 1px solid #515151; +} + +table.postForm>tbody>tr>td>input[type=text] { + width: 244px; +} + +table.postForm>tbody>tr>td>input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #282A2E; + color: #c5c8c6; + font-weight: bold; + padding: 0 5px; + font-size: 10pt; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: #c5c8c6; + text-align: center; +} + +.highlightPost:not(.op) { + background: #3A171C !important; + border-color: #3A171C !important; +} + +.hand { + cursor: pointer; +} + +.reply:target, .reply.highlight { + background: #1D1D21 !important; + border: 1px solid #111 !important; + padding: 2px; +} + +span.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +span.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #81a2be !important; +} + +span.capcodeDeveloper span.name, span.capcodeDeveloper span.name a, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +span.capcodeManager span.name, span.capcodeManager span.name a, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +span.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +#reportTypes a, +.useremail { + text-decoration: underline; +} + +.omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #999; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; +} + +div.posthover { + padding: 5px; + padding-left: 10px; + padding-right: 10px; +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + height: auto !important; + width: auto !important; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +#navtopright, #navbotright { + float: right; +} + +.preview { + background: #282a2e; + border: 1px solid #333 !important; + +} + +.qrWindow table.postForm>tbody>tr>td:first-child { + background-color: #232428; +} + +#settingsBox { + position: absolute; + right: 10px; + margin-top: 10px; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +.backlinkHr hr { + border-top-color: #1d1f21; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #282a2e; + overflow: hidden; + border-bottom: 2px solid #111; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail:not(:hover) .name, .useremail:not(:hover) .postertrip { + color: #81a2be !important; +} + +.useremail .name:hover, .useremail .postertrip:hover { + color: #5F89AC !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +#captchaContainer>img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; + + + margin: 0; +} + +.prettyprint { + background-color: rgba(255, 255, 255, .1); + border: 1px solid rgba(0, 0, 0, 0.5); +} + +span.tag { + color: #5F89AC; +} + +span.pun { + color: #72814D; +} + +span.com { + color: #BB6793; +} + +span.str { + color: #8EA062; +} + +span.kwd { + color: #81a2be; +} + +span.typ { + color: #b294bb; +} + +span.lit { + color: #6E9B89; +} + +span.pln { + color: #c5c8c6; +} + +/** this is not important **/ +#captchaContainer { + overflow: hidden; +} + +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; + font-size: 9pt; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #C5C8C6; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #C5C8C6; +} + +table.flashListing .subject { + color: #B294BB; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, .1); +} + +input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + color: #c5c8c6; + font-weight: bold; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +.ad-plea a { + text-decoration: none; +} + +.fileText a { + text-decoration: underline; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm textarea { + width: 292px; +} + +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #000; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #282A2E; + border: 1px solid #000; + border-bottom: none; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #000; +} +.dd-menu li:hover { + background-color: #1D1F21; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +#g-search-form { + text-align: center; +} +.g-search-ctrl { + vertical-align: top; + margin: 0 3px; +} +#js-sf-qf { + width: 185px; + padding: 2px 4px 3px 4px; +} +#js-sf-bf { + padding: 2px 4px 3px 4px; + width: 125px; +} +#js-sf-status { + text-align: center; + font-size: 24px; +} +.js-sf-err { + color: #C41E3A; +} +.boardBlock { + font-weight: bold; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.ogv-cnt { + position: relative; + display: inline-block; + text-align: center; + min-height: 16px; +} + +.ogv-cnt:not(.ogv-detached)::before { + content: 'Loading…'; + position: absolute; + top: 8px; + left: 0; + font-weight: bold; +} + +.ogv-cnt.ogv-loaded::before { + display: none; +} + +.ogv-cnt > ogvjs { + position: initial !important; +} + +.ogv-ctrl { + position: absolute; + bottom: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.65); + width: 100%; + height: 32px; + gap: 0; + display: none; +} + +.ogv-btn { + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + color: white; +} + +.ogv-btn svg { + vertical-align: middle; +} + +.ogv-btn svg:last-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:first-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:last-child { + display: inline; +} + +.ogv-ts { + font-family: sans-serif; + font-size: 10px; + text-align: center; + line-height: 32px; + width: 70px; + color: white; + overflow: hidden; +} + +.ogv-vol { + width: 50px; + margin: 0; +} + +.ogv-seek { + width: 0; + margin: 0; + flex-grow: 1; +} diff --git a/css/yotsuba.css b/css/yotsuba.css new file mode 100644 index 0000000..57730d1 --- /dev/null +++ b/css/yotsuba.css @@ -0,0 +1,457 @@ +body { + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + background: #FFFFEE url(/image/fade.png) top center repeat-x; + color: #800000; + padding-left: 5px; + padding-right: 5px; + margin-right: 0px; + margin-left: 0px; + margin-top: 5px; +} + +td { + font-size: 10pt; + padding: 0; + margin: 0; +} + +td.reply { + border: 1px solid #D9BFB7; + border-left: none; + border-top: none; + padding: 2px; +} + +blockquote { + font-size: 10pt; +} + +.doubledash { + color: #D9BFB7; +} + +.inputtext { + margin: 0; + margin-right: 2px; + padding: 1px 4px; + border: 1px solid #aaa; + outline: none; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +input.inputtext { + height: 1.75em; +} + +td > .inputtext { + height: auto; + padding-top: 2px; + padding-bottom: 3px; +} + +.inputtext:focus { + border: 1px solid #ea8; +} + +.logo img { + border: 1px solid #800; +} + +.logo b span { + font-family: tahoma; +} + +.logo span { + font-size: 28px; + letter-spacing: -2px; +} + +hr { + border: none; + border-top: 1px solid #D9BFB7; + height: 0px; +} + +.postblock { + border: 1px solid #800; + font-size: 10pt; + padding: 0px 5px; + line-height: 20px; +} + +.rules { + font-size: 10pt; +} + +.rules a { + text-decoration: none; +} + +form a img { + margin-top: 3px; +} + +iframe, .rotating { + background: #F0E0D6; + border-right: 1px solid #D9BFB7; + border-bottom: 1px solid #D9BFB7; +} + +.pages { + border: none; + background: #F0E0D6; + border-right: 1px solid #D9BFB7; + border-bottom: 1px solid #D9BFB7; +} + +.pages td { + border: none; + padding: 1px 5px; +} + +.pages td { + color: #b86; +} + +.pages td b { + color: #800; +} + +.deletebuttons { + text-align: right; +} + +.deletebuttons br { + display: none; +} + +a, a:visited { + color: #0000EE; +} + +a:hover { + color: #DD0000; +} + +a.quotelink { + color: #000080; +} + +.logo { + clear: both; + text-align: center; + font-size: 24pt; + color: #800000; + width: 100%; +} + +form { + margin-top: 0px; +} + +.rules { + width: 468px; + font-size: 10px; +} + +.rules a, .rules a:visited, .rules a:link { +} + +.rules > li { + list-style: none; +} + +.rules > li:before { + content: "\2022 \20"; +} + +.postblock { + background: #EEAA88; + color: #800000; + font-weight: 800; +} + +.footer { + text-align: center; + font-size: 12px; +} + +.unkfunc { + color: #789922; +} + +.filesize { + text-decoration: none; +} + +td .filesize { + display: inline; + background: none; +} + +.filesize span, span.postername, span.filetitle, span.commentpostername { + unicode-bidi: embed; +} + +.filetitle, .replytitle { + background: inherit; + + color: #CC1105; + font-weight: 800; +} + +.postername, .commentpostername { + background: inherit; + color: #117743; + font-weight: 800; +} + +.postertrip { + background: inherit; + color: #228854; +} + +.oldpost { + background: inherit; + color: #f00000; + font-weight: 800; +} + +.omittedposts, .abbr { + color: #707070; +} + +.reply { + background: #F0E0D6; + color: #800000; +} + +.replyhl { + background: #F0C0B0; + color: #800000; +} + +.doubledash { + vertical-align: top; + clear: both; + float: left; +} + +a.quotejs:active, a.quotejs:link, a.quotejs:visited { + color: #800000; + text-decoration: none; +} + +a.quotejs:hover { + color: #d00; +} + +.tn_thread { + width: 200px; + height: 100px; + margin: 0px 20px 20px 20px; + float: left; + background: #eed; + border: #ea8 1px solid; + text-align: center; +} + +.tn_reply { + width: 100px; + height: 100px; + margin: 0px 20px 20px 20px; + float: left; + background: #eed; + border: #ea8 1px solid; + text-align: center; +} + +.adHeadline { + font: bold 10pt serif; + text-decoration: underline; + color: #00e +} + +.adText { + font: normal 10pt serif; + text-decoration: none; + color: #800 +} + +#ad { + width: 120px; + + margin: 0; + padding: 0; + + position: absolute; + right: 150px; + + border: 1px solid #EEAA88; + + font-family: arial, helvetica, sans-serif; + font-size: 11px; + +} + +#ad div { + margin: 0; + padding: 0.4em; + +} + +#ad div.ad-title { + padding: 0em; + background: #EEAA88; + color: #800000; + font-size: 11px; +} + +#ad div.ad-title a { + font-family: arial, helvetica, sans-serif; + color: #800000; +} + +#ad div.ad-text { +} + +#ad div.ad-text a { + font-family: arial, helvetica, sans-serif; +} + +#ad div.ad-text a:visited { +} + +.bottomAdTitle { + font-family: arial, helvetica, sans-serif; + background: #EEAA88; + color: #800000; +} + +#bottomAd { + height: 52px; + font-size: 11px; +} + +#bottomAdOuter { + width: 600px; + border: 1px solid #EEAA88; + font-size: 11px; +} + +.spoiler a.quotelink, .spoiler .unkfunc { + color: inherit; +} + +.exif { + display: none; + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td { + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td b { + font-weight: 800; + text-decoration: underline; + font-size: x-small; +} + +#header { + position: absolute; + top: 2px; + left: 5px; + right: 5px; +} + +* html #header { + width: 100%; +} + +#navtop, #navbot { + left: 0px; + float: left; +} + +#navtopr, #navbotr { + right: 0px; + display: block; + float: right; + text-align: right; +} + +#header, #navbot, #navbotr { + font-size: 9pt; + color: #b86; +} + +#header a, #navbot a, #navbotr a, .pages td a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #800; +} + +#header a:hover, #navbot a:hover, #navbotr a:hover, .pages td a:hover { + color: #d00; +} + +#footer { + clear: both; + padding-top: 10px; +} + +#footer center font { + font-size: 7pt; +} + +.pages td a { + color: #00e; +} + +td.replyhl { + border: 1px solid #D99F91; + border-left: none; + border-top: none; + padding: 2px; +} + +td.deletebuttons input.checkbox { + margin: 1px 2px 1px 2px; +} + +div.logo img { + margin: 5px 0px 5px 0px; +} + +.fstitle { + float: left; + width: 25px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#recaptcha_response_field { + border: 1px solid #AAA !important; +} + +#recaptcha_div { + height: 107px; + width: 442px; +} + +#recaptcha_challenge_field { + width: 400px +} \ No newline at end of file diff --git a/css/yotsubamobile.css b/css/yotsubamobile.css new file mode 100644 index 0000000..c89757b --- /dev/null +++ b/css/yotsubamobile.css @@ -0,0 +1,1035 @@ +/** Here's one for you: internet explorer in wp7 can only handle 1 stylesheet at a time it seems **/ +/** Chrome Mobile seems to dislike media="" in html, so we do it this way instead. **/ +@media only screen and (max-width: 480px) { + /** GENERIC / ELEMENT STYLING **/ + html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + } + + .postMenuBtn { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); + margin: 4px -5px 0px 4px !important; + float: left; + font-weight: bold; + opacity: 1 !important; + height: 0.5em !important; + font-size: 16px; + } + + .dd-menu { + font-size: 16px; + line-height: 2.5em; + } + + .adg-rects > span.mobile { + display: inline-block !important; + } + + .party-hat { + left: -15px; + margin-top: -30px; + position: absolute; + width: 100px; + pointer-events: none; + } + + .pu-img { + background: url('//s.4cdn.org/image/minileaf@2x.gif'); + background-size: 100%; + } + + body { + background: #FFE url(/image/fade.png) top center repeat-x; + + color: #800000; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 0px; + padding-right: 0px; + + width: 100%; + } + + embed { + display: none !important; + } + + .jlist-link { + margin: 15px auto 30px auto; + } + + img { + border: none; + } + + img.topad, .topad > div, .topad a img { + width: 300px; + height: 250px; + max-width: 100%; + overflow: hidden; + margin: auto; + } + + img.middlead, .middlead > div, .middlead a img { + width: 234px; + height: 30px; + max-width: 100%; + overflow: hidden; + margin: auto; + } + + img.bottomad, .bottomad > div, .bottomad a img { + width: 320px; + height: 40px; + max-width: 100%; + overflow: hidden; + margin: auto; + } + + ul.rules { + display: none; + } + + .button a { + text-decoration: none !important; + } + + div.boardBanner { + margin-top: 40px !important; + } + + .mobile { + display: block !important; + clear: left !important; + } + + #quote-preview { + box-shadow: 0px 0px 4px 0 rgba(45, 77, 59, 0.5); + } + + .noPictures .mFileInfo { + display: none !important; + } + + .postLink.mobile { + clear: both !important; + } + + .mobileinline { + display: inline !important; + } + + .mobileib { + display: inline-block !important; + } + + span.replyTextM { + display: inline !important; + } + + .desktop { + display: none !important; + } + + .hideMobile { + display: none !important; + } + + div.board > hr { + border: none; + border-top: 1px solid #D9BFB7; + + height: 0; + + margin-top: 30px !important; + margin-bottom: 30px !important; + } + + div.board > hr:last-of-type { + margin-bottom: 10px !important; + } + + hr.abovePostForm { + width: 90%; + } + + span.x-small { + font-size: x-small; + } + + .backlink.mobile { + background-color: #EAD6CA; + border-top: 1px solid #D9BFB7; + } + + /** Mobile Specific CSS **/ + div.postLink { + background-color: #ead6ca; + border-top: 1px solid #D9BFB7; + padding: 5px; + + overflow: hidden; + } + + div.postLink span { + float: left; + + } + + div.postLink a { + float: right; + + color: #800 !important; + } + + .mobilePostFormToggle { + text-align: center; + + font-weight: bold; + + margin: 0 auto; + padding-top: 15px; + } + + a.mobilePostFormToggle { + text-align: center; + + display: inline-block !important; + } + + .mobilePostFormToggle div { + width: 300px; + background-color: #EA8; + padding-top: 5px; + padding-bottom: 5px; + border: 1px solid #800; + margin: 0 auto; + } + + div.post div.file .fileThumb img { + max-width: 125px; + max-height: 125px; + object-fit: scale-down; + } + + span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; + } + + div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; + } + + div.post div.file .fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; + text-decoration: none; + } + + div.post div.file.image-expanded .fileThumb { + margin-left: 0 !important; + margin-right: 0 !important; + } + + div.post div.file div.fileText { + display: none; + } + + div.sideArrows { + display: none; + } + + div.reply { + padding: 2px; + + background-color: #f0e0d6; + + margin: 0px; + border: 0; + } + + div.replyContainer { + background-color: #F0E0D6; + margin: 5px 0 0; + padding-left: 0; + } + + .is_index div.replyContainer { + margin: 7px 0 0 0; + } + + div.opContainer { + margin: 5px 0 0; + padding-left: 0; + + display: block; + overflow: hidden; + + background-color: #f5e9e1; + + border: none; + } + + span.summary:not(.preview-summary) { + padding: 5px; + text-align: center; + display: block; + + background-color: #EAD6CA; + border-bottom: 1px solid #D9BFB7 !important; + + color: #800; + + margin-top: 0px; + margin-bottom: 5px; + + font-weight: bold; + } + + div.postLink span.info { + color: #800; + margin-top: 8px; + } + + blockquote { + margin: 10px !important; + } + + blockquote.postMessage { + font-size: 11pt; + } + + .omittedposts, .abbr { + color: #707070 !important; + font-size: 10pt !important; + } + + div.thread { + border-top: none; + } + + div.opContainer div.postInfo { + display: none !important; + } + + div.post div.postInfoM { + overflow: hidden; + + border-bottom: 1px solid #D9BFB7; + background-color: #EAD6CA; + + padding: 5px; + } + + div.replyContainer div.postInfoM { + margin: 0px; + } + + div.post div.postInfoM span.postNum, div.postInfo span.postNum { + float: left; + } + + div.post div.postInfoM span.postNum a, div.postInfo span.postNum a { + text-decoration: none; + color: #800000 !important; + } + + div.post div.postInfoM span.postNum a:hover { + color: red; + } + + /* Name */ + div.post div.postInfoM span.nameBlock, div.postInfo span.nameBlock { + display: inline; + float: left; + + clear: left; + } + + div.post div.postInfoM span.nameBlock span.name, div.postInfo span.nameBlock span.name { + color: #117743; + font-weight: bold; + } + + div.post div.postInfoM span.nameBlock span.tripcode, div.postInfo span.nameBlock span.tripcode { + color: #117743; + } + + /* Date/Time */ + div.post div.postInfoM span.dateTime { + float: right; + /*font-style: italic;*/ + text-align: right; + } + + div.post div.postInfoM span.time { + + } + + div.post div.postInfoM span.subject { + color: #cc1105; + font-weight: bold; + } + + span.fileText { + font-size: smaller; + } + + div.replyContainer div.reply { + width: 100%; + + padding: 0px !important; + + } + + div.replyContainer div.post div.postInfo { + overflow: hidden; + + border-bottom: 1px solid #D9BFB7; + background-color: #EAD6CA; + + padding: 3px; + + margin: 0px; + } + + div.replyContainer div.post div.postInfo input[type=checkbox] { + display: none; + } + + div.replyContainer div.post div.postInfo span.postNum { + + font-style: italic; + } + + div.replyContainer div.post div.postInfo span.userInfo { + float: left; + padding-left: 5px; + } + + div.replyContainer div.post div.postInfo span.nameBlock { + + } + + div.replyContainer div.post div.postInfo span.postNum a:first-child:after { + content: " "; + } + + div.replyContainer div.post div.postInfo span.dateTime { + float: right; + text-align: right; + + padding-right: 10px; + + font-style: italic; + } + + div.replyContainer div.post div.postInfo span.dateTime span.date { + display: block; + } + + div.thread > div:nth-of-type(2) > div.reply { + margin-top: 0px !important; + } + + div.replyContainer div.post div.fileInfo { + margin-left: 0px; + } + + div.replyContainer div.post div.file:not(.image-expanded) { + padding: 5px; + } + + div.mPagelist { + margin-top: 10px; + text-align: center; + + border-bottom: 1px solid #D9BFB7; + + padding-bottom: 10px; + + color: #B86; + } + + div.mPagelist strong { + color: #800; + } + + div.mPagelist > div.prev, div.mPagelist div.next { + margin: 20px 2px 15px; + display: inline-block; + } + + .button { + border-radius: 3px; + padding: 6px 10px 5px 10px; + font-weight: bold; + background-color: #f0e0d6; + border: 1px solid #c0a69d; + user-select: none; + + background-image: url(/image/buttonfade.png); + background-repeat: repeat-x; + + text-decoration: none; + color: #800 !important; + } + + #globalToggle { + width: 200px; + display: inline; + text-align: center; + + margin: 0 auto; + margin-bottom: 10px; + } + + .redButton { + background-color: #ffadad; + background-image: url(/image/buttonfade-red.png); + border: 1px solid #C45858; + + color: #800 !important; + } + + div.mPagelist span { + padding-left: 3px; + padding-right: 3px; + + font-size: larger; + } + + .button:hover { + cursor: pointer; + } + + .mobileCatalogLink, + div.absBotText { + margin-top: 10px; + } + + .mobileCatalogLink { + font-size: larger; + } + + #disable-mobile { + font-size: small !important; + } + + #enable-mobile { + font-size: small !important; + display: none; + } + + .absBotDisclaimer { + display: none !important; + } + + div#boardNavMobile { + padding: 2px 4px; + + background-color: #F0E0D6; + + overflow: hidden; + + border-bottom: 2px solid #D9BFB7; + + position: fixed; + top: 0px; + + width: 100%; + font-size: x-small; + } + + div#boardNavMobile select, div#boardNavMobile option { + font-size: x-small; + } + + div.boardSelect { + float: left; + } + + div.boardSelect > strong { + padding-right: 5px; + } + + div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; + font-size: 7.5pt; + } + + .pageJump a { + text-decoration: none; + padding-right: 5px; + } + + div.postingMode { + background-color: #e04000; + padding: 3px; + text-align: center; + + width: 300px; + margin: 0 auto; + } + + div.navLinks { + margin-top: 9px !important; + margin-bottom: 0px !important; + text-align: center; + } + + /** WP7 ADDITIONS **/ + div.post div.postInfo span.postNum a { + text-decoration: none; + color: #800000!important; + } + + div.boardBanner { + text-align: center; + clear: both; + } + + div.boardBanner > img { + border: 1px solid #880000; + margin: 5px 0px 5px 0px; + height: 50px; + width: 150px; + max-width: 100%; + } + + div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + + margin-top: 0px; + } + + div.boardBanner > div.boardSubtitle { + font-size: x-small; + } + + div.post div.file .fileThumb img { + border: none; + + float: left; + } + + div.post div.file .fileThumb .expanded-thumb { + max-width: 100vw !important; + max-height: none !important; + } + + .expandedWebm { + width: auto !important; + height: auto !important; + margin: 3px 0 5px; + max-width: 100vw !important; + max-height: none !important; + } + + span.subject { + display: block; + } + + hr { + border: none; + border-top: 1px solid #D9BFB7; + + height: 0; + } + + .commentpostername { + font-weight: bold; + } + + .identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; + } + + .stickyIcon { + margin-bottom: -2px; + padding-left: 2px; + height: 16px; + width: 16px; + } + + .archivedIcon, + .closedIcon { + margin-bottom: -2px; + margin-left: -1px; + height: 16px; + width: 16px; + } + + .trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; + } + + .fileDeleted { + height: 13px; + width: 172px; + } + + .fileDeletedRes { + height: 13px; + width: 127px; + } + + .fileDeleted { + height: 13px; + width: 172px; + } + + .fileDeletedRes { + height: 13px; + width: 127px; + } + + .navSmall { + font-size: 90%; + } + + .center { + text-align: center; + } + + .bold { + font-weight: bold; + } + + .smaller { + font-size: smaller; + } + + .password { + font-size: smaller; + } + + .passNotice { + font-size: smaller; + padding-left: 3px; + } + + .qcDiv { + display: none; + } + + .qcImg { + height: 1px; + width: 1px; + border: 0px; + } + + .jpnFlag { + height: 11px; + width: 17px; + } + + .globalMessage { + color: red; + text-align: center; + } + + .highlightPost { + background: #f0d6d6 !important; + } + + span.postertrip { + color: #117743; + font-weight: normal !important; + } + + span.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; + } + + span.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; + } + + .omittedposts, .abbr { + color: #707070; + } + + span.spoiler { + color: #000 !important; + background: #000 !important; + } + + span.spoiler:hover, span.spoiler:focus { + color: #fff !important; + } + + table.exif { + display: none; + min-width: 450px; + } + + table.exif td { + color: #707070; + min-width: 150px; + font-size: 8pt; + } + + table.exif td b { + text-decoration: underline; + } + + div.posthover { + max-width: 400px; + margin-left: 20px; + + } + + div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + + } + + div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; + } + + div.posthover blockquote { + margin: 5px; + + } + + div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + } + + div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; + } + + #navtopright, #navbotright { + float: right; + } + + .preview { + background: #F0E0D6; + + border: 1px solid #D9BFB7 !important; + border-right: 2px solid #D9BFB7 !important; + border-bottom: 2px solid #D9BFB7 !important; + } + + #settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; + + background-color: #f0e0d6; + } + + div.mPagelist a { + text-decoration: none !important; + } + + .pages a { + bottom: -1px; + position: relative; + } + + img.expandedImg { + max-width: auto !important; + max-height: auto !important; + } + + .prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + + max-width: 300px; + word-wrap: break-word; + } + + td > input[type="text"], td > textarea, td > input[type="password"] { + width: 220px !important; + margin-right: 0 !important; + } + + td > input { + margin-left: 1px; + } + + #postForm { + width: auto; + } + + #postForm input[name="sub"] { + width: 160px !important; + } + + #postForm input[type="submit"] { + width: 60px; + padding: 2px 4px 3px 4px; + margin: 0px; + } + + #postForm input[type="password"] { + width: 70px !important; + } + + tr.mobile { + display: table-row !important; + } + + tr.mobile td { + padding: 5px; + } + + .recaptcha_image_cell { + width: auto !important; + padding: 0 !important; + } + + #recaptcha_table tr > td:last-child { + display: none; + } + + #recaptcha_table tr[height="73"] { + height: auto !important; + } + + #recaptcha_table tr > td { + padding: 0 !important; + } + + #recaptcha_image { + width: 280px !important; + + } + + .recaptchatable .recaptcha_image_cell center { + + } + + #recaptcha_response_field { + width: 272px !important; + margin-left: 3px; + margin-top: -1px; + + font-size: 10pt !important; + } + + #recaptcha_image > img { + width: 280px !important; + } + + #postForm:not(.hideMobile) { + /*max-width: 280px !important;*/ + overflow: hidden; + margin-top: 20px; + display: table; + } + + form[name="post"] { + margin: auto; + max-width: 100%; + } + + input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; + } + + input:focus, textarea:focus { + border: 1px solid #ea8 !important; + } + + a, + .useremail:not(:hover) .name:not(.capcode), + .useremail:not(:hover) .postertrip:not(.capcode) { + color: #00E !important; + } + + a.replylink, div#absbot a { + text-decoration: underline !important; + } + + a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #00E !important; + } + + a:hover, + .useremail .name:hover, .useremail .postertrip:hover, + a.quoteLink:hover, a.quotelink:hover, a.deadlink:hover, + .useremail *:hover, .useremail:hover *, + .posteruid .hand:hover { + color: red !important; + } + + .quoteLink, .quotelink, .deadlink, .pageJump a { + color: #000080 !important; + } + + .button a, a.button:hover, .button a:visited { + color: #800 !important; + } + + a.redButton:hover, + a.redButton:focus { + color: #880000 !important; + } + + table#recaptcha_table > tbody > tr:first-child > td:nth-child(2) { + display: none; + } + + .reply:target, .reply:focus, .reply.highlight { + background: #F0C0B0 !important; + border: none !important; + } + + .mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; + } + + .mobileSpoiler { + padding: 2px !important; + } + + #mpostform { + text-align: center; + margin-top: 10px; + } + +} diff --git a/css/yotsubanew.css b/css/yotsubanew.css new file mode 100644 index 0000000..3046b84 --- /dev/null +++ b/css/yotsubanew.css @@ -0,0 +1,1731 @@ +/** GENERIC / ELEMENT STYLING **/ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #FFFFEE url(/image/fade.png) top center repeat-x; + + color: #800000; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +.adc-resp-bg { + margin: auto; + width: 728px; + height: 112px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } + + #quickReply { + z-index: 9000 !important; + } + + .adc-resp-bg { + width: 300px; + height: 250px; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.n-atb { + border-radius: 6px; + padding: 1px 4px; + color: #353839; +} + +.atsb2018 table { + margin: auto; + font-size: 90%; +} +.atsb2018 td { + white-space: nowrap; + padding: 0px 4px; +} +.atsb2018 .atsgc { + width: 280px; +} +.atsb2018 .atgg { + height: 20px; +} + +.n-atb-0 { background-color: #F56FA1; } +.n-atb-1 { background-color: #7B3F00; color: #F2F3F4; } +.n-atb-2 { background-color: #FFFDD0; } +.n-atb-3 { background-color: #E4D00A; } +.n-atb-4 { background-color: #50C878; } + +.n-atb-0::after { content: 'Team Peep'; } +.n-atb-1::after { content: 'Team Chocolate'; } +.n-atb-2::after { content: 'Team Creme'; } +.n-atb-3::after { content: 'Team Peanut Butter'; } +.n-atb-4::after { content: 'Team Mini'; } + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #f0e0d6; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #d9bfb7; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_div { + height: 107px; width: 442px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline, .mobileib { + display: none !important; +} + +a, a:visited { + color: #00E; + text-decoration: none; +} + +a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #00E !important; +} + +a:hover { + color: red !important; +} + +div#absbot { + color: #800000; + clear: both; +} + +div.board > hr { + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img{ + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +hr { + border: none; + border-top: 1px solid #D9BFB7; + + height: 0; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#qr-painter-ctrl .oe-r-cb { + vertical-align: sub; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/** MOBILE ONLY DISABLES **/ +.mobile { + display: none; +} + +/** HEADER **/ +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules > li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; +} + +#bannerCnt { + border: 1px solid #800; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktop { + font-size: 9pt; + color: #B86; + display: block; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #800000; +} + +/** General Containers **/ +div.pContainer { + +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + color: #e0bfb7; + + float: left; + + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +/** Thread Container **/ +div.thread { + + margin: 0px; + clear: both; + +} + +/** Post Container **/ +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread > div:nth-of-type(2) > div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #f0e0d6; + + border: 1px solid #D9BFB7; + border-left: none; + border-top: none; + + display: table; + + padding: 2px; +} + +div.reply input { + float: none; +} + +/** Post Information **/ +div.post div.postInfo { + display: block; + width: 100%; +} + +.fileText { + max-width: 600px; + white-space: nowrap; +} + +div.post div.postInfo span.postNum { + +} + +div.post div.postInfo span.postNum a { + text-decoration: none; + color: #800000; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: red !important; +} + +/* Name */ +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + color: #117743; + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + color: #117743; + font-weight: normal !important; +} + +/* Date/Time */ +div.post div.postInfo span.date { + +} + +div.post div.postInfo span.time { + +} + +/* Subject */ +div.post div.postInfo span.subject { + color: #cc1105; + font-weight: bold; +} + +/** Message **/ +div.post blockquote.postMessage { + display: block; +} + +blockquote > span.quote { + color: #789922; +} + +.quoteLink, .quotelink, .deadlink, .pageJump a { + color: #000080 !important; + text-decoration: underline; +} + +.pageJump a:hover { + color: #ff0000 !important; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-list .quotelink { + color: #34345C !important; +} + +#arc-list .quotelink:hover { + color: #D00 !important; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +/** File Information **/ +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + margin-bottom: 5px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + + float: left; +} + +/** Summary **/ +span.summary { + color: #707070; + margin-top: 10px; +} + +/** POST FORM **/ +div.postingMode { + background-color: #e04000; + padding: 1px; + text-align: center; + + color: #fff; + font-weight: bold; + font-size: larger; + + margin-top: 8px; +} + +#verification table { + border: none !important; + margin: 0px; +} + +/** FOOTER **/ +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; + +} + +div.pagelist { + font-size: 13px !important; + + margin: 0; + padding: 3px 7px; + + float: left; + + border: none; + background: #F0E0D6; + + border-right: 1px solid #D9BFB7; + border-bottom: 1px solid #D9BFB7; + + list-style: none; + overflow: hidden; + + color: #B86; +} + +div.pagelist > div { + float: left; +} + +div.pagelist > div span { + padding: 4px; + display: inline-block; +} + +div.pagelist div.pages { + padding: 4px; +} + +div.pagelist div.pages a { + text-decoration: none !important; + /* FINE MOOT GOSH JEEZ */ +} + +div.pagelist form { + display: inline; +} + +div.pagelist strong { + color: #800000; +} + +div.pagelist div.cataloglink { + border-left: 1px solid #D9BFB7; + padding-left: 12px; + margin-left: 7px; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +input[type=password] { + width: 50px; + text-align: center; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; + font-size: 10pt; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #B86; + + clear: both; + + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #800000; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + + padding-bottom: 4px; + padding-top: 10px; + + color: #800; +} + +#recaptcha_response_field { + padding: 0px; +} + +/** POST FORM **/ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #EA8; + color: #800; + font-weight: bold; + border: 1px solid #800; + padding: 0 5px; + + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +input[type=text], input[type=password], table.postForm > tbody textarea, #recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + border: 1px solid #AAA; + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +input[type=text]:focus, input[type=password]:focus, input:not([type]):focus, textarea:focus { + border: 1px solid #ea8 !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #EA8; + color: #800; + font-weight: bold; + border: 1px solid #800; + padding: 0 5px; + font-size: 10pt; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + width: 292px + } +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: red; + text-align: center; +} + +.highlightPost:not(.op) { + background: #f0d6d6 !important; + border-color: #d69595 !important; +} + +.reply:target, .reply.highlight { + background: #F0C0B0 !important; + border: 1px solid #D99F91 !important; + border-left: none !important; + border-top: none !important; + padding: 2px; +} + +.hand { + cursor: pointer; +} + +.nameBlock.capcodeAdmin span.name, span.capcodeAdmin a span.name, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +.nameBlock.capcodeMod span.name, span.capcodeMod a span.name, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; +} + +.nameBlock.capcodeDeveloper span.name, span.capcodeDeveloper a span.name, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +.nameBlock.capcodeManager span.name, span.capcodeManager a span.name, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +.nameBlock.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +#reportTypes a, +.useremail { + text-decoration: underline; +} + +.omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #707070; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; + +} + +div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; + +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + + height: auto !important; + width: auto !important; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +#navtopright, #navbotright { + float: right; +} + +.preview { + background: #F0E0D6; + + border: 1px solid #D9BFB7 !important; + border-right: 2px solid #D9BFB7 !important; + border-bottom: 2px solid #D9BFB7 !important; +} + +#settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #F0E0D6; + overflow: hidden; + border-bottom: 2px solid #D9BFB7; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/** QUICK REPLY **/ +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; + +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +.op .backlinkHr { + width: 55%; +} + +.useremail:not(:hover) .name, .useremail:not(:hover) .postertrip { + color: #0000EE !important; +} + +.useremail:hover * { + color: red !important; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +/** this is not important **/ +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + max-width: 600px; + + margin: 0; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; + font-size: 9pt; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #117743; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #117743; +} + +table.flashListing .subject { + color: #cc1105; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: #ede2d4; +} + +input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + background: inherit; + color: #f00000; + font-weight: 800; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +.ad-plea a { + text-decoration: none; +} + +.fileText a { + text-decoration: underline; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm textarea { + width: 292px; +} + +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #F0E0D6; + border: 1px solid #D9BFB7; + border-right-width: 2px; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #D9BFB7; +} +.dd-menu li:hover { + background-color: #FFFFEE; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +#g-search-form { + text-align: center; +} +.g-search-ctrl { + vertical-align: top; + margin: 0 3px; +} +#js-sf-qf { + width: 185px; + padding: 2px 4px 3px 4px; +} +#js-sf-bf { + padding: 2px 4px 3px 4px; + width: 125px; +} +#js-sf-status { + text-align: center; + font-size: 24px; +} +.js-sf-err { + color: #C41E3A; +} +.boardBlock { + font-weight: bold; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.ogv-cnt { + position: relative; + display: inline-block; + text-align: center; + min-height: 16px; +} + +.ogv-cnt:not(.ogv-detached)::before { + content: 'Loading…'; + position: absolute; + top: 8px; + left: 0; + font-weight: bold; +} + +.ogv-cnt.ogv-loaded::before { + display: none; +} + +.ogv-cnt > ogvjs { + position: initial !important; +} + +.ogv-ctrl { + position: absolute; + bottom: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.65); + width: 100%; + height: 32px; + gap: 0; + display: none; +} + +.ogv-btn { + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + color: white; +} + +.ogv-btn svg { + vertical-align: middle; +} + +.ogv-btn svg:last-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:first-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:last-child { + display: inline; +} + +.ogv-ts { + font-family: sans-serif; + font-size: 10px; + text-align: center; + line-height: 32px; + width: 70px; + color: white; + overflow: hidden; +} + +.ogv-vol { + width: 50px; + margin: 0; +} + +.ogv-seek { + width: 0; + margin: 0; + flex-grow: 1; +} diff --git a/css/yotsublue.css b/css/yotsublue.css new file mode 100644 index 0000000..81b2379 --- /dev/null +++ b/css/yotsublue.css @@ -0,0 +1,457 @@ +body { + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + background: #EEF2FF url(/image/fade-blue.png) top center repeat-x; + color: #000000; + padding-left: 5px; + padding-right: 5px; + margin-right: 0px; + margin-left: 0px; + margin-top: 5px; +} + +td { + font-size: 10pt; + padding: 0; + margin: 0; +} + +td.reply { + border: 1px solid #B7C5D9; + border-left: none; + border-top: none; + padding: 2px; +} + +blockquote { + font-size: 10pt; +} + +.doubledash { + color: #B7C5D9; +} + +.inputtext { + margin: 0; + margin-right: 2px; + padding: 1px 4px; + border: 1px solid #aaa; + outline: none; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +input.inputtext { + height: 1.75em; +} + +td > .inputtext { + height: auto; + padding-top: 2px; + padding-bottom: 3px; +} + +.inputtext:focus { + border: 1px solid #9988EE; +} + +.logo img { + border: 1px solid #34345C; +} + +.logo b span { + font-family: tahoma; +} + +.logo span { + font-size: 28px; + letter-spacing: -2px; +} + +hr { + border: none; + border-top: 1px solid #B7C5D9; + height: 0px; +} + +.postblock { + border: 1px solid #000; + font-size: 10pt; + padding: 0px 5px; + line-height: 20px; +} + +.rules { + font-size: 10pt; +} + +.rules a { + text-decoration: none; +} + +form a img { + margin-top: 3px; +} + +iframe, .rotating { + background: #D6DAF0; + border-right: 1px solid #B7C5D9; + border-bottom: 1px solid #B7C5D9; +} + +.pages { + border: none; + background: #D6DAF0; + border-right: 1px solid #B7C5D9; + border-bottom: 1px solid #B7C5D9; +} + +.pages td { + border: none; + padding: 1px 5px; +} + +.pages td { + color: #89a; +} + +.pages td b { + color: #000; +} + +.deletebuttons { + text-align: right; +} + +.deletebuttons br { + display: none; +} + +a, a:visited { + color: #34345C; +} + +a:hover { + color: #DD0000; +} + +a.quotelink { + color: #DD0000; +} + +.logo { + clear: both; + text-align: center; + font-size: 24pt; + color: #AF0A0F; + width: 100%; +} + +form { + margin-top: 0px; +} + +.rules { + width: 468px; + font-size: 10px; +} + +.rules a, .rules a:visited, .rules a:link { +} + +.rules > li { + list-style: none; +} + +.rules > li:before { + content: "\2022 \20"; +} + +.postblock { + background: #9988EE; + color: #000000; + font-weight: 800; +} + +.footer { + text-align: center; + font-size: 12px; +} + +.unkfunc { + color: #789922; +} + +.filesize { + text-decoration: none; +} + +.filesize span, span.postername, span.filetitle, span.commentpostername { + unicode-bidi: embed; +} + +.filetitle, .replytitle { + background: inherit; + color: #0F0C5D; + font-weight: 800; +} + +.postername, .commentpostername { + background: inherit; + color: #117743; + font-weight: 800; +} + +.postertrip { + background: inherit; + color: #228854; +} + +.oldpost { + background: inherit; + color: #0F0C5D; + font-weight: 800; +} + +.omittedposts, .abbr { + color: #070707; +} + +.reply { + background: #D6DAF0; + color: #000000; +} + +.replyhl { + background: #D6BAD0; + color: #000000; +} + +.doubledash { + vertical-align: top; + clear: both; + float: left; +} + +a.quotejs:active, a.quotejs:link, a.quotejs:visited { + color: #000000; + text-decoration: none; +} + +a.quotejs:hover { + color: #d00; +} + +.tn_thread { + width: 200px; + height: 100px; + margin: 0px 20px 20px 20px; + float: left; + background: #eef2ff; + border: #98e 1px solid; + text-align: center; +} + +.tn_reply { + width: 100px; + height: 100px; + margin: 0px 20px 20px 20px; + float: left; + background: #eef2ff; + border: #98e 1px solid; + text-align: center; +} + +.adHeadline { + font: bold 10pt serif; + text-decoration: underline; + color: #00e +} + +.adText { + font: normal 10pt serif; + text-decoration: none; + color: #000 +} + +#ad { + width: 120px; + + margin: 0; + padding: 0; + + position: absolute; + right: 150px; + + border: 1px solid #98E; + + font-family: arial, helvetica, sans-serif; + font-size: 11px; + +} + +#ad div { + margin: 0; + padding: 0.4em; + +} + +#ad div.ad-title { + padding: 0em; + background: #98E; + color: #000; + font-size: 11px; +} + +#ad div.ad-title a { + font-family: arial, helvetica, sans-serif; + color: #000; +} + +#ad div.ad-text { +} + +#ad div.ad-text a { + font-family: arial, helvetica, sans-serif; +} + +#ad div.ad-text a:visited { +} + +.bottomAdTitle { + font-family: arial, helvetica, sans-serif; + background: #98E; + color: #000000; +} + +#bottomAd { + height: 52px; + font-size: 11px; +} + +#bottomAdOuter { + width: 600px; + border: 1px solid #98E; + font-size: 11px; +} + +.spoiler a.quotelink, .spoiler .unkfunc { + color: inherit; +} + +.exif { + display: none; + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td { + font-family: arial, helvetica, sans-serif; + font-size: xx-small; +} + +.exif td b { + font-weight: 800; + text-decoration: underline; + font-size: x-small; +} + +#header { + position: absolute; + top: 2px; + left: 5px; + right: 5px; +} + +* html #header { + width: 100%; +} + +#navtop, #navbot { + left: 0px; + float: left; +} + +#navtopr, #navbotr { + right: 0px; + display: block; + float: right; + text-align: right; +} + +#header, #navbot, #navbotr { + font-size: 9pt; + color: #89a; +} + +#header a, #navbot a, #navbotr a, .pages td a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #34345C; +} + +#header a:hover, #navbot a:hover, #navbotr a:hover, .pages td a:hover { + color: #d00; +} + +#footer { + clear: both; + padding-top: 10px; +} + +#footer center font { + font-size: 7pt; +} + +.pages td a { + color: #34345C; +} + +td.replyhl { + border: 1px solid #BA9DBF; + border-left: none; + border-top: none; + padding: 2px; +} + +td.deletebuttons input.checkbox { + margin: 1px 2px 1px 2px; +} + +div.logo img { + margin: 5px 0px 5px 0px; +} + +.fstitle { + float: left; + width: 25px; +} + +.recaptchatable { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#recaptcha_response_field { + border: 1px solid #AAA !important; +} + +#recaptcha_div { + height: 107px; + width: 442px; +} + +#recaptcha_challenge_field { + width: 400px +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} diff --git a/css/yotsubluemobile.css b/css/yotsubluemobile.css new file mode 100644 index 0000000..5ae2dff --- /dev/null +++ b/css/yotsubluemobile.css @@ -0,0 +1,1030 @@ +/** Here's one for you: internet explorer in wp7 can only handle 1 stylesheet at a time it seems **/ +/** Chrome Mobile seems to dislike media="" in html, so we do it this way instead. **/ +@media only screen and (max-width: 480px) { + /** GENERIC / ELEMENT STYLING **/ + html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + } + + .postMenuBtn { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); + margin: 4px -5px 0px 4px !important; + float: left; + font-weight: bold; + opacity: 1 !important; + height: 0.5em !important; + font-size: 16px; + } + + .dd-menu { + font-size: 16px; + line-height: 2.5em; + } + + .adg-rects > span.mobile { + display: inline-block !important; + } + + .party-hat { + left: -15px; + margin-top: -30px; + position: absolute; + width: 100px; + pointer-events: none; + } + + .pu-img { + background: url('//s.4cdn.org/image/minileaf@2x.gif'); + background-size: 100%; + } + + body { + background: #EEF2FF url(/image/fade-blue.png) top center repeat-x; + + color: #000000; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 0px; + padding-right: 0px; + + width: 100%; + } + + embed { + display: none !important; + } + + .jlist-link { + margin: 15px auto 30px auto; + } + + a, + .useremail:not(:hover) .name:not(.capcode), + .useremail:not(:hover) .postertrip:not(.capcode) { + color: #34345C !important; + } + + a.replylink, div#absbot a { + text-decoration: underline !important; + } + + a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #34345C !important; + } + + a:hover, + .useremail .name:hover, .useremail .postertrip:hover, + a.quoteLink:hover, a.quotelink:hover, a.deadlink:hover, + .useremail *:hover, .useremail:hover * { + color: #DD0000 !important; + } + + .posteruid .hand:hover { + color: #DD0000 !important; + } + + img { + border: none; + } + + img.topad, .topad > div, .topad a img { + width: 300px; + height: 250px; + } + + img.middlead, .middlead > div, .middlead a img { + width: 234px; + height: 30px; + max-width: 100%; + overflow: hidden; + margin: auto; + } + + img.bottomad, .bottomad > div, .bottomad a img { + width: 320px; + height: 40px; + max-width: 100%; + overflow: hidden; + margin: auto; + } + + ul.rules { + display: none; + } + + .button a { + text-decoration: none !important; + } + + div.boardBanner { + margin-top: 40px !important; + } + + .backlink.mobile { + background-color: #C9CDE8; + border-top: 1px solid #B7C5D9; + } + + .mobile { + display: block !important; + clear: left !important; + } + + #quote-preview { + box-shadow: 0px 0px 4px 0 rgba(45, 77, 59, 0.5); + } + + .noPictures .mFileInfo { + display: none !important; + } + .postLink.mobile { + clear: both !important; + } + + .mobileinline { + display: inline !important; + } + + .mobileib { + display: inline-block !important; + } + + span.replyTextM { + display: inline !important; + } + + .desktop { + display: none !important; + } + + .hideMobile { + display: none !important; + } + + div.board > hr { + border: none; + border-top: 1px solid #B7C5D9; + + height: 0; + + margin-top: 30px !important; + margin-bottom: 30px !important; + } + + div.board > hr:last-of-type { + margin-bottom: 10px !important; + } + + hr.abovePostForm { + width: 90%; + } + + span.x-small { + font-size: x-small; + } + + /** Mobile Specific CSS **/ + div.postLink { + background-color: #c9cde8; + border-top: 1px solid #B7C5D9; + padding: 5px; + + overflow: hidden; + } + + div.postLink span { + float: left; + + } + + div.postLink a { + float: right; + + color: #34345C !important; + } + + .mobilePostFormToggle { + text-align: center; + + font-weight: bold; + + margin: 0 auto; + padding-top: 15px; + } + + a.mobilePostFormToggle { + text-align: center; + + display: inline-block !important; + } + + div.mobilePostFormToggle div { + width: 300px; + background-color: #98E; + padding-top: 5px; + padding-bottom: 5px; + border: 1px solid #000; + margin: 0 auto; + } + + div.post div.file .fileThumb img { + max-width: 125px; + max-height: 125px; + object-fit: scale-down; + } + + span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; + } + + div.post div.file .fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; + text-decoration: none; + } + + div.post div.file.image-expanded .fileThumb { + margin-left: 0 !important; + margin-right: 0 !important; + } + + div.post div.file div.fileText { + display: none; + } + + div.sideArrows { + display: none; + } + + div.reply { + padding: 2px; + + background-color: #D6DAF0; + + margin: 0px; + border: 0; + } + + div.replyContainer { + background-color: #D6DAF0; + margin: 5px 0 0; + padding-left: 0; + } + + .is_index div.replyContainer { + margin: 7px 0 0 0; + } + + div.opContainer { + margin: 5px 0 0; + padding-left: 0; + + display: block; + overflow: hidden; + + background-color: #D6DAF0; + + border: none; + } + + span.summary:not(.preview-summary) { + padding: 5px; + text-align: center; + display: block; + + background-color: #c9cde8; + border-bottom: 1px solid #B7C5D9 !important; + + margin-top: 0px; + margin-bottom: 5px; + + font-weight: bold; + } + + div.postLink span.info { + color: #34345C; + margin-top: 8px; + } + + blockquote { + margin: 10px !important; + } + + blockquote.postMessage { + font-size: 11pt; + } + + .omittedposts, .abbr { + color: #707070 !important; + font-size: 10pt !important; + } + + div.thread { + border-top: none; + } + + div.opContainer div.postInfo { + display: none !important; + } + + div.post div.postInfoM { + overflow: hidden; + + border-bottom: 1px solid #B7C5D9; + background-color: #c9cde8; + + padding: 5px; + } + + div.replyContainer div.postInfoM { + margin: 0px; + } + + div.post div.postInfoM span.postNum, div.postInfo span.postNum { + float: left; + } + + div.post div.postInfoM span.postNum a, div.postInfo span.postNum a { + text-decoration: none; + color: #000000 !important; + } + + div.post div.postInfoM span.postNum a:hover { + color: red; + } + + /* Name */ + div.post div.postInfoM span.nameBlock, div.postInfo span.nameBlock { + display: inline; + float: left; + + clear: left; + } + + div.post div.postInfoM span.nameBlock span.name, div.postInfo span.nameBlock span.name { + color: #117743; + font-weight: bold; + } + + div.post div.postInfoM span.nameBlock span.tripcode, div.postInfo span.nameBlock span.tripcode { + color: #117743; + } + + /* Date/Time */ + div.post div.postInfoM span.dateTime { + float: right; + /*font-style: italic;*/ + text-align: right; + color: black !important; + } + + div.post div.postInfoM span.time { + + } + + div.post div.postInfoM span.subject { + color: #0F0C5D; + font-weight: bold; + } + + span.fileText { + font-size: smaller; + } + + div.replyContainer div.reply { + width: 100%; + + padding: 0px !important; + + } + + div.replyContainer div.post div.postInfo { + overflow: hidden; + + border-bottom: 1px solid #B7C5D9; + background-color: #c9cde8; + + padding: 3px; + + margin: 0px; + } + + div.replyContainer div.post div.postInfo input[type=checkbox] { + display: none; + } + + div.replyContainer div.post div.postInfo span.postNum { + + font-style: italic; + } + + div.replyContainer div.post div.postInfo span.userInfo { + float: left; + padding-left: 5px; + } + + div.replyContainer div.post div.postInfo span.nameBlock { + + } + + div.replyContainer div.post div.postInfo span.postNum a:first-child:after { + content: " "; + } + + div.replyContainer div.post div.postInfo span.dateTime { + float: right; + text-align: right; + + padding-right: 10px; + + font-style: italic; + } + + div.replyContainer div.post div.postInfo span.dateTime span.date { + display: block; + } + + div.thread > div:nth-of-type(2) > div.reply { + margin-top: 0px !important; + } + + div.replyContainer div.post div.fileInfo { + margin-left: 0px; + } + + div.replyContainer div.post div.file:not(.image-expanded) { + padding: 5px; + } + + div.mPagelist { + margin-top: 10px; + text-align: center; + + border-bottom: 1px solid #B7C5D9; + + padding-bottom: 10px; + + color: #89A; + + } + + div.mPagelist strong { + color: #34345C; + } + + div.mPagelist > div.prev, div.mPagelist div.next { + margin: 20px 2px 15px; + display: inline-block; + } + + .button { + border-radius: 3px; + padding: 6px 10px 5px 10px; + font-weight: bold; + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + user-select: none; + + background-image: url(/image/buttonfade-blue.png); + background-repeat: repeat-x; + + text-decoration: none; + color: #34345C !important; + } + + #globalToggle { + width: 200px; + display: inline; + text-align: center; + + margin: 0 auto; + margin-bottom: 10px; + } + + .redButton { + background-color: #ffadad; + background-image: url(/image/buttonfade-red.png); + border: 1px solid #C45858; + + color: #800 !important; + } + + div.mPagelist span { + padding-left: 3px; + padding-right: 3px; + + font-size: larger; + } + + .button:hover { + cursor: pointer; + } + + .mobileCatalogLink, + div.absBotText { + margin-top: 10px; + } + + .mobileCatalogLink { + font-size: larger; + } + + #disable-mobile { + font-size: small !important; + } + + #enable-mobile { + font-size: small !important; + display: none; + } + + .absBotDisclaimer { + display: none !important; + } + + div#boardNavMobile { + padding: 2px 4px; + + background-color: #D6DAF0; + + overflow: hidden; + + border-bottom: 2px solid #B7C5D9; + + position: fixed; + top: 0px; + + width: 100%; + font-size: x-small; + } + + div#boardNavMobile select, div#boardNavMobile option { + font-size: x-small; + } + + div.boardSelect { + float: left; + } + + div.boardSelect > strong { + padding-right: 5px; + } + + div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; + font-size: 7.5pt; + } + + .pageJump a { + text-decoration: none; + padding-right: 5px; + } + + div.postingMode { + background-color: #e04000; + padding: 3px; + text-align: center; + + width: 300px; + margin: 0 auto; + } + + div.navLinks { + margin-top: 9px !important; + margin-bottom: 0px !important; + text-align: center; + } + + /** WP7 ADDITIONS **/ + div.post div.postInfo span.postNum a { + text-decoration: none; + color: #000000; + } + + a.quoteLink { + color: #D00 !important; + } + + div.boardBanner { + text-align: center; + clear: both; + } + + div.boardBanner > img { + border: 1px solid #34345C; + margin: 5px 0px 5px 0px; + height: 50px; + width: 150px; + max-width: 100%; + } + + div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + + margin-top: 0px; + } + + div.boardBanner > div.boardSubtitle { + font-size: x-small; + } + + div.post div.file .fileThumb img { + border: none; + + float: left; + } + + div.post div.file .fileThumb .expanded-thumb { + max-width: 100vw !important; + max-height: none !important; + } + + .expandedWebm { + width: auto !important; + height: auto !important; + margin: 3px 0 5px; + max-width: 100vw !important; + max-height: none !important; + } + + span.subject { + display: block; + } + + hr { + border: none; + border-top: 1px solid #B7C5D9; + + height: 0; + } + + .commentpostername { + font-weight: bold; + } + + .identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; + } + + .stickyIcon { + margin-bottom: -2px; + padding-left: 2px; + height: 16px; + width: 16px; + } + + .archivedIcon, + .closedIcon { + margin-bottom: -2px; + margin-left: -1px; + height: 16px; + width: 16px; + } + + .trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; + } + + .fileDeleted { + height: 13px; + width: 172px; + } + + .fileDeletedRes { + height: 13px; + width: 127px; + } + + .navSmall { + font-size: 90%; + } + + .center { + text-align: center; + } + + .bold { + font-weight: bold; + } + + .smaller { + font-size: smaller; + } + + .password { + font-size: smaller; + } + + .passNotice { + font-size: smaller; + padding-left: 3px; + } + + .qcDiv { + display: none; + } + + .qcImg { + height: 1px; + width: 1px; + border: 0px; + } + + .jpnFlag { + height: 11px; + width: 17px; + } + + .globalMessage { + color: red; + text-align: center; + } + + .highlightPost { + background: #f0d6d6 !important; + } + + span.postertrip { + color: #117743; + font-weight: normal !important; + } + + span.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; + } + + span.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; + } + + .omittedposts, .abbr { + color: #070707; + } + + span.spoiler { + color: #000 !important; + background: #000 !important; + } + + span.spoiler:hover, span.spoiler:focus { + color: #fff !important; + } + + table.exif { + display: none; + min-width: 450px; + } + + table.exif td { + color: #070707; + min-width: 150px; + font-size: 8pt; + } + + table.exif td b { + text-decoration: underline; + } + + #navtopright, #navbotright { + float: right; + } + + .preview { + background-color: #D6DAF0; + + border: 1px solid #B7C5D9 !important; + border-right-width: 2px !important; + border-bottom-width: 2px !important; + } + + div.posthover { + max-width: 400px; + margin-left: 20px; + + } + + div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + + } + + div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; + } + + div.posthover blockquote { + margin: 5px; + + } + + div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + } + + div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; + } + + #settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; + + background-color: #D6DAF0; + } + + div.mPagelist a { + text-decoration: none !important; + } + + .pages a { + bottom: -1px; + position: relative; + } + + img.expandedImg { + max-width: auto !important; + max-height: auto !important; + } + + .prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + + max-width: 300px; + word-wrap: break-word; + } + + td > input[type="text"], td > textarea, td > input[type="password"] { + width: 220px !important; + margin-right: 0 !important; + } + + #postForm { + width: auto; + } + + #postForm input[name="sub"] { + width: 160px !important; + } + + #postForm input[type="submit"] { + width: 60px; + padding: 2px 4px 3px 4px; + margin: 0px; + } + + #postForm input[type="password"] { + width: 70px !important; + } + + td > input { + margin-left: 1px; + } + + tr.mobile { + display: table-row !important; + } + + tr.mobile td { + padding: 5px; + } + + .recaptcha_image_cell { + width: auto !important; + padding: 0 !important; + } + + #recaptcha_table tr > td:last-child { + display: none; + } + + #recaptcha_table tr[height="73"] { + height: auto !important; + } + + #recaptcha_table tr > td { + padding: 0 !important; + } + + #recaptcha_image { + width: 280px !important; + + } + + .recaptchatable .recaptcha_image_cell center { + + } + + #recaptcha_response_field { + width: 272px !important; + margin-left: 3px; + margin-top: -1px; + + font-size: 10pt !important; + } + + #recaptcha_image > img { + width: 280px !important; + } + +#postForm:not(.hideMobile) { + /*max-width: 280px !important;*/ + overflow: hidden; + margin-top: 20px; + display: table; + } + + form[name="post"] { + margin: auto; + max-width: 100%; + } + + input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; + } + + input:focus, textarea:focus { + border: 1px solid #9988EE !important; + } + + a:hover, .posteruid .hand:hover { + color: #DD0000 !important; + } + + .button a:hover, a.button:hover { + color: #34345C!important; + } + + a.redButton:hover, + a.redButton:focus { + color: #880000 !important; + } + + table#recaptcha_table > tbody > tr:first-child > td:nth-child(2) { + display: none; + } + + .reply:target, .reply:focus, .reply.highlight { + background: #D6BAD0 !important; + border: none !important; + } + + .mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; + } + + a.replylink, div#absbot a { + text-decoration: underline !important; + } + + a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #34345C !important; + } + + .mobileSpoiler { + padding: 2px !important; + } + + #mpostform { + text-align: center; + margin-top: 10px; + } + +} diff --git a/css/yotsubluenew.css b/css/yotsubluenew.css new file mode 100644 index 0000000..78c0a5f --- /dev/null +++ b/css/yotsubluenew.css @@ -0,0 +1,1733 @@ +/** GENERIC / ELEMENT STYLING **/ +html { + -moz-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + background: #EEF2FF url(/image/fade-blue.png) top center repeat-x; + + color: #000000; + font-family: arial, helvetica, sans-serif; + font-size: 10pt; + + margin-left: 0; + margin-right: 0; + margin-top: 5px; + + padding-left: 5px; + padding-right: 5px; +} + +.nwsb { display: none; } + +.adg-rects { + margin: 0; + text-align: center; +} +.adg, .adg-m { overflow: hidden; } +.adg-rep .adg { margin-left: 15px } +.adp-228 { margin: auto; width: 728px; height: 320px; } +.adp-90 { margin: auto; width: 728px; height: 90px; } +.adp-128 { margin: auto; width: 728px; height: 128px; } +.adp-j { margin: auto; width: 728px; height: 102px; } +.adp-50 { margin: auto; width: 320px; height: 50px; } +.adp-250 { margin: auto; width: 300px; height: 250px; } +.adp-row { display: inline-block; margin: 0 10px; } +.adl { font-size: 10px; text-align: center; } +.adl + .ad-bgls, .ad-bgls + .adl { margin-top: 8px; } + +.danbo-slot { width: 728px; height: 90px; margin: 10px auto; overflow: hidden; } +@media only screen and (max-width: 480px) { .danbo-slot { width: 300px; height: 250px; } } + +.bsa-cnt { + margin: 20px 0; + height: 90px; + text-align: center; +} + +#t-root { + overflow: hidden; + box-sizing: border-box; + background: #eee; + border: 1px solid #777; + margin: 2px 0 2px 0; + width: 300px; +} + +@media only screen and (max-width: 640px) { + .bsa-cnt { + height: 250px; + display: flex; + justify-content: center; + align-items: center; + } +} + +.adc-resp { + margin: auto; + width: 728px; + height: 228px; + overflow: hidden; +} + +.adc-resp-bg { + margin: auto; + width: 728px; + height: 112px; + overflow: hidden; +} + +@media only screen and (max-width: 480px) { + .adc-resp { + width: 300px; + height: 300px; + } + + #quickReply { + z-index: 9000 !important; + } + + .adc-resp-bg { + width: 300px; + height: 250px; + } +} + +hr#op, #ctrl-top { clear: both; } + +.party-hat { + left: 0; + margin-top: -80px; + position: absolute; + pointer-events: none; +} + +.sjis, #quickReply .sjis { + font-size: 16px; + line-height: 17px; + white-space: pre; + font-family: 'IPAMonaPGothic', 'Mona', 'MS PGothic', monospace; + overflow: auto; + display: block; + clear: left; +} + +.tex-logo { font-size: 0.8em; } +.tex-logo sub { font-size: 0.8em; text-transform: uppercase; } + +.mu-s { font-weight: bold } +.mu-i { font-style: italic } +.mu-r { color: #C41E3A } +.mu-g { color: #00A550 } +.mu-b { color: #1d8dc4 } + +.pu-lbl { + color: #117743; + font-weight: bold; + font-size: 12px; +} +.pu-img { + vertical-align: middle; + margin-top: -3px; + margin-right: 3px; + display: inline-block; + width: 16px; + height: 16px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.n-pu { + vertical-align: middle; + display: inline-block; + width: 16px; + height: 16px; + margin-top: -2px; + background: url('//s.4cdn.org/image/minileaf.gif'); +} + +.jla-it { + height: 420px; + width: 680px; +} + +.jla-it-p { + font-size: 0.85em; + text-align: center; +} + +.n-atb { + border-radius: 6px; + padding: 1px 4px; + color: #353839; +} + +.atsb2018 table { + margin: auto; + font-size: 90%; +} +.atsb2018 td { + white-space: nowrap; + padding: 0px 4px; +} +.atsb2018 .atsgc { + width: 280px; +} +.atsb2018 .atgg { + height: 20px; +} + +.n-atb-0 { background-color: #F56FA1; } +.n-atb-1 { background-color: #7B3F00; color: #F2F3F4; } +.n-atb-2 { background-color: #FFFDD0; } +.n-atb-3 { background-color: #E4D00A; } +.n-atb-4 { background-color: #50C878; } + +.n-atb-0::after { content: 'Team Peep'; } +.n-atb-1::after { content: 'Team Chocolate'; } +.n-atb-2::after { content: 'Team Creme'; } +.n-atb-3::after { content: 'Team Peanut Butter'; } +.n-atb-4::after { content: 'Team Mini'; } + +@media only screen and (max-width: 700px) { + .jla-it { + width: 340px; + height: 220px; + margin: auto; + } + + .jla-it img { + width: 340px; + height: 200px; + } +} + +.centeredThreads .party-hat { + left: 12%; +} + +.aplnk, .linkified { text-decoration: underline } + +#disclaimer { + font-size: 14px; + position: absolute; + overflow: hidden; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 9998; +} + +#disclaimer a { + color: #0000ff; +} + +#disclaimer-bg { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background-color: rgba(0, 0, 0, 0.25); +} + +#disclaimer-modal { + z-index: 9999; + width: 320px; + top: 0; + left: 50%; + margin-left: -170px; + display: block; + padding: 10px; + position: relative; + background-color: #d6daf0; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); +} + +#disclaimer ol { + margin-left: 20px; + padding: 0; +} + +#disclaimer li { + margin: 10px 0; +} + +#disclaimer h3 { + border-bottom: 1px solid #b7c5d9; + margin: 0; + padding-bottom: 5px; + text-align: center; +} + +#disclaimer-modal div { + margin-top: 10px; + text-align: center; +} + +#disclaimer-modal button { + margin: 0 10px; +} + +#disclaimer-accept { + font-weight: bold; +} + +#footer-links { margin-top: 5px; } + +.isMobileDevice blockquote.postMessage { + font-size: 11pt; +} + +.belowLeaderboard { + width: 728px; + max-width: 100%; +} + +.aboveMidAd { + width: 468px; + max-width: 100%; +} + +/* reCaptcha */ +#captchaContainerAlt { height: 80px; } +#captchaContainerAlt .recaptcha_image_cell { padding: 0 0 5px 0 !important } +#captchaContainerAlt #recaptcha_table { border: 0 !important; } +#captchaContainerAlt #recaptcha_response_field { width: 300px !important; padding: 0 !important } + +#qrCaptchaContainerAlt #recaptcha_response_field { + width: 296px !important; +} + +#captchaContainerAlt #recaptcha_image, +#qrCaptchaContainerAlt #recaptcha_image { + border: 0 !important; +} + +#qrCaptchaContainerAlt .recaptcha_image_cell { + padding: 0 !important; +} + +#qrCaptchaContainerAlt #recaptcha_table { + border: 0 !important; +} + +#captchaContainerAlt td:nth-child(2), +#captchaContainerAlt td:nth-child(3), +#qrCaptchaContainerAlt td:nth-child(2), +#qrCaptchaContainerAlt td:nth-child(3) { + display: none !important; +} + +#qrCaptchaContainerAlt { + height: 80px; + width: 300px; +} + +#recaptcha_table { + background-color: transparent !important; + border: none !important; +} + +.recaptcha_image_cell { + background-color: transparent !important; +} + +#g-recaptcha { + height: 78px; +} + +#recaptcha_challenge_field { width: 400px } + +.recaptcha_input_area { + padding: 0!important; +} +#recaptcha_table tr:first-child { + height: auto!important; +} + +#recaptcha_table tr:first-child > td:not(:first-child) { + padding: 0 7px 0 7px!important; +} + +#recaptcha_table tr:last-child td:last-child { + padding-bottom: 0!important; +} + +#recaptcha_table tr:last-child td:first-child { + padding-left: 0!important; +} +#recaptcha_image { + cursor: pointer; +} +#recaptcha_response_field { + width: 292px; + margin-right: 0px!important; + font-size: 10pt!important; +} +input:-moz-placeholder { color: gray !important; } +#recaptcha_image { + border: 1px solid #aaa !important; +} +#recaptcha_table tr > td:last-child { + display: none !important; +} +#captchaContainer { + width: 343px; + height: 86px; + line-height: 99px; + overflow: hidden; +} +#captchaContainer .placeholder { + font-style: italic; + padding-left: 5px; +} + +.mobile, .mobileinline, .mobileib { + display: none !important; +} + +a, a:visited { + color: #34345C; + text-decoration: none; +} + +a.replylink:not(:hover), div#absbot a:not(:hover) { + color: #34345C !important; +} + +a:hover { + color: #DD0000 !important; +} + +div#absbot { + color: #000000; + clear: both; +} + +img { + border: none; +} + +img.topad, .topad > div, .topad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.middlead, .middlead > div, .middlead a img { + width: 468px; + height: 60px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +img.bottomad, .bottomad > div, .bottomad a img { + width: 728px; + height: 90px; + max-width: 100%; + overflow: hidden; + margin: auto; +} + +hr { + border: none; + border-top: 1px solid #B7C5D9; + + height: 0; +} + +div.board > hr { + clear: both; +} + +hr.abovePostForm { + width: 90%; +} + +span.x-small { + font-size: x-small; +} + +div.backlink { + font-size: x-small !important; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} + +.backlink span { + padding-right: 5px; +} + +#postFormError { + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; + font-weight: bold; + padding: 3px 5px; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + display: none; + margin-top: 3px; +} + +#postFormError a { + color: #fff; +} + +.painter-ctrl input[type="text"], +#qr-painter-ctrl input[type="text"] { + width: 30px !important; + text-align: center; +} + +#qr-painter-ctrl .oe-r-cb { + vertical-align: sub; +} + +#oe-canvas-preview { + position: absolute; + margin-left: 5px; + margin-top: 3px; + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25); +} + +/** MOBILE ONLY DISABLES **/ +.mobile { + display: none; +} + +/** HEADER **/ +ul.rules { + margin: 0px; + padding: 0px; + margin-top: 5px; +} + +ul.rules > li { + list-style: none; + font-size: 11px; +} + +.rules > li:before { + content: "\2022 \20"; +} + +div.boardBanner { + text-align: center; + clear: both; + + color: #AF0A0F; +} + +#bannerCnt { + border: 1px solid #34345C; + margin: 5px auto; + width: 300px; + height: 100px; + max-width: 100%; +} + +div.boardBanner > div.boardTitle { + font-family: Tahoma, sans-serif; + font-size: 28px; + font-weight: bold; + letter-spacing: -2px; + + margin-top: 0px; +} + +div.boardBanner > div.boardSubtitle { + font-size: x-small; +} + +div#boardNavDesktop { + font-size: 9pt; + color: #89A; + display: block; +} + +.hasDropDownNav #navtopright { + display: none; +} + +#boardNavDesktop .pageJump { + padding: 0; +} + +#boardNavDesktop .pageJump a { + padding-right: 5px; +} + +div#boardNavDesktop a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #34345C; +} + +/** General Containers **/ +div.pContainer { + +} + +div.opContainer { + display: inline; +} + +div.sideArrows { + color: #B7C5D9; + + float: left; + + margin-right: 2px; + margin-top: 0px; + margin-left: 2px; +} + +/** Thread Container **/ +div.thread { + + margin: 0px; + + clear: both; + +} + +/** Post Container **/ +div.post { + margin: 4px 0; + overflow: hidden; +} + +div.thread > div:nth-of-type(2) > div.reply { + margin-top: 2px !important; +} + +div.op { + display: inline; +} + +div.reply { + background-color: #D6DAF0; + + border: 1px solid #B7C5D9; + border-left: none; + border-top: none; + + display: table; + + padding: 2px; +} + +div.reply input { + float: none; +} + +/** Post Information **/ +div.post div.postInfo { + display: block; + width: 100%; +} + +.fileText { + max-width: 600px; + white-space: nowrap; +} + +div.post div.postInfo span.postNum { + +} + +div.post div.postInfo span.postNum a { + text-decoration: none; + color: #000000; +} + +div.post div.postInfo span.postNum a:hover, .posteruid .hand:hover { + color: #DD0000 !important; +} + +/* Name */ +div.post div.postInfo span.nameBlock { + display: inline-block; +} + +div.post div.postInfo span.nameBlock span.name { + color: #117743; + font-weight: bold; +} + +div.post div.postInfo span.nameBlock span.postertrip { + color: #117743; + font-weight: normal !important; +} + +/* Date/Time */ +div.post div.postInfo span.date { + +} + +div.post div.postInfo span.time { + +} + +/* Subject */ +div.post div.postInfo span.subject { + color: #0F0C5D; + font-weight: bold; +} + +/** Message **/ +div.post blockquote.postMessage { + display: block; +} + +blockquote > span.quote { + color: #789922; +} + +.quoteLink, .quotelink, .deadlink { + color: #D00 !important; + text-decoration: underline; +} + +/* Archived threads list */ +.is_arclist .belowLeaderboard { display: none; } + +#arc-list { + max-width: 80%; + margin: 10px auto 0 auto; +} + +#arc-list td { + text-align: center; +} + +#arc-list .teaser-col { + text-align: left; + word-break: break-all; +} + +#arc-list .quotelink { + color: #34345C !important; +} + +#arc-list .quotelink:hover { + color: #D00 !important; +} + +#arc-sort { + border-bottom: 1px dotted; + cursor: pointer; +} + +/** File Information **/ +div.post div.file { + display: block; +} + +div.post div.file div.fileInfo { + margin-right: 10px; + word-break: break-all; +} + +div.replyContainer div.post div.file div.fileInfo { + margin-left: 20px; +} + +div.post div.file .fileThumb { + float: left; + margin-left: 20px; + margin-right: 20px; + margin-top: 3px; + margin-bottom: 5px; +} + +span.fileThumb { + margin-left: 0px !important; + margin-right: 5px !important; +} + +div.reply span.fileThumb, div.reply span.fileThumb img { + float: none !important; + margin-top: 0px !important; + margin-bottom: 0px !important; +} + +div.post div.file .fileThumb img { + border: none; + + float: left; +} + +/** Summary **/ +span.summary { + color: #707070; + margin-top: 10px; +} + +/** POST FORM **/ +div.postingMode { + background-color: #e04000; + padding: 1px; + text-align: center; + + color: #fff; + font-weight: bold; + font-size: larger; + + margin-top: 8px; +} + +#verification table { + border: none !important; + margin: 0px; +} + +/** FOOTER **/ +div.thread:last-child { + padding-bottom: 21px; + margin-bottom: 6px; + +} + +div.pagelist { + font-size: 13px !important; + + margin: 0; + padding: 3px 7px; + + float: left; + + border: none; + background: #D6DAF0; + + border-right: 1px solid #B7C5D9; + border-bottom: 1px solid #B7C5D9; + + list-style: none; + overflow: hidden; + + color: #89A; +} + +div.pagelist > div { + float: left; +} + +div.pagelist > div span { + padding: 4px; + display: inline-block; +} + +div.pagelist div.pages { + padding: 4px; +} + +div.pagelist div.pages a { + text-decoration: none !important; + /* FINE MOOT GOSH JEEZ */ +} + +div.pagelist form { + display: inline; +} + +div.pagelist strong { + color: #000000; +} + +div.pagelist div.cataloglink { + border-left: 1px solid #B7C5D9; + padding-left: 12px; + margin-left: 7px; +} + +.bottomCtrl { + float: right; + margin-top: 2px; +} + +input[type=password] { + width: 50px; + text-align: center; +} + +div.deleteform input[type=checkbox] { + margin: 1px 2px 1px 2px; +} + +.stylechanger { + margin-left: 5px; + font-size: 10pt; +} + +div#boardNavDesktopFoot { + font-size: 9pt; + color: #89A; + + clear: both; + + padding-top: 10px; + padding-bottom: 3px; +} + +div#boardNavDesktopFoot a { + font-weight: normal; + padding: 1px; + text-decoration: none; + color: #34345C; +} + +div.homelink { + float: right +} + +div#absbot { + text-align: center; + font-size: x-small !important; + + padding-bottom: 4px; + padding-top: 10px; + + color: #000; +} + +#recaptcha_response_field { + padding: 0px; +} + +/** POST FORM **/ +table { + border-spacing: 1px; + + margin-left: auto; + margin-right: auto; +} + +table.postForm > tbody > tr > td:first-child { + background-color: #98E; + color: #000; + font-weight: bold; + border: 1px solid #000; + padding: 0 5px; + + font-size: 10pt; +} + +tr.rules td { + border: 0px !important; + background-color: transparent !important; + font-weight: normal !important; +} + +td { + margin: 0px; + padding: 0px; + + font-size: 10pt; +} + +input[type=text], input[type=password], table.postForm > tbody textarea, #recaptcha_response_field { + margin: 0px; + margin-right: 2px; + padding: 2px 4px 3px 4px; + + border: 1px solid #AAA; + outline: none; + + font-family: arial, helvetica, sans-serif; + font-size: 10pt; +} + +#recaptcha_response_field:not(:focus) { + border: 1px solid #AAA !important; +} + +input[type=text]:focus, input[type=password]:focus, input:not([type]):focus, textarea:focus { + border: 1px solid #9988EE !important; +} + +table.postForm > tbody > tr > td > input[type=text] { + width: 244px; +} + +table.postForm > tbody > tr > td > input[name="subject"] { + width: 300px; +} + +.postblock { + background-color: #98E; + color: #000; + font-weight: bold; + border: 1px solid #000; + padding: 0 5px; + font-size: 10pt; +} + +div.closed { + font-size: x-large; + text-align: center; + color: red; + font-weight: bold; + + padding-top: 100px; + padding-bottom: 100px; +} + +.arc-ads { + width: 300px; + margin: 30px auto -60px auto; +} + +.arc-ads::after { + display: block; + content: ''; + clear: both; +} + +@media screen and (-webkit-min-device-pixel-ratio:0) { + tbody textarea { + margin-bottom: -3px !important; + width: 292px + } +} + +.commentpostername { + font-weight: bold; +} + +.identityIcon { + margin-bottom: -3px; + height: 16px; + width: 16px; +} + +.stickyIcon { + margin-bottom: -1px; + padding-left: 2px; + height: 16px; + width: 16px; +} + +.archivedIcon, +.closedIcon { + margin-bottom: -1px; + margin-left: -1px; + height: 16px; + width: 16px; +} + +.trashIcon { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.fileDeleted { + height: 13px; + width: 172px; +} + +.fileDeletedRes { + height: 13px; + width: 127px; +} + +.navSmall { + font-size: 90%; +} + +.center { + text-align: center; +} + +.bold { + font-weight: bold; +} + +.smaller { + font-size: smaller; +} + +.password { + font-size: smaller; +} + +.passNotice { + font-size: smaller; + padding-left: 6px; +} + +.qcDiv { + display: none; +} + +.qcImg { + height: 1px; + width: 1px; + border: 0px; +} + +.jpnFlag { + height: 11px; + width: 17px; +} + +.globalMessage { + color: red; + text-align: center; +} + +.highlightPost:not(.op) { + background: #c1c6e2 !important; + border-color: #7b82ac !important; +} + +.reply:target, .reply.highlight { + background: #D6BAD0 !important; + border: 1px solid #BA9DBF !important; + border-left: none !important; + border-top: none !important; + padding: 2px; +} + +.useremail:not(:hover) .name:not(.capcode), .useremail:not(:hover) .postertrip:not(.capcode) { + color: #34345C!important; +} + +.hand { + cursor: pointer; +} + +.nameBlock.capcodeAdmin span.name, span.capcodeAdmin a span.name, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode { + color: #F00 !important; +} + +span.capcodeFounder span.name, span.capcodeFounder span.name a, span.capcodeFounder span.postertrip, span.capcodeFounder strong.capcode { + color: #117743 !important; +} + +.nameBlock.capcodeMod span.name, span.capcodeMod a span.name, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode { + color: #800080 !important; +} + +.nameBlock.capcodeDeveloper span.name, span.capcodeDeveloper a span.name, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode { + color: #0000F0 !important; +} + +.nameBlock.capcodeManager span.name, span.capcodeManager a span.name, span.capcodeManager span.postertrip, span.capcodeManager strong.capcode { + color: #FF0080 !important; +} + +.nameBlock.capcodeVerified span.name, span.capcodeVerified span.name a, span.capcodeVerified span.postertrip, span.capcodeVerified strong.capcode { + color: #007FFF !important; +} + +.useremail *:hover, .useremail:hover * { + color: #DD0000!important; +} + +#reportTypes a, +.useremail { + text-decoration: underline; +} + +.omittedposts, .abbr { + color: #707070; +} + +span.spoiler { + color: #000 !important; + background: #000 !important; +} + +span.spoiler:hover, span.spoiler:focus { + color: #fff !important; +} + +s, s a:not(:hover) { + color: #000 !important; + background: #000 !important; + text-decoration: none; +} + +s:hover, s:focus, s:hover a { + color: #fff !important; +} + +s:hover a { + text-decoration: underline; +} + +table.exif { + display: none; + min-width: 450px; +} + +table.exif td { + color: #070707; + min-width: 150px; + font-size: 8pt; +} + +table.exif td b { + text-decoration: underline; +} + +#navtopright, #navbotright { + float: right; +} + +.preview { + background-color: #D6DAF0; + + border: 1px solid #B7C5D9 !important; + border-right-width: 2px !important; + border-bottom-width: 2px !important; +} + +div.posthover { + max-width: 400px; + margin-left: 20px; + +} + +div.posthover { + padding: 5px; + + padding-left: 10px; + padding-right: 10px; + +} + +div.posthover a.fileThumb { + margin-left: 5px !important; + margin-right: 10px !important; +} + +div.posthover blockquote { + margin: 5px; + +} + +div.posthover img[data-md5] { + max-width: 80px; + max-height: 80px; + + width: auto !important; + height: auto !important; +} + +div.posthover div.fileThumb { + margin-left: 0px !important; + margin-right: 10px !important; +} + +#settingsBox { + position: absolute; + + right: 10px; + margin-top: 10px; +} + +.persistentNav, +div#boardNavMobile { + padding: 2px 4px; + background-color: #D6DAF0; + overflow: hidden; + border-bottom: 2px solid #B7C5D9; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + font-size: 12px; + z-index: 9001; +} + +div#boardNavMobile select, div#boardNavMobile option { + font-size: 11px; +} + +div.boardSelect { + float: left; +} + +div.boardSelect > strong { + padding-right: 5px; +} + +div.pageJump { + float: right; + padding-right: 5px; + padding-top: 3px; +} + +.pageJump a { + text-decoration: none; + padding-right: 5px; +} + +/** QUICK REPLY **/ +div.qrWindow { + position: absolute; + z-index: 8000; +} + +div.qrHeader { + padding: 2px; + font-size: small; + text-align: center; + +} + +div.qrForm { + padding: 3px; +} + +span.qrButtonHolder { + position: absolute; + right: 5px; + text-align: right; + + top: 3px; +} + +span.qrButtonHolder a { + text-decoration: none; +} + +span.qrButtonHolder img { + cursor: pointer; + + margin-bottom: -1px; + margin-top: 1px; +} + +.extButton img { + margin-top: 3px; + margin-bottom: -3px; + margin-left: 4px; +} + +.qrMessage { + padding: 2px; + text-align: center; +} + +.op .backlinkHr { + width: 55%; +} + +img.expandedImg { + max-width: none !important; + max-height: none !important; +} + +/** this is not important **/ +#captchaContainer > img { + float: left; + border: 1px solid #aaa; + margin-bottom: 1px; +} + +#captchaInfo { + float: left; + margin-left: 5px; + + visibility: hidden; +} + +#captchaResponse { + width: 292px; +} + +.prettyprint { + border: none !important; + background-color: #fff; + padding: 5px !important; + display: inline-block; + + max-height: 400px; + overflow-x: auto; + + max-width: 600px; + + margin: 0; +} + +.embed { + position: absolute; + width: 0px; + height: 0px; + + overflow: hidden; +} + +table.flashListing td.postblock { + padding: 5px; + text-align: center; +} + +table.flashListing td { + padding: 2px; + font-size: 9pt; +} + +table.flashListing td:not(.subject) { + text-align: center; +} + +table.flashListing .name { + color: #117743; + font-weight: bold; +} + +table.flashListing .postertrip { + color: #117743; +} + +table.flashListing .subject { + color: #cc1105; + font-weight: bold; +} + +table.flashListing tr:nth-of-type(odd) { + background-color: #e0e5f6; +} + +input[type="text"], input[type="password"], textarea { + -webkit-appearance: none; + -webkit-border-radius: 0; +} + +.countryFlag { + padding-top: 1px; + margin-bottom: -1px; +} + +.deadlink { + text-decoration: line-through; +} + +.oldpost { + background: inherit; + color: #0F0C5D; + font-weight: 800; +} + +#enable-mobile { + font-size: small !important; +} + +#disable-mobile { + font-size: small !important; +} + +.mFileInfo { + padding-top: 5px; + text-align: center; + color: #707070 !important; + font-size: 9pt !important; + text-decoration: none!important; +} + +.name-col, +.file-col, +table.flashListing .subject { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: break-word; +} + +.ad-plea { + margin-top: 2px; + text-align: center; + font-size: smaller; +} + +.ad-plea a { + text-decoration: none; +} + +.fileText a { + text-decoration: underline; +} + +#search-box { + height: 16px; + line-height: 16px; + margin-left: 2px; + padding: 0 2px; + width: 120px; +} + +#blotter { + width: 468px; + margin: auto; +} +#blotter td { + vertical-align: top; + font-size: 11px; +} +.blotter-date { + width: 50px; + text-align: center; +} +#blotter tfoot { + text-align: right; +} +.redtxt { + color: red; +} +#blotter-msgs s { + background-color: inherit !important; + color: inherit !important; + text-decoration: line-through; +} +#postForm textarea { + width: 292px; +} + +#postForm { + width: 468px; + display: none; +} + +#togglePostFormLink { + font-size: 22px; + font-weight: bold; + text-align: center; +} + +.fileWebm:hover:before { + background-color: rgba(0, 0, 0, 0.75); + color: #FFF; + font-weight: bold; + line-height: 18px; + padding: 0 3px 0 2px; + position: absolute; + content: 'webm'; + display: block; + font-size: 11px; + text-decoration: none; +} + +.expandedWebm { + margin: 3px 20px 5px; +} + +#tooltip { + position: absolute; + background-color: #181f24; + font-size: 11px; + line-height: 13px; + padding: 3px 6px; + z-index: 100000; + word-wrap: break-word; + white-space: pre-line; + max-width: 400px; + color: #fff; + text-align: center; +} + +.tip-top-left:before, +.tip-top-right:before, +.tip-top:before { + content:""; + display:block; + width:0; + height:0; + position:absolute; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #181f24; + margin-left: -4px; + bottom: -4px; +} + +.tip-top:before { + left: 50%; +} + +.tip-top-right:before { + left: 2px; + margin-left: 0; +} + +.tip-top-left:before { + right: 2px; +} + +.boardSelect .customBoardList, +.boardSelect .custom-menu-ctrl { color: #89A; } + +#postFile { + margin-right: 10px; + width: 200px; +} + +.dd-menu { + position: absolute; + font-size: 12px; + line-height: 1.3em; +} +.dd-menu a { + text-decoration: none; + color: inherit !important; + display: block; +} +.dd-menu ul { + background-color: #D6DAF0; + border: 1px solid #B7C5D9; + border-right-width: 2px; + list-style: none; + padding: 0; + margin: 0; + white-space: nowrap; +} +.dd-menu ul ul { + display: none; + position: absolute; +} +.dd-menu li { + cursor: pointer; + position: relative; + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #B7C5D9; +} +.dd-menu li:hover { + background-color: #EEF2FF; +} +.dd-menu li:hover ul { + display: block; + left: 100%; + margin-top: -3px; +} +.dd-menu.dd-menu-left li:hover ul { + left: auto; + right: 100%; +} + +#g-search-form { + text-align: center; +} +.g-search-ctrl { + vertical-align: top; + margin: 0 3px; +} +#js-sf-qf { + width: 185px; + padding: 2px 4px 3px 4px; +} +#js-sf-bf { + padding: 2px 4px 3px 4px; + width: 125px; +} +#js-sf-status { + text-align: center; + font-size: 24px; +} +.js-sf-err { + color: #C41E3A; +} +.boardBlock { + font-weight: bold; +} + +.blink { + -webkit-animation: blink 1.5s step-end infinite; + -moz-animation: blink 1.5s step-end infinite; + -o-animation: blink 1.5s step-end infinite; + animation: blink 1.5s step-end infinite; +} + +@-webkit-keyframes blink { 50% { opacity: 0 } } +@-moz-keyframes blink { 50% { opacity: 0 } } +@-o-keyframes blink { 50% { opacity: 0 } } +@keyframes blink { 50% { opacity: 0 } } + +.ogv-cnt { + position: relative; + display: inline-block; + text-align: center; + min-height: 16px; +} + +.ogv-cnt:not(.ogv-detached)::before { + content: 'Loading…'; + position: absolute; + top: 8px; + left: 0; + font-weight: bold; +} + +.ogv-cnt.ogv-loaded::before { + display: none; +} + +.ogv-cnt > ogvjs { + position: initial !important; +} + +.ogv-ctrl { + position: absolute; + bottom: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.65); + width: 100%; + height: 32px; + gap: 0; + display: none; +} + +.ogv-btn { + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + color: white; +} + +.ogv-btn svg { + vertical-align: middle; +} + +.ogv-btn svg:last-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:first-child { + display: none; +} + +.ogv-btn.ogv-toggled svg:last-child { + display: inline; +} + +.ogv-ts { + font-family: sans-serif; + font-size: 10px; + text-align: center; + line-height: 32px; + width: 70px; + color: white; + overflow: hidden; +} + +.ogv-vol { + width: 50px; + margin: 0; +} + +.ogv-seek { + width: 0; + margin: 0; + flex-grow: 1; +} diff --git a/derefer.php b/derefer.php new file mode 100644 index 0000000..46c741a --- /dev/null +++ b/derefer.php @@ -0,0 +1,48 @@ + + + + + Redirecting... + + + + +
    Redirecting you to ...
    + + diff --git a/emotes_xa22.php b/emotes_xa22.php new file mode 100644 index 0000000..5244233 --- /dev/null +++ b/emotes_xa22.php @@ -0,0 +1,665 @@ + [ 2, '03d18964', 27, 32 ], + 'Aquacry' => [ 2, '83cf2699', 30, 30 ], + 'AWOOOO' => [ 2, 'e2ad2cb7', 28, 32 ], + 'AYAYA' => [ 2, 'f93f9e5a', 32, 31 ], + 'AYAYAHyper' => [ 2, 'bc1ff2b8', 32, 32 ], + 'BOOBA' => [ 2, 'fb56168f', 32, 32 ], + 'BOOMER' => [ 2, '51ca59c2', 32, 32 ], + 'Bruh' => [ 2, '67905c4f', 32, 32 ], + 'Catcry' => [ 2, '967f06c9', 28, 28 ], + 'ChadYes' => [ 2, 'c7375c9d', 31, 32 ], + 'COPIUM' => [ 2, '4dfb5c71', 32, 31 ], + 'DontBully' => [ 2, '199f7d0e', 31, 32 ], + 'EZY' => [ 2, 'adf2d2f0', 28, 26 ], + 'FeelsBadMan' => [ 2, '59b6bba6', 30, 29 ], + 'FeelsGoodMan' => [ 2, 'cda7b2fb', 32, 24 ], + 'FeelsOkayMan' => [ 2, '08b66b75', 28, 27 ], + 'FeelsSpecialMan' => [ 2, '25086889', 28, 25 ], + 'FeelsStrongMan' => [ 2, '0ee6ba1c', 32, 31 ], + 'FeelsWeirdMan' => [ 2, 'ad2977e6', 28, 27 ], + 'gachiGASM' => [ 2, 'c291d202', 24, 28 ], + 'gachiHYPER' => [ 2, '634a21ba', 24, 28 ], + 'Gigachad' => [ 2, '7a95728b', 29, 32 ], + 'GoodNight' => [ 2, 'a6d16707', 31, 32 ], + 'Hahaa' => [ 2, 'af528e56', 28, 28 ], + 'HeavyBreathing' => [ 2, '4623886c', 32, 32 ], + 'KannaNom' => [ 2, '5de4addd', 32, 32 ], + 'KannaPolice' => [ 2, '98cf0be7', 32, 32 ], + 'KEKW' => [ 2, 'e54792d7', 32, 32 ], + 'KEKWait' => [ 2, 'c2cfb2e3', 32, 32 ], + 'MarisaFace' => [ 2, '857a9ea0', 24, 24 ], + 'MeguminHappy' => [ 2, 'a19762fc', 32, 32 ], + 'MikuStare' => [ 2, 'b674048b', 32, 32 ], + 'monkaChrist' => [ 2, '48c107b3', 28, 28 ], + 'monkaGIGA' => [ 2, 'de27847b', 28, 28 ], + 'monkaH' => [ 2, 'acb11630', 28, 28 ], + 'monkaHmm' => [ 2, 'd3c674ba', 32, 32 ], + 'monkaMEGA' => [ 2, '0b3318e4', 28, 28 ], + 'monkaOMEGA' => [ 2, 'bb299b4d', 32, 32 ], + 'monkaS' => [ 2, 'ed1cc57f', 28, 28 ], + 'monkaSpeed' => [ 2, '87c89650', 32, 30 ], + 'monkaW' => [ 2, 'b05923f5', 32, 32 ], + 'nepSmug' => [ 2, '57b01648', 32, 32 ], + 'OMEGALUL' => [ 2, 'c4035570', 31, 32 ], + 'peepoBlanket' => [ 2, '099390a2', 31, 32 ], + 'peepoClown' => [ 2, '8ea2d160', 32, 32 ], + 'peepoHappy' => [ 2, '68104e2a', 28, 20 ], + 'peepoWTF' => [ 2, '3d8675e9', 28, 19 ], + 'Pepega' => [ 2, '1ee7c5a1', 32, 25 ], + 'PepeHands' => [ 2, 'f2ecf801', 32, 32 ], + 'PepeLaugh' => [ 2, '51cbf903', 30, 29 ], + 'PepeLmao' => [ 2, '25908e08', 28, 28 ], + 'pepePoint' => [ 2, '90786369', 32, 32 ], + 'PepoG' => [ 2, '4459d60b', 32, 26 ], + 'pepoRope' => [ 2, '6ec0dd2c', 32, 31 ], + 'PepoThink' => [ 2, '9ecd704b', 32, 31 ], + 'pikachuS' => [ 2, '42faedcc', 32, 32 ], + 'PillowNo' => [ 2, '1e4d8dfa', 32, 32 ], + 'PillowYes' => [ 2, '6f5bc7e5', 32, 32 ], + 'Pog' => [ 2, 'fad6951c', 28, 28 ], + 'POGGERS' => [ 2, '6f0d4e37', 32, 32 ], + 'PressF' => [ 2, 'f0a256b9', 32, 30 ], + 'REEeee' => [ 2, 'b06b1566', 32, 32 ], + 'REEEEE' => [ 2, 'ba70c4d9', 32, 28 ], + 'ReimuGlare' => [ 2, 'bdf28159', 32, 32 ], + 'ReimuPalm' => [ 2, '41a37aa0', 32, 32 ], + 'SadCatW' => [ 2, 'd8f61d71', 32, 32 ], + 'Sadge' => [ 2, 'e024965e', 28, 22 ], + 'SeetheWojak' => [ 2, 'a9f848d3', 28, 32 ], + 'Stonks' => [ 2, '53478ca5', 31, 32 ], + 'ThisIsFine' => [ 2, 'd9bf8456', 28, 31 ], + 'Thonk' => [ 2, 'ec538b5c', 32, 27 ], + 'TooLewd' => [ 2, 'ddc55766', 32, 32 ], + 'Tuturu' => [ 2, 'c72e8e84', 32, 32 ], + 'umaruCry' => [ 2, '7242c342', 28, 28 ], + 'WanWan' => [ 2, '8a527ac8', 32, 31 ], + 'WeirdChamp' => [ 2, '3021a426', 31, 32 ], + 'weSmart' => [ 2, '6476e57d', 27, 28 ], + 'wojakNPC' => [ 2, '24edafcc', 32, 32 ], + 'wojakWithered' => [ 2, 'ed7d4c3a', 32, 30 ], + 'WTFF' => [ 2, '8b7cc3e0', 32, 32 ], + 'YEP' => [ 2, 'e1899bbe', 32, 31 ], + 'YesHoney' => [ 2, '2b414cf1', 31, 25 ], + // 28 emotes + 'bane' => [ 3, 'c458ef22', 32, 32 ], + 'bog' => [ 3, 'c2e2602a', 32, 32 ], + 'cia' => [ 3, 'c69a1ef1', 32, 32 ], + 'cockmongler' => [ 3, 'eda6f332', 22, 32 ], + 'desu' => [ 3, '80692b94', 28, 32 ], + 'desusmirk' => [ 3, '72694e0e', 41, 32 ], + 'frodo' => [ 3, 'e9d526e8', 32, 32 ], + 'goldface' => [ 3, '7081142e', 32, 32 ], + 'happycat' => [ 3, '1d3f2a13', 27, 32 ], + 'happyn' => [ 3, 'afd49202', 25, 32 ], + 'jannydog' => [ 3, 'f0dcbf8a', 35, 32 ], + 'koiwai' => [ 3, '1d7e369a', 44, 32 ], + 'koiwaiwave' => [ 3, '0e313986', 31, 32 ], + 'konata' => [ 3, 'eb07a2c8', 32, 32 ], + 'laughingw' => [ 3, '6e6217c7', 32, 32 ], + 'longcat' => [ 3, '0ee48fb4', 37, 32 ], + 'longcata' => [ 3, '95c37417', 37, 30 ], + 'longcatb' => [ 3, 'e77bc341', 37, 32 ], + 'moetron' => [ 3, 'cf1d4b8d', 32, 32 ], + 'mudkip' => [ 3, 'a4b23eff', 31, 32 ], + 'shoopdw' => [ 3, '11339e7b', 23, 32 ], + 'shoopdw2' => [ 3, '49bde730', 100, 32 ], + 'troll' => [ 3, 'd89a0070', 37, 32 ], + 'trollface' => [ 3, '7b4acfbf', 32, 26 ], + 'yaranaika' => [ 3, 'a6955123', 32, 32 ], + 'yaranaika2' => [ 3, '4d00227b', 32, 32 ], + + // Unicode emojis (type 1): [ type, html entity ] + // 58 emojis + 'happy' => [ 1, '😀' ], + 'grin' => [ 1, '😄' ], + 'xd' => [ 1, '😆' ], + 'grinsweat' => [ 1, '😅' ], + 'rofl' => [ 1, '🤣' ], + 'lmao' => [ 1, '😂' ], + 'smile' => [ 1, '🙂' ], + 'wink' => [ 1, '😉' ], + 'glad' => [ 1, '😊' ], + 'kiss' => [ 1, '😙' ], + 'crazy' => [ 1, '🤪' ], + 'think' => [ 1, '🤔' ], + 'wot' => [ 1, '🤨' ], + 'kay' => [ 1, '😐' ], + 'yikes' => [ 1, '😒' ], + 'eyeroll' => [ 1, '🙄' ], + 'confused' => [ 1, '😕' ], + 'pensive' => [ 1, '😔' ], + 'disgust' => [ 1, '🤢' ], + 'vomit' => [ 1, '🤮' ], + 'dizzy' => [ 1, '😵' ], + 'nerd' => [ 1, '🤓' ], + 'worry' => [ 1, '😟' ], + 'sad' => [ 1, '🙁' ], + 'frown' => [ 1, '☹️' ], + 'wow' => [ 1, '😲' ], + 'blush' => [ 1, '😳' ], + 'cry' => [ 1, '😢' ], + 'plead' => [ 1, '🥺' ], + 'baw' => [ 1, '😭' ], + 'shock' => [ 1, '😱' ], + 'anguish' => [ 1, '😧' ], + 'devil' => [ 1, '😈' ], + 'angry' => [ 1, '😠' ], + 'struggle' => [ 1, '😣' ], + 'proud' => [ 1, '😤' ], + 'smirk' => [ 1, '😏' ], + 'drool' => [ 1, '🤤' ], + 'love' => [ 1, '😍' ], + 'skull' => [ 1, '💀' ], + 'clown' => [ 1, '🤡' ], + 'alien' => [ 1, '👽' ], + 'robot' => [ 1, '🤖' ], + 'ok' => [ 1, '👌' ], + 'fu' => [ 1, '🖕' ], + 'thup' => [ 1, '👍' ], + 'thdown' => [ 1, '👎' ], + 'punch' => [ 1, '👊' ], + 'pray' => [ 1, '🙏' ], + 'flex' => [ 1, '💪' ], + 'eyes' => [ 1, '👀' ], + 'drip' => [ 1, '💦' ], + 'wind' => [ 1, '💨' ], + 'fire' => [ 1, '🔥' ], + 'clover' => [ 1, '🍀' ], + 'anger' => [ 1, '💢' ], + 'perfect' => [ 1, '💯' ], + 'zzz' => [ 1, '💤' ] +]; + +$emote_pools = [ + // 58 + ['happy','grin','xd','grinsweat','rofl','lmao','smile','wink','glad','kiss', + 'crazy','think','wot','kay','yikes','eyeroll','confused','pensive','disgust', + 'vomit','dizzy','nerd','worry','sad','frown','wow','blush','cry','plead','baw', + 'shock','anguish','devil','angry','struggle','proud','smirk','drool','love', + 'skull','clown','alien','robot','ok','fu','thup','thdown','punch','pray', + 'flex','eyes','drip','wind','fire','clover','anger','perfect','zzz'], + + // 81 + ['03d18964','83cf2699','e2ad2cb7','f93f9e5a','bc1ff2b8','fb56168f','51ca59c2', + '67905c4f','967f06c9','c7375c9d','4dfb5c71','199f7d0e','adf2d2f0','59b6bba6', + 'cda7b2fb','08b66b75','25086889','0ee6ba1c','ad2977e6','c291d202','634a21ba', + '7a95728b','a6d16707','af528e56','4623886c','5de4addd','98cf0be7','e54792d7', + 'c2cfb2e3','857a9ea0','a19762fc','b674048b','48c107b3','de27847b','acb11630', + 'd3c674ba','0b3318e4','bb299b4d','ed1cc57f','87c89650','b05923f5','57b01648', + 'c4035570','099390a2','8ea2d160','68104e2a','3d8675e9','1ee7c5a1','f2ecf801', + '51cbf903','25908e08','90786369','4459d60b','6ec0dd2c','9ecd704b','42faedcc', + '1e4d8dfa','6f5bc7e5','fad6951c','6f0d4e37','f0a256b9','b06b1566','ba70c4d9', + 'bdf28159','41a37aa0','d8f61d71','e024965e','a9f848d3','53478ca5','d9bf8456', + 'ec538b5c','ddc55766','c72e8e84','7242c342','8a527ac8','3021a426','6476e57d', + '24edafcc','ed7d4c3a','8b7cc3e0','e1899bbe','2b414cf1'], + + // 28 + ['c458ef22','c2e2602a','c69a1ef1','eda6f332','80692b94','72694e0e','e9d526e8', + '7081142e','1d3f2a13','afd49202','f0dcbf8a','1d7e369a','0e313986','eb07a2c8', + '6e6217c7','0ee48fb4','95c37417','e77bc341','cf1d4b8d','a4b23eff','11339e7b', + '49bde730','d89a0070','7b4acfbf','a6955123','4d00227b'] +]; + +// ------------ + +function xa_error($msg, $extra = null) { + $data = array('status' => 'error', 'msg' => $msg); + + if ($extra) { + $data = array_merge($data, $extra); + } + + echo json_encode($data); + + die(); +} + +function xa_success($data = null) { + $ret = array('status' => 'success'); + + if ($data) { + $ret['data'] = $data; + } + + echo json_encode($ret); + + die(); +} + +/** + * Sessions + */ +function xa_start_session() { + // Recover previous session by sid + if (isset($_COOKIE['xa_sid']) && $_COOKIE['xa_sid']) { + $data = xa_recover_session_by_sid($_COOKIE['xa_sid']); + + if ($data) { + xa_success($data); + } + } + + $ip = $_SERVER['REMOTE_ADDR']; + + // Recover previous session by IP + $sid = hash_hmac('sha1', $ip, HMAC_SECRET); + + if (!$sid) { + xa_error(ERR_GENERIC . ' (ssx9)'); + } + + $data = xa_recover_session_by_sid($sid); + + if ($data) { + xa_set_sid_cookie($sid); + xa_success($data); + } + + // Create new session + $data = []; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($ip, XA_DOMAIN, $_COOKIE['4chan_pass']); + + if ($userpwd->maskLifetime() >= XA_PWD_TTL) { + $balance = 100; + } + } + else { + $balance = 0; + } + + $data['start_ts'] = $_SERVER['REQUEST_TIME']; + $data['balance'] = $balance; + $data['owned'] = []; + + $ret = xa_save_session($sid, $ip, $data); + + if (!$ret) { + xa_error(ERR_GENERIC . ' (ssx5)'); + } + + xa_set_sid_cookie($sid); + xa_success($data); +} + +function xa_set_sid_cookie($sid) { + setcookie('xa_sid', $sid, $_SERVER['REQUEST_TIME'] + XA_COOKIE_TTL, '/', '.' . XA_DOMAIN, true); +} + +function xa_save_session($sid, $ip, $data) { + $sql = "INSERT INTO april_emotes (session_id, ip, data) VALUES('%s', '%s', '%s')"; + + $data = json_encode($data); + + if (!$data) { + return false; + } + + return mysql_global_call($sql, $sid, $ip, $data); +} + +function xa_rebuild_owned_emotes($data) { + global $emotes; + + $owned_meta = []; + + if (!isset($data['owned'])) { + return $owned_meta; + } + + foreach ($data['owned'] as $key) { + $_e = $emotes[$key]; + + if ($_e) { + $owned_meta[] = [$key, $_e[0], $_e[1]]; + } + } + + return $owned_meta; +} + +function xa_recover_session_by_sid($sid) { + $sql = "SELECT data FROM april_emotes WHERE session_id = '%s' LIMIT 1"; + + $res = mysql_global_call($sql, $sid); + + if (!$res) { + xa_error(ERR_GENERIC . ' (gsbs5)'); + } + + $data = mysql_fetch_assoc($res)['data']; + + if (!$data) { + return null; + } + + $data = json_decode($data, true); + + if (!$data) { + xa_error(ERR_GENERIC . ' (gsbs4)'); + } + + $data['owned'] = xa_rebuild_owned_emotes($data); + + return $data; +} + +/** + * Rolling + */ +function xa_roll($size) { + global $emotes, $emote_pools; + + if (!isset($_COOKIE['xa_sid']) || !$_COOKIE['xa_sid']) { + xa_error(ERR_BAD_REQ); + } + + $sid = $_COOKIE['xa_sid']; + + $rolled_eids = []; + + for ($i = 0; $i < $size; $i++) { + $_r = mt_rand(0, 99); + + if ($_r >= 90) { + // 28 emotes in pool 3 + $rolled_eids[] = $emote_pools[2][mt_rand(0, count($emote_pools[2]) - 1)]; + } + else if ($_r >= 50) { + // 81 emotes in pool 2 + $rolled_eids[] = $emote_pools[1][mt_rand(0, count($emote_pools[1]) - 1)]; + } + else { + // 58 emojis in pool 1 + $rolled_eids[] = $emote_pools[0][mt_rand(0, count($emote_pools[0]) - 1)]; + } + } + + mysql_global_call('START TRANSACTION'); + + $sql = "SELECT data FROM april_emotes WHERE session_id = '%s' FOR UPDATE"; + + $res = mysql_global_call($sql, $sid); + + if (!$res) { + mysql_global_call('COMMIT'); + xa_error(ERR_GENERIC . ' (lbr8)'); + } + + $data = mysql_fetch_assoc($res)['data']; + + if (!$data) { + mysql_global_call('COMMIT'); + xa_error(ERR_BAD_REQ); + } + + $data = json_decode($data, true); + + if (!$data) { + mysql_global_call('COMMIT'); + xa_error(ERR_GENERIC . ' (lbr9)'); + } + + // Check if enough points + $full_balance = xa_get_full_balance($data); + $total_cost = $size * XA_ROLL_PRICE; + + if ($full_balance < $total_cost) { + mysql_global_call('COMMIT'); + xa_error(ERR_NO_PTS, [ 'pts' => $data['balance'] ]); + } + + // Roll results to return to the client for visual purposes + $obtained = []; + + $recycled_points = 0; + + if (!isset($data['owned'])) { + $data['owned'] = []; + } + + foreach ($rolled_eids as $eid) { + $_e_meta = xa_get_emote_by_id($eid); + + list($key, $kind, $arg) = $_e_meta; + + // Emote already owned, recycle it + if (in_array($key, $data['owned'])) { + if ($kind === 3) { + $recycled_points += 10; + } + else { + $recycled_points += 5; + } + } + // New emote, add it to the owned list + else { + $data['owned'][] = $key; + } + + $obtained[] = $_e_meta; + } + + $data['balance'] += $recycled_points; + $data['balance'] -= $total_cost; + + $balance = $data['balance']; + + $data = json_encode($data); + + $sql = "UPDATE april_emotes SET data = '%s' WHERE session_id = '%s' LIMIT 1"; + + $res = mysql_global_call($sql, $data, $sid); + + if (!$res) { + mysql_global_call('COMMIT'); + xa_error(ERR_GENERIC . ' (lbr6)'); + } + + mysql_global_call('COMMIT'); + + xa_success(['obtained' => $obtained, 'balance' => $balance]); +} + +function xa_get_full_balance($data) { + $now = $_SERVER['REQUEST_TIME']; + $start_ts = (int)$data['start_ts']; + $tick_balance = floor(($now - $start_ts) / XA_TICK_INTERVAL) * XA_TICK_POINTS; + return $tick_balance + $data['balance']; +} + +function xa_get_emote_by_id($id) { + global $emotes; + + foreach ($emotes as $key => $meta) { + $_kind = $meta[0]; + $_arg = $meta[1]; + + // Emoji (type 1) + if ($_kind === 1) { + if ($key === $id) { + return [$key, $_kind, $_arg]; + } + } + // Image emotes + else { + if ($_arg === $id) { + return [$key, $_kind, $_arg]; + } + } + } + + return null; +} + +/** + * Buying + */ +function xa_buy() { + global $emotes, $emote_pools; + + if (!isset($_COOKIE['xa_sid']) || !$_COOKIE['xa_sid']) { + xa_error(ERR_BAD_REQ); + } + + if (!isset($_POST['eid']) || !$_POST['eid']) { + xa_error(ERR_BAD_REQ); + } + + $obtained = xa_get_emote_by_id($_POST['eid']); + + if (!$obtained) { + xa_error(ERR_BAD_REQ); + } + + list($key, $kind, $arg) = $obtained; + + // Pool 1 + if ($kind === 1) { + $total_cost = 50; + } + // Pool 2 + else if ($kind === 2) { + $total_cost = 150; + } + // Pool 3 + else { + $total_cost = 300; + } + + $sid = $_COOKIE['xa_sid']; + + mysql_global_call('START TRANSACTION'); + + $sql = "SELECT data FROM april_emotes WHERE session_id = '%s' FOR UPDATE"; + + $res = mysql_global_call($sql, $sid); + + if (!$res) { + mysql_global_call('COMMIT'); + xa_error(ERR_GENERIC . ' (be0)'); + } + + $data = mysql_fetch_assoc($res)['data']; + + if (!$data) { + mysql_global_call('COMMIT'); + xa_error(ERR_BAD_REQ); + } + + $data = json_decode($data, true); + + if (!$data) { + mysql_global_call('COMMIT'); + xa_error(ERR_GENERIC . ' (be1)'); + } + + // Check if already owned + if (in_array($key, $data['owned'])) { + mysql_global_call('COMMIT'); + xa_error(ERR_ALREADY_OWNED); + } + + // Check if enough points + $full_balance = xa_get_full_balance($data); + + if ($full_balance < $total_cost) { + mysql_global_call('COMMIT'); + xa_error(ERR_NO_PTS, [ 'pts' => $data['balance'] ]); + } + + $data['owned'][] = $key; + $data['balance'] -= $total_cost; + + $balance = $data['balance']; + + $data = json_encode($data); + + $sql = "UPDATE april_emotes SET data = '%s' WHERE session_id = '%s' LIMIT 1"; + + $res = mysql_global_call($sql, $data, $sid); + + if (!$res) { + mysql_global_call('COMMIT'); + xa_error(ERR_GENERIC . ' (lbr6)'); + } + + mysql_global_call('COMMIT'); + + xa_success(['obtained' => $obtained, 'balance' => $balance]); +} + +// -------------- + +if (!isset($_POST['action'])) { + xa_error(ERR_BAD_REQ); +} + +if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] != '' + && !preg_match('/^https?:\/\/([_a-z0-9]+)\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) { + xa_error(ERR_BAD_REQ . '(xr1)'); +} + +// ------------- + +$_action = $_POST['action']; + +if ($_action === 'start') { + xa_start_session(); +} +else if ($_action === 'roll3') { + xa_roll(3); +} +else if ($_action === 'roll10') { + xa_roll(10); +} +else if ($_action === 'buy') { + xa_buy(); +} +else { + xa_error(ERR_BAD_REQ); +} diff --git a/footer-test.txt b/footer-test.txt new file mode 100644 index 0000000..115c7ba --- /dev/null +++ b/footer-test.txt @@ -0,0 +1,14 @@ +
    + + + + +All trademarks and copyrights on this page are owned by their respective parties. Images uploaded are the responsibility of the Poster. Comments are owned by the Poster. + + +
    + +
    diff --git a/footer-ws.txt b/footer-ws.txt new file mode 100644 index 0000000..dca88d9 --- /dev/null +++ b/footer-ws.txt @@ -0,0 +1,14 @@ +
    + + + + +All trademarks and copyrights on this page are owned by their respective parties. Images uploaded are the responsibility of the Poster. Comments are owned by the Poster. + + +
    + +
    diff --git a/footer.txt b/footer.txt new file mode 100644 index 0000000..695ce75 --- /dev/null +++ b/footer.txt @@ -0,0 +1,14 @@ +
    + + + + +All trademarks and copyrights on this page are owned by their respective parties. Images uploaded are the responsibility of the Poster. Comments are owned by the Poster. + + +
    + +
    diff --git a/forms/ban.php b/forms/ban.php new file mode 100644 index 0000000..433a75f --- /dev/null +++ b/forms/ban.php @@ -0,0 +1,327 @@ + +Ban form + + + + +$err

    "; + $err .= "
    Back"; + die($err); // ok, not very fancy yet +} + +function format_host($dec_ip,$reverse='') { + if(!$reverse) + $reverse = gethostbyaddr($dec_ip); + if($reverse && $reverse != $dec_ip) { + $reverse = htmlspecialchars($reverse); + return "$reverse ($dec_ip)"; + } + else return "$dec_ip"; +} + +function format_name($name) { + $name = strip_tags($name); + $name = strtr($name, '!', '#'); + $name = htmlspecialchars($name); + return $name; +} + + + +function ban_history($dec_ip) { + $query = mysql_global_call("SELECT COUNT(*) as total,COUNT(active||NULL) as active FROM banned_users WHERE host='%s'", $dec_ip); + $row = mysql_fetch_assoc($query); + if(!$row) + return ''; + if($row['total'] == 0) + return ''; + if($row['active'] == 0) + $linkdesc = sprintf("{$row['total']} past ban%s for this IP.", ($row['total']>1)?'s':'' ); + else if($row['active'] == $row['total']) + $linkdesc = sprintf("{$row['active']} ban%s already active for this IP.", ($row['active']>1)?'s':''); + else { + $row['total'] -= $row['active']; + $linkdesc = sprintf("{$row['total']} past ban%s and {$row['active']} ban%s already active for this IP.", ($row['total']>1)?'s':'' , ($row['active']>1)?'s':''); + } + $dec_ip = urlencode($dec_ip); + return "$linkdesc"; +} + +function other_ban_requests($than,$dec_ip) { + $query = mysql_global_call("SELECT COUNT(*) as total from ban_requests WHERE id!=%d AND host='%s'", $than, $dec_ip); + $row = mysql_fetch_assoc($query); + if(!$row) + return 0; + return $row['total']; +} + +function get_xff($board,$tim) { + $query = mysql_global_call("SELECT xff from xff where tim='%s' AND board='%s'", $board, $tim); + $row = mysql_fetch_assoc($query); + if(!$row) + return ''; + return format_host($row['host']); +} + +function form_ban($o) { + head(); + if($o['load_reporter']) { + $query = mysql_global_call("SELECT ip FROM reports where ip=%d LIMIT 1",$o['load_reporter']); + if(!($row=mysql_fetch_assoc($query))) + fancydie("No reports found with specified IP."); + $form['load_name'] = 'load_reporter'; + $form['load_value'] = $o['load_reporter']; + $form['name'] = 'Anonymous'; + $form['host'] = format_host(long2ip($row['ip'])); + $form['xff'] = ''; + $form['banhist'] = ban_history(long2ip($row['ip'])); + $form['board'] = ''; + $form['title'] = "Banning reporter " . long2ip($row['ip']); + $o['hide_postbans'] = 1; + $form['id'] = (int)$o['load_reporter']; + } + else if($o['load_ban_request']) { + $query = mysql_global_call("SELECT * FROM ban_requests where id=%d", $o['load_ban_request']); + if(!($row=mysql_fetch_assoc($query))) + fancydie("Specified ban request does not exist."); + $form['load_name'] = 'load_ban_request'; + $form['load_value'] = $o['load_ban_request']; + $post = unserialize($row['spost']); + $form['name'] = format_name($post['name']); + $form['host'] = format_host($post['host'],$post['reverse']); + $form['xff'] = htmlspecialchars($post['xff']); + $form['banhist'] = ban_history($post['host']); + $form['board'] = $row['board']; + $form['title'] = htmlspecialchars("Filling {$row['janitor']}'s ban request for /{$row['board']}/{$post['no']}"); + //$form['public_reason'] = htmlspecialchars($row['reason']); + //$form['private_reason'] = htmlspecialchars("requested by {$row['janitor']}"); + $form['other_ban_reqs'] = other_ban_requests($o['load_ban_request'], $post['host']); + $o['hide_postbans'] = 1; + $form['id'] = (int)$o['load_ban_request']; + } + else if($o['load_post']) { + + } + else if($GLOBALS['my_access']['manual_ban']) { + $o['name_edit'] = $o['host_edit'] = /*$o['bannedby_edit'] =*/ true; + $form['load_name'] = 'manual'; + $form['load_value'] = 'yes'; + } + + // overrides + if(isset($_COOKIE['4chan_bpubr'])) + $form['public_reason'] = htmlspecialchars($_COOKIE['4chan_bpubr']); + if(isset($_COOKIE['4chan_bprvr'])) + $form['private_reason'] = htmlspecialchars($_COOKIE['4chan_bprvr']); + if(isset($_COOKIE['4chan_blen'])) { + $clen = (int)$_COOKIE['4chan_blen']; + if($clen==0) + $form['warn'] = 1; + else if($clen==-1) + $form['indef'] = 1; + else + $form['length'] = $clen; + $form['remember'] = 1; + } + + if($o['public_reason']) + $form['public_reason'] = htmlspecialchars($o['public_reason']); + if($o['private_reason']) + $form['private_reason'] = htmlspecialchars($o['private_reason']); + if($o['length']) + $form['length'] = htmlspecialchars($o['length']); + + $form['modname'] = htmlspecialchars($_COOKIE['4chan_auser']); + +?> +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    Name:>
    Host:>
    Proxy For: title="This is possibly the user's real IP, but only the above IP will be banned.">
    Ban History:
    Public Ban Reason:
    Private Info:
    Unban in: days [> Warn] [> Permanent]
    Banned by:>
    Ban options: + + [msg] + + +
    Post-ban actions: + + + + + [ Clear other ban request1)?'s':'' ?> for this IP] + +
    +
    + + + + + "yotsubanew.$cssVersion.css", + 'Yotsuba B New' => "yotsubluenew.$cssVersion.css", + 'Futaba New' => "futabanew.$cssVersion.css", + 'Burichan New' => "burichannew.$cssVersion.css", + 'Photon' => "photon.$cssVersion.css", + 'Tomorrow' => "tomorrow.$cssVersion.css" + ); + + if( !$no ) $no = $_GET['no']; + + $no = (int)$no; + + $css = ''; + + if( isset( $_COOKIE[$sg] ) ) { + if( isset( $styles[$_COOKIE[$sg]] ) ) { + $css = ''; + } + } else { + $dcssl = $defaultcss . '.' . $cssVersion . '.css'; + $css = ''; + } + + ?> + + + + Report Post No.<?=$no ? $no : ""?> + + + + + + + + +

    $err

    "; + + if ($needs_back_button) { + $err .= "
    [Back]"; + } + else { + if ($success) { + $err .= ""; + } + $err .= "
    [Close]"; + } + + die($err); +} + + +function form_report($board, $no, $no_captcha = false) { + report_head($no, 0); + $cats = get_report_categories($board, $no, DEFAULT_BURICHAN == 1); +?> + +
    +
    + Report type +
    +
    +
    +
    + document.addEventListener('DOMContentLoaded', function() { TCaptcha.init(document.getElementById('t-root'), '" . BOARD_DIR . "', 1); }, false);"; + } + else { + echo captcha_form(true, null, $dark); + } + ?> + 4chan Pass users can bypass this CAPTCHA. [More Info] + +
    +
    Submitting false or misclassified reports will result in a ban.
    + + + +
    + "yotsubanew.$cssVersion.css", + 'Yotsuba B New' => "yotsubluenew.$cssVersion.css", + 'Futaba New' => "futabanew.$cssVersion.css", + 'Burichan New' => "burichannew.$cssVersion.css", + 'Photon' => "photon.$cssVersion.css", + 'Tomorrow' => "tomorrow.$cssVersion.css" + ); + + if( !$no ) $no = $_GET['no']; + + $no = (int)$no; + + $css = ''; + + if( isset( $_COOKIE[$sg] ) ) { + if( isset( $styles[$_COOKIE[$sg]] ) ) { + $css = ''; + } + } else { + $dcssl = $defaultcss . '.' . $cssVersion . '.css'; + $css = ''; + } + + ?> + + + + Report Post No.<?=$no ? $no : ""?> + + + + + + + + +

    $err

    "; + + if ($needs_back_button) { + $err .= "
    [Back]"; + } + else { + if ($success) { + $err .= ""; + } + $err .= "
    [Close]"; + } + + die($err); +} + + +function form_report($board, $no, $no_captcha = false) { + report_head($no, 0); + $cats = get_report_categories($board, $no, DEFAULT_BURICHAN == 1); +?> + +
    +
    + Report type +
    +
    +
    +
    + document.addEventListener('DOMContentLoaded', function() { TCaptcha.init(document.getElementById('t-root'), '" . BOARD_DIR . "', 1); }, false);"; + } + else { + echo captcha_form(true, null, $dark); + } + ?> + 4chan Pass users can bypass this CAPTCHA. [More Info] + +
    +
    Submitting false or misclassified reports will result in a ban.
    + + + +
    + + +[a / +b / +c / +d / +e / +f / +g / +gif / +h / +hr / +k / +m / +o / +p / +r / +s / +t / +u / +v / +vg / +vmg / +vr / +vrpg / +vst / +w / +wg] + +[i / +ic] + +[r9k] + +[s4s] + +[cm / +hm / +lgbt / +y] + +[3 / +adv / +an / +asp / +bant / +biz / +cgl / +ck / +co / +diy / +fa / +fit / +gd / +hc / +int / +jp / +lit / +mlp / +mu / +n / +out / +po / +pol / +pw / +sci / +soc / +sp / +tg / +toy / +trv / +tv / +vp / +vt / +wsg / +x / +xs] + + + +[Settings] +[Home] + + +
    +
    + Board + +
    + +
    + + Settings + Home +
    + +
    diff --git a/header-test.txt b/header-test.txt new file mode 100644 index 0000000..1fb4e5b --- /dev/null +++ b/header-test.txt @@ -0,0 +1,100 @@ +
    + +[a / +b / +c / +d / +e / +f / +g / +gif / +h / +hr / +k / +m / +o / +p / +r / +s / +t / +u / +v / +vg / +vr / +w + / wg] + +[i / +ic] + +[r9k / +s4s / +vip / +qa] + +[cm / +hm / +lgbt + / y] + +[3 / +aco / +adv / +an / +asp / +bant / +biz / +cgl / +ck / +co / +diy / +fa / +fit / +gd / +hc / +his / +int / +jp / +lit / +mlp / +mu / +n / +news / +out / +po / +pol / +qst / +sci / +soc / +sp / +tg / +toy / +trv / +tv / +vp / +wsg / +wsr / +x] + + + +[Settings] +[Search] +[Mobile] +[Home] +
    + +
    +
    + Board + +
    + +
    + + Settings + Mobile + Home +
    + +
    diff --git a/header-ws.txt b/header-ws.txt new file mode 100644 index 0000000..5c60f6e --- /dev/null +++ b/header-ws.txt @@ -0,0 +1,106 @@ +
    + +[a / +b / +c / +d / +e / +f / +g / +gif / +h / +hr / +k / +m / +o / +p / +r / +s / +t / +u / +v / +vg / +vm / +vmg / +vr / +vrpg / +vst / +w + / wg] + +[i / +ic] + +[r9k / +s4s / +vip / +qa] + +[cm / +hm / +lgbt + / y] + +[3 / +aco / +adv / +an / +bant / +biz / +cgl / +ck / +co / +diy / +fa / +fit / +gd / +hc / +his / +int / +jp / +lit / +mlp / +mu / +n / +news / +out / +po / +pol / +pw / +qst / +sci / +soc / +sp / +tg / +toy / +trv / +tv / +vp / +vt / +wsg / +wsr / +x / +xs] + + + +[Settings] +[Search] +[Mobile] +[Home] +
    + +
    +
    + Board + +
    + +
    + + Settings + Mobile + Home +
    + +
    diff --git a/header.txt b/header.txt new file mode 100644 index 0000000..9b1a966 --- /dev/null +++ b/header.txt @@ -0,0 +1,106 @@ +
    + +[a / +b / +c / +d / +e / +f / +g / +gif / +h / +hr / +k / +m / +o / +p / +r / +s / +t / +u / +v / +vg / +vm / +vmg / +vr / +vrpg / +vst / +w / +wg] + +[i / +ic] + +[r9k / +s4s / +vip / +qa] + +[cm / +hm / +lgbt / +y] + +[3 / +aco / +adv / +an / +bant / +biz / +cgl / +ck / +co / +diy / +fa / +fit / +gd / +hc / +his / +int / +jp / +lit / +mlp / +mu / +n / +news / +out / +po / +pol / +pw / +qst / +sci / +soc / +sp / +tg / +toy / +trv / +tv / +vp / +vt / +wsg / +wsr / +x / +xs] + + + +[Settings] +[Search] +[Mobile] +[Home] +
    + +
    +
    + Board + +
    + +
    + + Settings + Mobile + Home +
    + +
    diff --git a/imgboard-test.php b/imgboard-test.php new file mode 100644 index 0000000..855f643 --- /dev/null +++ b/imgboard-test.php @@ -0,0 +1,10433 @@ +' . LOCKDOWN_MSG . '
    '); + } +} + +if (TEST_BOARD && (!has_level() || !has_flag('developer'))) { + die(''); +} + +// test +if( TEST_BOARD || has_flag('developer') ) { + ini_set( 'display_errors', 1 ); + error_reporting(E_ALL & ~E_NOTICE); +} + +extract( $_POST, EXTR_SKIP ); +extract( $_GET, EXTR_SKIP ); +extract( $_COOKIE, EXTR_SKIP ); + +if (isset( $_COOKIE['4chan_pass']) && $_COOKIE['4chan_pass']) { + $pwdc = $_COOKIE['4chan_pass']; +} +else { + $pwdc = null; +} + +// FIXME whitelist +unset( $dest ); +unset( $log ); +unset( $update_avg_secs ); + +if( $argv[1] ) $mode = $argv[1]; +$id = intval( $id ); + +if( $_SERVER["REQUEST_METHOD"] == "POST" ) { + // bust cache + header( 'Cache-Control: private, no-cache, must-revalidate' ); + header( 'Expires: -1' ); + header( 'Vary: *' ); +} + +if( array_key_exists( 'upfile', $_FILES ) ) { + $upfile_name = $_FILES["upfile"]["name"]; + $upfile = $_FILES["upfile"]["tmp_name"]; +} else { + $upfile_name = $upfile = ''; +} + +$fwritetimer = 0.0; + +ignore_user_abort( true ); + +$word_filters_enabled = false; +if (WORD_FILT) { + $word_filt_root = '/www/global/yotsuba/wordfilters/'; + + if (file_exists($word_filt_root . BOARD_DIR . '.php')) { + include_once($word_filt_root . BOARD_DIR . '.php'); + $word_filters_enabled = true; + } + else if (file_exists($word_filt_root . 'global.php')) { + include_once($word_filt_root . 'global.php'); + $word_filters_enabled = true; + } +} + +if( JANITOR_BOARD == 1 && !has_level( 'janitor' ) ) { + die( '' ); +} + +if( JANITOR_BOARD == 1 ) + include_once 'plugins/broomcloset.php'; + +// QENHANCE +if( META_BOARD ) { + include_once 'plugins/enhance_q.php'; +} + +$mysql_connect_opts = 0; +mysql_board_connect(BOARD_DIR); + +$board_flags_array = null; + +if (ENABLE_BOARD_FLAGS) { + $_flags_type = (defined('BOARD_FLAGS_TYPE') && BOARD_FLAGS_TYPE) ? BOARD_FLAGS_TYPE : BOARD_DIR; + $_board_flags_path = '/www/global/yotsuba/lib/board_flags_' . $_flags_type . '.php'; + if (file_exists($_board_flags_path)) { + include_once($_board_flags_path); + $board_flags_array = get_board_flags_array(); + } +} + +$thread_unique_ips = 0; + +$index_rbl = PAGE_MAX; +$index_last_thread = 0; +$index_last_post = 0; + +if (JANITOR_BOARD && PAGE_MAX == 0) { + $index_rbl = ceil(LOG_MAX / DEF_PAGES); +} + +$valid_boards = "3|aco|adv|an|biz|diy|fa|fit|gd|gif|int|lit|hc|hr|a|b|ck|co|cm|c|d|e|f|g|h|i|k|lgbt|m|n|o|out|p|r|s|t|u|vp|vg|vr|v|w|x|y|wg|ic|cgl|hm|mlp|mu|pol|po|r9k|s4s|sci|soc|tg|tv|toy|trv|jp|sp|wsg|qa|qst|his|trash|news|wsr|vip|bant|vrpg|vmg|vst|vt|vm|pw|xs"; + +$boards_matching_arr = array(); + +$captcha_bypass = null; +$rangeban_bypass = false; +$passid = ''; + +// FIXME, this should be put somewhere else. +function is_local() { + $longip = ip2long( $_SERVER[ 'REMOTE_ADDR' ] ); + + return !$longip || cidrtest( $longip, "10.0.0.0/24" ) || cidrtest( $longip, "204.152.204.0/24" ) || cidrtest( $longip, "127.0.0.0/24" ); +} + +/** + * Abbreviates posts on index pages. + * Truncate $str to $max_lines lines and return $str and $abbr + * where $abbr = whether or not $str was actually truncated. + * Expects well-formed HTML. + */ +function abbreviate($str, $max_lines = 20) { + $lines = explode('
    ', $str); + + if (count($lines) > $max_lines) { + $abbr = 1; + $lines = array_slice($lines, 0, $max_lines); + $str = implode('
    ', $lines ); + + $unpaired_tags = array( + 'img' => true, + 'br' => true, + 'input' => true, + 'hr' => true, + 'param' => true + ); + + preg_match_all('/<\/([^>]+)>/', $str, $closed_tags, PREG_SET_ORDER); + $closed_count = count($closed_tags); + + $closed_map = array(); + + foreach ($closed_tags as $m) { + if (!isset($closed_map[$m[1]])) { + $closed_map[$m[1]] = 1; + } + else { + $closed_map[$m[1]] += 1; + } + } + + preg_match_all('/<([a-z0-9]+)(?: |>)/', $str, $open_tags, PREG_SET_ORDER); + $open_count = count($open_tags); + + for ($i = 0; $i < $open_count; ++$i) { + $tag = $open_tags[$i][1]; + + if (isset($unpaired_tags[$tag])) { + continue; + } + + if (!isset($closed_map[$tag])) { + $str .= ""; + } + else if ($closed_map[$tag] > 0) { + $closed_map[$tag] -= 1; + } + else if ($closed_map[$tag] <= 0) { + $str .= ""; + } + } + } + else { + $abbr = 0; + } + + return array($str, $abbr); +} + +/** + * Currently only used on /archive + * strips html tags and replaces sjis art with [SJIS] placeholders + */ +function truncate_comment($str, $length, $keep_spoilers = false) { + // remove sjis + if (SJIS_TAGS && strpos($str, '/', '[SJIS]', $str); + } + + $len = mb_strlen($str); + + if ($len <= $length) { + return $str; + } + + if (!$keep_spoilers) { + $str = strip_tags($str); + } + else { + $str = strip_tags($str, ''); + } + + if ($len <= $length) { + return $str; + } + + $str = mb_substr($str, 0, $length); + + // remove truncated html entities + $str = preg_replace('/&[^;]*$/', '', $str); + + if ($keep_spoilers) { + $str = preg_replace('/<[^>]*$/', '', $str); + + $oc = substr_count($str, ''); + + if ($oc) { + $cc = substr_count($str, ''); + $dc = $oc - $cc; + if ($dc > 0) { + $str .= str_repeat('', $dc); + } + } + } + + $str .= '…'; + + return $str; +} + +function paranoid_rename( $src, $dest ) +{ + $across_devices = false; //keep around for future use + $u = false; + + if( $across_devices ) { + // rename to dest dir, then over dest + $dsrc = dirname( $dest ) . "/" . basename( $src ); + if( !@rename( $src, $dsrc ) ) $u = $src; + else if( !@rename( $dsrc, $dest ) ) $u = $dsrc; + } else { + if( !@rename( $src, $dest ) ) $u = $src; + } + + if( $u ) + unlink( $u ); +} + +function rename_across_device( $src, $dest ) +{ + // FIXME: copy() does a chmod but we don't need that + copy($src, $dest); + unlink($src); +} + +function getmypid_cached() +{ + static $pid = -1; + + if ($pid === -1) $pid = getmypid(); + + return $pid; +} + +// print $contents to $filename by using a temporary file and renaming it +// may destroy $contents in the process +// (makes *.html and *.gz if USE_GZIP is on) +function print_page( $filename, &$contents, $force_nogzip = 0, $trim_whitespace = 1 ) +{ + global $fwritetimer; + + $timestarted = microtime( true ); + + if( NEW_HTML == 1 && $trim_whitespace ) { + $contents = str_replace( array("\r\n", "\n", "\t"), array('', '', ''), $contents ); + } + + $gzip = ( USE_GZIP == 1 && !$force_nogzip ); + + if( $gzip ) { + $tempname = dirname( $filename )."/gztmp".getmypid_cached(); + + // FIXME: number of syscalls done by gzwrite is not optimal (it does a small one then 4KB writes after) + // for small files (how small?) do gzencode() and file_put_contents() instead. + + $fp = gzopen($tempname, "wb9"); + if( $fp === false ) return; + gzwrite($fp, $contents); + gzclose($fp); + // chmod( $tempname, 0664 ); //it was created 0600 + + paranoid_rename( $tempname, $filename . ".gz" ); + } else { + $tempname = dirname( $filename )."/tmp".getmypid_cached(); + if( file_put_contents( $tempname, $contents ) === false ) return; + // chmod( $tempname, 0664 ); //it was created 0600 + paranoid_rename( $tempname, $filename ); + } + + $fwritetimer += ( microtime( true ) - $timestarted ); +} + +function file_get_contents_cached( $filename ) +{ + static $cache = array(); + if( isset( $cache[$filename] ) ) + return $cache[$filename]; + $cache[$filename] = @file_get_contents( $filename ); + + return $cache[$filename]; +} + +function file_array_cached( $filename ) +{ + static $cache = array(); + if( isset( $cache[$filename] ) ) + return $cache[$filename]; + $cache[$filename] = @file( $filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); + + return $cache[$filename]; +} + +function get_blotter() { + if (!SHOW_BLOTTER) { + return ''; + } + + $msg_limit = 3; + + $blotter = <<
    +HTML; + + $query = << 0) { + while ($row = mysql_fetch_assoc($res)) { + if ($mtime === 0) { + $mtime = $row['date']; + } + + $blotter .= '' . + date('m/d/y', $row['date']) . '' . + $row['content'] . ''; + } + } + else { + return ''; + } + + $blotter .= '[Hide] [Show All]'; + + return $blotter; +} + +function blotter_contents() +{ + static $cache; + if( isset( $cache ) ) return $cache; + $ret = ""; + $topN = 4; //how many lines to print + $bl_lines = file( BLOTTER_PATH ); + $bl_top = array_slice( $bl_lines, 0, $topN ); + $date = ""; + foreach( $bl_top as $line ) { + if( !$date ) { + $lineparts = explode( ' - ', $line ); + if( strpos( $lineparts[0], '', $lineparts[0] ); + $date = $dateparts[1]; + $date = "
  • Blotter updated: $date"; + } else { + $date = $lineparts[0]; + $date = "
  • Blotter updated: $date"; + } + } + $line = trim( $line ); + $line = str_replace( "\\", "\\\\", $line ); + $line = str_replace( "'", "\'", $line ); + $ret .= "'
  • $line'+\n"; + } + $ret .= "''"; + $cache = array($date, $ret); + + return array($date, $ret); +} + +function find_match_and_prefix( $regex, $str, $off, &$match ) +{ + if( !preg_match( $regex, $str, $m, PREG_OFFSET_CAPTURE, $off ) ) return false; + + $moff = $m[0][1]; + $match = array(substr( $str, $off, $moff - $off ), $m[0][0]); + + return true; +} + +// skip_on_spoilers will stop parsing and return the unmodified comment +// if spoiler tags are found inside the string to wrap. +// This is to avoid sjis.spoiler tags mixing mostly. +function parse_bbcode_one( $com, $tn, $st, $et, $nest_limit = 2, $skip_on_spoilers = false ) +{ + if( !find_match_and_prefix( "/\[$tn\]/", $com, 0, $m ) ) return $com; + + $bracket_tn = "[$tn]"; + $bl = strlen( $bracket_tn ); + $el = $bl + 1; + $ret = $m[0] . $st; + $lev = 1; + $off = strlen( $m[0] ) + $bl; + + while( 1 ) { + if (!find_match_and_prefix( "@\[/?$tn\]@", $com, $off, $m)) break; + list( $txt, $tag ) = $m; + + if (!$skip_on_spoilers || $tag === $bracket_tn) { + $ret .= $txt; + } + else if (preg_match('/\[\/?spoiler\]/', $txt)) { + return $com; + } + + $off += strlen( $txt ) + strlen( $tag ); + + if( $tag == $bracket_tn ) { + if( $lev < $nest_limit ) + $ret .= $st; + $lev++; + } else if( $lev ) { + if( $lev <= $nest_limit ) + $ret .= $et; + $lev--; + } + } + + $tail = substr($com, $off, strlen($com) - $off); + $ret .= $tail; + + $lev = min( $lev, $nest_limit ); + + if ($lev > 0) { + if ($skip_on_spoilers && preg_match('/\[\/?spoiler\]/', $tail)) { + return $com; + } + + $ret .= str_repeat( $et, $lev ); + } + + return $ret; +} + +function spoiler_parse( $com ) +{ + return parse_bbcode_one( $com, 'spoiler', '', '' ); +} + +function jsmath_parse( $com ) +{ + $com = parse_bbcode_one( $com, "math", '', '' ); + $com = parse_bbcode_one( $com, "eqn", '
    ', '
    ' ); + + return $com; +} + +/* BBCode for bold, italic, and r/g/b color tags */ +function parse_op_markup($com) { + $com = parse_bbcode_one($com, 'b', '', '', 1); + $com = parse_bbcode_one($com, 'i', '', '', 1); + + $com = parse_bbcode_one($com, 'red', '', '', 1); + $com = parse_bbcode_one($com, 'green', '', '', 1); + $com = parse_bbcode_one($com, 'blue', '', '', 1); + + return $com; +} + +function code_parse( $com ) +{ + return parse_bbcode_one( $com, 'code', '
    ', '
    ' ); +} + +function sjis_parse($com) { + $skip_on_spoilers = strpos($com, '[spoiler]') !== false; + + return parse_bbcode_one($com, 'sjis', '', '', 1, $skip_on_spoilers); +} + +// convenience function for wordfilters. +// text must be html escaped +function random_color( $str, $background = 1, $foreground = 1 ) +{ + $style = ""; + + if( $background ) { + $r = rand( 0, 255 ); + $g = rand( 0, 255 ); + $b = rand( 0, 255 ); + $style = $style . "background: #" . sprintf( "%02x%02x%02x", $r, $g, $b ) . "; "; + } + + if( $foreground ) { + $r = rand( 0, 255 ); + $g = rand( 0, 255 ); + $b = rand( 0, 255 ); + $style = $style . "color: #" . sprintf( "%02x%02x%02x", $r, $g, $b ) . "; "; + } + + if( $style ) { + return "$str"; + } + + return $str; +} + +function append_ban( $board, $ip ) +{ + $cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $board $ip >/dev/null 2>&1 &"; + exec( $cmd ); +} + +// check whether the current user can perform $action (on $no, for some actions) +// board-level access is cached in $valid_cache. +// FIXME move to lib/admin.php +function valid( $action = 'moderator', $no = 0 ) +{ + static $valid_cache, $can_post_html; // the access level of the user + $access_level = array('none' => 0, 'janitor' => 1, 'janitor_this_board' => 2, 'moderator' => 5, 'manager' => 10, 'admin' => 20); + if( !isset( $valid_cache ) ) { + $valid_cache = $access_level['none']; + if( isset( $_COOKIE['4chan_auser'] ) && isset( $_COOKIE['apass'] ) ) { + $user = mysql_real_escape_string( $_COOKIE['4chan_auser'] ); + $pass = $_COOKIE['apass']; + } + if( $user && $pass ) { + $result = mysql_global_call( "SELECT allow,deny,password_expired,username,password FROM " . SQLLOGMOD . " WHERE username='$user' LIMIT 1" ); + list( $allow, $deny, $expired, $username, $password ) = mysql_fetch_row( $result ); + mysql_free_result( $result ); + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $hashed_admin_password = hash('sha256', $username . $password . $admin_salt); + + if ($hashed_admin_password !== $pass) { + return false; + } + + if( $expired ) { + error( 'Your password has expired; check IRC for instructions on changing it.' ); + } + + if( $allow ) { + $allows = explode( ',', $allow ); + $seen_janitor_token = false; + // each token can increase the access level, + // except that we only know that they're a moderator or a janitor for another board + // AFTER we read all the tokens + $cphtml = false; + + foreach( $allows as $token ) { + if( $token == 'janitor' ) { + $seen_janitor_token = true; + } else if( $token == 'manager' && $valid_cache < $access_level['manager'] ) { + $valid_cache = $access_level['manager']; + } else if( $token == 'admin' && $valid_cache < $access_level['admin'] ) { + $valid_cache = $access_level['admin']; + } else if( ( $token == BOARD_DIR || $token == 'all' ) && $valid_cache < $access_level['janitor_this_board'] ) { + $valid_cache = $access_level['janitor_this_board']; // or could be moderator, will be increased in next step + } elseif( $token == 'html' ) { + $cphtml = true; + } + } + + $can_post_html = $cphtml; + // now we can set moderator or janitor status + if( !$seen_janitor_token ) { + if( $valid_cache < $access_level['moderator'] ) + $valid_cache = $access_level['moderator']; + } else { + if( $valid_cache < $access_level['janitor'] ) + $valid_cache = $access_level['janitor']; + } + if( $deny ) { + $denies = explode( ',', $deny ); + if( in_array( BOARD_DIR, $denies ) ) { + $valid_cache = $access_level['none']; + } + } + } + } + } + { + // local rpc can do anything + $longip = ip2long( $_SERVER['REMOTE_ADDR'] ); + if( !$longip || cidrtest( $longip, "10.0.0.0/24" ) || + cidrtest( $longip, "204.152.204.0/24" ) || cidrtest( $longip, "127.0.0.0/24" ) + ) + return YES; + } + switch( $action ) { + case 'moderator': + return $valid_cache >= $access_level['moderator']; + case 'textonly': + return $valid_cache >= $access_level['moderator']; + case 'htmlnopw': + return $valid_cache >= $access_level['manager']; + case 'htmlpost': + return $can_post_html; + case 'janitor_board': + return $valid_cache >= $access_level['janitor']; + case 'delete': + if( $valid_cache >= $access_level['janitor_this_board'] ) { + return true; + } // if they're a janitor on another board, check for illegal post unlock + else if( $valid_cache >= $access_level['janitor'] ) { + $query = mysql_global_do( "SELECT COUNT(*) from reports WHERE board='" . BOARD_DIR . "' AND no=$no AND cat=2" ); + $illegal_count = mysql_result( $query, 0, 0 ); + mysql_free_result( $query ); + + return $illegal_count >= 3; + } + case 'reportflood': + return $valid_cache >= $access_level['janitor']; + case 'floodbypass': + return $valid_cache >= $access_level['janitor']; + case 'rebuild': + return $valid_cache >= $access_level['janitor']; + case 'admin': + return $valid_cache >= $access_level['admin']; + default: // unsupported action + return false; + } +} + +function iplog_add( $board, $no, $ip, $time, $is_thread, $tim, $had_image ) +{ + mysql_global_call( "INSERT INTO user_actions (board,postno,ip,time,uploaded,action,had_image) VALUES ('%s',%d,%d,from_unixtime(%d),%d,'%s',%d)",$board, $no, ip2long($ip), $time, $tim, $is_thread ? "new_thread" : "new_reply", $had_image); +} + +function clean_log_bool( &$row ) +{ + static $bool_cols = array('sticky', 'permasage', 'closed', 'filedeleted', 'permaage', 'undead', 'archived'); + + foreach ($bool_cols as $col) { + if (!isset($row[$col])) continue; // FIXME split this function up to avoid this test + $c = &$row[$col]; + settype($c, "bool"); + // NOTES: $c = $c ? TRUE : FALSE allocates new bools each time + // $c = $c ? &$itrue : &$ifalse causes them to be converted to strings(!) + // Put this back to TRUE : FALSE instead of a settype call sometime so we can look at the php bytecodes + } +} + +function clean_log_int( &$row ) +{ + static $int_cols = array('no', 'w', 'h', 'tn_w', 'tn_h', 'last_modified', 'time', 'fsize', 'resto'); + + // turn columns into int (does this help?) + foreach( $int_cols as $col ) { + if (!isset($row[$col])) continue; + + $c = &$row[$col]; + settype($c, "int"); + } +} + +function clean_log_delreply( &$row ) +{ + if (!$row['resto']) + return; + + static $del_cols = array('sticky', 'permasage', 'closed', 'last_modified', 'root', 'undead', 'permaage'); + + // delete fields not used for replies + foreach( $del_cols as $col ) { + unset($row[$col]); + } +} + +function clean_log_intern( &$row ) +{ + static $intern_cols = array('name', 'ext', 'capcode', 'country'); + + // Intern repeated strings that are usually the same value. + // In PHP 6 this... doesn't seem to do anything? Let's try again in 7. + static $log_intern; + if( !isset( $log_intern ) ) { + $log_intern = array(); + foreach( $intern_cols as $col ) + $log_intern[$col] = array(); + } + + foreach( $intern_cols as $col ) { + $intern_array = &$log_intern[$col]; + $c = &$row[$col]; + + $v = $c; + if( !isset($intern_array[$v]) ) + $intern_array[$v] = $v; + $c = &$intern_array[$c]; + } +} + +function clean_log_row( &$row ) +{ + //static $rn = 0; + clean_log_delreply( $row ); + clean_log_bool( $row ); + clean_log_int( $row ); + //clean_log_intern( $row ); + //if (++$rn == 100) { + // debug_zval_dump($row); + //} +} + +function log_bad_cache_entry($no) +{ + global $log_cache_level; + global $log; + + internal_error_log("logcache", "missing children for OP no $no, cache level $log_cache_level, cache contents ".count($log)); +} + +function invalidate_log($thread) +{ + global $log_cache_level; + global $log; + + if (isset($log[$thread])) { + if ($log[$thread]['resto']) { + die(S_ASSERT); + } + unset($log[$thread]); + } + + if ($log_cache_level==2) + $log_cache_level = 1; +} + +// build a structure out of all the posts in the database. +// this lets us replace a LOT of queries with a simple array access. +// it only builds the first time it was called. +// rather than calling log_cache(1) to rebuild everything, +// you should just manipulate the structure directly. +// $thread may be any postno in a thread +// without a thread, $archive_mode fetches all live threads if 0 and all archived threads if 1 +function log_cache($invalidate = 0, $thread = 0, $archive_mode = 0) { + global $log_cache_level; + global $log, $ipcount, $mysql_unbuffered_reads; + + if (!isset($log) || $invalidate) { + $log = array(); + $log_cache_level = 0; + } + + // Optimisation for index rebuilding when REPLIES_SHOWN is 0. + // No need to fetch the entire board in this case. + $optimised_indexes = !$thread && !$archive_mode && !REPLIES_SHOWN && IS_REBUILDD; + + // Handle cache + // 1 = Live OPs are cached, 2 = Some threads are cached, 3 = Whole live board is cached + if ($optimised_indexes) { + $nlog_cache_level = 1; + } + else { + $nlog_cache_level = $thread ? 2 : 3; + } + + // Whole board is cached, nothing to do. + if ($log_cache_level == 3 && $archive_mode === 0) { + return; + } + + // Thread is cached, nothing to do. + if ($log_cache_level == 2 && isset($log[$thread])) { + return; + } + + // Live OPs are cached, nothing to do. + if ($log_cache_level == 1 && $optimised_indexes) { + return; + } + + if ($nlog_cache_level > $log_cache_level) { + $log_cache_level = $nlog_cache_level; + } + + $ips = array(); + + mysql_board_call( "SET read_buffer_size=1048576" ); + $mysql_unbuffered_reads = 1; + + $query_archived = false; + + if ($thread) { + if ($archive_mode === 0) { + $where = " WHERE archived = 0 AND (resto = $thread OR no = $thread)"; + } + else if ($archive_mode === 1) { + $where = " WHERE archived = 1 AND (resto = $thread OR no = $thread)"; + } + else { + $query_archived = true; + $where = " WHERE (archived = 0 AND resto = $thread) OR (archived = 1 AND resto = $thread) OR no = $thread"; + } + } + else { + if ($archive_mode === 1) { + $query_archived = true; + $where = ' WHERE archived = 1'; + } + else if ($optimised_indexes) { + $where = ' WHERE archived = 0 AND resto = 0'; + + $_thread_ids = array(); + } + else { + $where = ' WHERE archived = 0'; + } + } + + $fields = "no,sticky,permasage,closed,now,name,sub,com,host,pwd,filename,ext,w,h,tn_w,tn_h,tim,time,md5,fsize,last_modified,root,resto,filedeleted,id,capcode,country,undead,permaage,since4pass"; + + if ($query_archived) { + $fields .= ",archived"; + } + + if (MOBILE_IMG_RESIZE) { + $fields .= ",m_img"; + } + + if (ENABLE_BOARD_FLAGS) { + $fields .= ",board_flag"; + } + + $sql_cache = "sql_no_cache"; + + $query = mysql_board_call( "SELECT $sql_cache $fields FROM `" . SQLLOG . "`" . $where ); + $offset = 0; + + while( $row = mysql_fetch_assoc( $query ) ) { + if (!$query_archived) $row['archived'] = $archive_mode; + clean_log_row( $row ); + + $row_no = $row['no']; + $row_resto = $row['resto']; + + // IF mysql returns rows in order by default then replies come after OP + // so if OP doesn't exist, this post is orphaned and should be skipped + // TODO: let's not skip for now, it seems more likely to cause bugs than anything + //if ($fetching_whole_threads && $row_resto && !isset($log[$row_resto])) continue; + + if ($optimised_indexes) { + $_thread_ids[] = (int)$row_no; + } + + $ips[$row['host']] = TRUE; + + if (!$row_resto) { + $row['children'] = array(); + $row['imgreplycount'] = 0; + $row['replycount'] = 0; + } + else { + if (isset($log[$row_resto])) { + $log[$row_resto]['children'][$row_no] = TRUE; + + if ($row['fsize'] && !$row['filedeleted']) { + $log[$row_resto]['imgreplycount']++; + } + + $log[$row_resto]['replycount']++; + } + } + + $log[$row_no] = $row; + } + + $query = null; + + $nrows = count($log); + //if (BOARD_DIR=="b" && !$thread) quick_log_to("/www/perhost/logcaches.log", "inefficient all-board log_cache run t=$thread r=$nrows inv=$invalidate lev=$log_cache_level", true); + // if (!STATIC_REBUILD && $thread) quick_log_to("/www/perhost/logcaches.log", "inefficient? one-thread log_cache run t=$thread r=$nrows inv=$invalidate lev=$log_cache_level", true); + + $is_single_thread = $thread && isset($log[$thread]['children']); + $ipcount = count( $ips ); + unset($ips); + + $mysql_unbuffered_reads = 0; + mysql_board_call( "SET read_buffer_size=131072" ); + + if (!$thread) { + if ($optimised_indexes) { + if (empty($_thread_ids)) { + return; + } + + $_thread_ids = implode(',', $_thread_ids); + + $query = mysql_board_call("SELECT resto, COUNT(*) AS r_count, SUM(IF(fsize > 0 AND filedeleted = 0, 1, 0)) AS i_count FROM `" . SQLLOG . "` WHERE resto IN($_thread_ids) GROUP BY resto"); + + while ($row = mysql_fetch_assoc($query)) { + $log[$row['resto']]['imgreplycount'] = (int)$row['i_count']; + $log[$row['resto']]['replycount'] = (int)$row['r_count']; + } + + $query = mysql_board_call("SELECT no FROM `" . SQLLOG . "` WHERE no IN($_thread_ids) ORDER BY root DESC"); + } + else { + if ($archive_mode === 1) { + $archived = "archived = 1"; + } else { + $archived = "archived = 0"; + } + $query = mysql_board_call("SELECT no FROM `" . SQLLOG . "` WHERE $archived AND resto = 0 AND root > 0 ORDER BY root DESC"); + } + + $threads = array(); // IDs + + while ($row = mysql_fetch_row($query)) { + $no = (int)$row[0]; + + if (isset($log[$no])) { + $threads[] = $no; + } + } + + $log['THREADS'] = $threads; + + foreach( $threads as $thread ) { + $this_thread = $log[$thread]; + + if (!$this_thread['permaage'] && !$this_thread['sticky'] && $this_thread['replycount'] >= MAX_RES) { + $log[$thread]['bumplimit'] = TRUE; + } + else { + $log[$thread]['bumplimit'] = FALSE; + } + + if ($this_thread['archived']) { + $log[$thread]['archived_on'] = strtotime($this_thread['root']); + } + + if (!$this_thread['permaage'] && !$this_thread['sticky'] && !$log[$thread]['undead'] && $this_thread['imgreplycount'] >= MAX_IMGRES) { + $log[$thread]['imagelimit'] = TRUE; + } + else { + $log[$thread]['imagelimit'] = FALSE; + } + + $log[$thread]['semantic_url'] = generate_href_context($this_thread['sub'], $this_thread['com']); + } + } + else if ($is_single_thread) { + if (!$log[$thread]['permaage'] && !$log[$thread]['sticky'] && $log[$thread]['replycount'] >= MAX_RES) { + $log[$thread]['bumplimit'] = TRUE; + } + else { + $log[$thread]['bumplimit'] = FALSE; + } + + if ($log[$thread]['archived']) { + $log[$thread]['archived_on'] = strtotime($log[$thread]['root']); + } + + if (!$log[$thread]['permaage'] && !$log[$thread]['sticky'] && !$log[$thread]['undead'] && $log[$thread]['imgreplycount'] >= MAX_IMGRES) { + $log[$thread]['imagelimit'] = TRUE; + } + else { + $log[$thread]['imagelimit'] = FALSE; + } + + $log[$thread]['semantic_url'] = generate_href_context($log[$thread]['sub'], $log[$thread]['com']); + } + + // calculate old-status for PAGE_MAX mode + //$threadcount = count( $threads ); + + /*if(EXPIRE_NEGLECTED != 1) { + rsort($threads, SORT_NUMERIC); + + if(PAGE_MAX > 0) // the lowest 5% of maximum threads get marked old + for($i = floor(0.95*PAGE_MAX*DEF_PAGES); $i < $threadcount; $i++) { + if(!$log[$threads[$i]]['sticky']) + $log[$threads[$i]]['old'] = 1; + } + else { // threads w/numbers below 5% of LOG_MAX get marked old + foreach($threads as $thread) { + if($lastno-LOG_MAX*0.95>$thread) + if(!$log[$thread]['sticky']) + $log[$thread]['old'] = 1; + } + } + } else { + $rthreads = array(); + foreach ($threads as $t) { + $root = $log[$t]['root']; + $rthreads[$t] = $root; + } + + arsort($rthreads); + $rthreads = array_keys($rthreads); + + if (PAGE_MAX > 0) { + $floor = (int)floor(0.95*PAGE_MAX*DEF_PAGES); + for($i = $floor; $i < $threadcount; $i++) { + if(!$log[$rthreads[$i]]['sticky']) + $log[$rthreads[$i]]['old'] = 1; + } + } + }*/ +} + +function rebuildallthumb($archiveonly = false) +{ + global $log; + if( !has_level() ) return; + $starttime = microtime( true ); + set_time_limit( 0 ); + if (!$archiveonly) { + log_cache(); + $nposts = count($log); + echo "Rebuilding $nposts live posts
    \n"; + + foreach( $log as $post ) { + if( !$post["ext"] ) continue; + + $ext = $post["ext"]; + $tim = $post["tim"]; + $resto = $post["resto"]; + $fname = IMG_DIR . $tim . $ext; + make_thumb( $fname, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5 ); + } + $totaltime = microtime( true ) - $starttime; + echo "Took $totaltime seconds for live posts
    \n"; + } + + // Run again for archived thumbs + $starttime = microtime( true ); + mysql_check_connections(); + log_cache(1, 0, 1); // fetch archives instead + $nposts = count($log); + echo "Rebuilding $nposts archived posts
    \n"; + + foreach( $log as $post ) { + if( !$post["ext"] ) continue; + + $ext = $post["ext"]; + $tim = $post["tim"]; + $resto = $post["resto"]; + $fname = IMG_DIR . $tim . $ext; + make_thumb( $fname, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5 ); + } + $totaltime = microtime( true ) - $starttime; + echo "Took $totaltime seconds for archived posts
    \n"; +} + +/** + * Displays some text on a lightweight page styled using the default stylesheet. + * $message should be html-escaped + */ +function headless_message($message, $autoclose = false) { + if (DEFAULT_BURICHAN) { + $css = 'yotsubluenew'; + $ws = 'ws'; + } + else { + $css = 'yotsubanew'; + $ws = ''; + } + + $css_ver = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION; + + if ($error) { + $err_css = 'color: red;'; + } + else { + $err_css = ''; + } + + if ($autoclose) { + $js = <<setTimeout(function() { self.close(); }, 3000); +JS; + } + else { + $js = ''; + } + + $html = << + + + + + + + + + + +
    + $message +
    $js + + +HTML; + + return $html; +} + +function do_move_thread() { + if (!isset($_POST['id']) || !isset($_POST['board'])) { + updating_index(); + } + + if (!has_level()) { + updating_index(); + } + + $del = isset($_POST['move_del']) && $_POST['move_del']; + + $ret = move_thread($_POST['id'], $_POST['board'], $del); + + if (is_array($ret)) { + $message = 'Thread moved to /' . $ret[0] . '/' . $ret[1] . ''; + + echo headless_message($message, true); + } + else { + echo headless_message("$ret"); + } +} + +function do_copy_threads() { + if (!has_level()) { + updating_index(); + return; + } + global $argv; + + $to_board = $_REQUEST["board"]; + if (!$to_board) $to_board = $argv[2]; + + if (!$to_board) { + updating_index(); + return; + } + + set_time_limit( 0 ); + header( "Pragma: no-cache" ); + _print(fancystyle()); + + if (UPLOAD_BOARD || JANITOR_BOARD) { + $ret = "The current board doesn't support this feature."; + } else if ($to_board === 'f' || $to_board === 'j') { + $ret = "The destination board doesn't support this feature."; + } else { + $threads = mysql_column_array(mysql_board_call("SELECT no FROM `%s` WHERE resto = 0 AND archived = 0", BOARD_DIR)); + foreach ($threads as $thread) {$ret = copy_thread($thread, $to_board); _print("$thread
    \n");} + } + + if (is_array($ret)) { + $message = 'Thread copied to /' . $ret[0] . '/' . $ret[1] . ''; + + echo headless_message($message, true); + } + else { + echo headless_message("$ret"); + } +} + +function copy_thread($thread_id, $to_board, $delete = false) { + $thread_id = (int)$thread_id; + + if (!$thread_id) { + return "Invalid thread ID."; + } + + // Validate destination board + if (!preg_match('/^[a-z0-9]+$/', $to_board)) { + return 'Invalid destination board.'; + } + + $query = "SELECT COUNT(*) FROM boardlist WHERE dir = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $to_board); + + if (!$res) { + return "Database Error (1)"; + } + + if (mysql_num_rows($res) < 1) { + return "Destination board doesn't exist."; + } + + // --- + + // Fetch the whole thread + $posts = array(); + + $res = mysql_board_call("SELECT * FROM `%s` WHERE no = %d AND resto = 0", BOARD_DIR, $thread_id); + if (!$res) { + return "Database Error (3)"; + } + + $row = mysql_fetch_assoc($res); + if (!$row) { + return "Thread not found."; + } + + $posts[] = $row; + $res = mysql_board_call("SELECT * FROM `%s` WHERE resto = %d", BOARD_DIR, $thread_id); + if (!$res) { + return "Database Error (4)"; + } + + while ($row = mysql_fetch_assoc($res)) { + if (!$row) { + continue; + } + $posts[] = $row; + } + + // Copy posts to the other board + mysql_board_call('START TRANSACTION'); + + $new_resto = 0; + + $from_pids = array(); + $to_pids = array(); + + foreach ($posts as &$post) { + $comment = str_replace($from_pids, $to_pids, $post['com']); + + if ($new_resto === 0) { + $root_time = $post['root']; + } + else { + $root_time = 0; + } + + $query = "INSERT INTO `$to_board`(now,name,sub,com,host,pwd,filename,ext,w, +h,tn_w,tn_h, tim,time,last_modified,md5,fsize,root,resto,capcode, +4pass_id,since4pass,filedeleted,tmd5,id,sticky,closed,country) +VALUE (" . + "'" . $post['now'] . "'," . + "'" . mysql_real_escape_string($post['name']) . "'," . + "'" . mysql_real_escape_string($post['sub']) . "'," . + "'" . mysql_real_escape_string($comment) . "'," . + "'" . mysql_real_escape_string($post['host']) . "'," . + "'" . mysql_real_escape_string($post['pwd']) . "'," . + "'" . mysql_real_escape_string($post['filename']) . "'," . + "'" . $post['ext'] . "'," . + (int)$post['w'] . "," . + (int)$post['h'] . "," . + (int)$post['tn_w'] . "," . + (int)$post['tn_h'] . "," . + "'" . $post['tim'] . "'," . + (int)$post['time'] . "," . + (int)$post['time'] . "," . + "'" . $post['md5'] . "'," . + (int)$post['fsize'] . "," . + "'" . $root_time . "'," . + $new_resto . "," . + "'" . $post['capcode'] . "'," . + "'" . $post['4pass_id'] . "'," . + (int)$post['since4pass'] . "," . + (int)$post['filedeleted'] . "," . + "'" . $post['tmd5'] . "'," . + "'" . mysql_real_escape_string($post['id']) . "'," . + (int)$post['sticky'] . "," . + (int)$post['closed'] . "," . + "'XX')"; + + $res = mysql_board_call($query); + if (!$res) { + if ($new_resto === 0) { + mysql_board_call('ROLLBACK'); + return 'Database Error (5)'; + } + + $post['ext'] = null; + + continue; + } + + $new_pid = mysql_board_insert_id(); + + if ($new_resto === 0) { + $new_resto = $new_pid; + } + + $from_pids[] = ">>{$post['no']}"; + $to_pids[] = ">>$new_pid"; + + $post['new_id'] = $new_pid; + } + + unset($post); + + mysql_board_call('COMMIT'); + + // Copy files + // If the file already exists, update the database and set it as "deleted" + $to_img_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', IMG_ROOT) . $to_board . '/'; + $to_thumb_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', THUMB_ROOT) . $to_board . '/'; + + $dup_pids = array(); + + foreach ($posts as $post) { + if (!$post['ext'] || $post['filedeleted']) { + continue; + } + + $src_thumb = THUMB_DIR . $post['tim'] . 's.jpg'; + $dest_thumb = $to_thumb_dir . $post['tim'] . 's.jpg'; + + if (file_exists($dest_thumb)) { + //$dup_pids[] = $post['new_id']; + continue; + } + + $src_img = IMG_DIR . $post['tim'] . $post['ext']; + $dest_img = $to_img_dir . $post['tim'] . $post['ext']; + + @copy($src_thumb, $dest_thumb); + @copy($src_img, $dest_img); + } + + if (!empty($dup_pids)) { + $dup_clause = implode(',', $dup_pids); + $query = "UPDATE `$to_board` SET filedeleted = 1, ext = '' WHERE no IN($dup_clause)"; + $res = mysql_board_call($query); + } + + // Ask the destination board to build the new thread + remote_rebuild_live_thread($to_board, $new_resto); + + // Rebuild indexes + if (!STATIC_REBUILD) { + updatelog(0, 0); + } + + return array($to_board, $new_resto); +} + +function move_thread($thread_id, $to_board, $delete = false) { + if (UPLOAD_BOARD || BOARD_DIR === 'b' || JANITOR_BOARD) { + return "The current board doesn't support this feature."; + } + + if ($to_board === 'f' || $to_board === 'j') { + return "The destination board doesn't support this feature."; + } + + if ($to_board === BOARD_DIR) { + return "Invalid destination board."; + } + + $thread_id = (int)$thread_id; + + if (!$thread_id) { + return "Invalid thread ID."; + } + + // Validate destination board + if (!preg_match('/^[a-z0-9]+$/', $to_board)) { + return 'Invalid destination board.'; + } + + $query = "SELECT COUNT(*) FROM boardlist WHERE dir = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $to_board); + + if (!$res) { + return "Database Error (1)"; + } + + if (mysql_num_rows($res) < 1) { + return "Destination board doesn't exist."; + } + + // --- + + $board = mysql_real_escape_string(BOARD_DIR); + + // Lock the thread immediately + $query = "UPDATE `$board` SET closed = 1 WHERE no = $thread_id"; + + $res = mysql_board_call($query); + + if (!$res) { + return "Database Error (2)"; + } + + if (mysql_affected_rows() !== 1) { + return "This thread is locked."; + } + + // Fetch the whole thread + $posts = array(); + + $query = "SELECT * FROM `$board` WHERE no = $thread_id AND resto = 0"; + + $res = mysql_board_call($query); + + if (!$res) { + return "Database Error (3)"; + } + + $row = mysql_fetch_assoc($res); + + if (!$row) { + return "Thread not found."; + } + + if ($row['archived'] !== '0') { + return "You cannot move archived threads."; + } + + $posts[] = $row; + + $query = "SELECT * FROM `$board` WHERE resto = $thread_id"; + + $res = mysql_board_call($query); + + if (!$res) { + return "Database Error (4)"; + } + + while ($row = mysql_fetch_assoc($res)) { + if (!$row) { + continue; + } + $posts[] = $row; + } + + // Copy posts to the other board + mysql_board_call('START TRANSACTION'); + + $new_resto = 0; + + $from_pids = array(); + $to_pids = array(); + + foreach ($posts as &$post) { + $comment = str_replace($from_pids, $to_pids, $post['com']); + + if ($new_resto === 0) { + $root_time = 'NOW()'; + } + else { + $root_time = 0; + } + + if (SHOW_COUNTRY_FLAGS && $post['board_flag'] == '') { + $flag_val = $post['country']; + } + else { + $flag_val = 'XX'; + } + + $query = "INSERT INTO `$to_board`(now,name,sub,com,host,pwd,email,filename,ext,w, +h,tn_w,tn_h, tim,time,last_modified,md5,fsize,root,resto,capcode, +4pass_id,since4pass,filedeleted,tmd5,id,country) +VALUE (" . + "'" . $post['now'] . "'," . + "'" . mysql_real_escape_string($post['name']) . "'," . + "'" . mysql_real_escape_string($post['sub']) . "'," . + "'" . mysql_real_escape_string($comment) . "'," . + "'" . mysql_real_escape_string($post['host']) . "'," . + "'" . mysql_real_escape_string($post['pwd']) . "'," . + "'" . mysql_real_escape_string($post['email']) . "'," . + "'" . mysql_real_escape_string($post['filename']) . "'," . + "'" . $post['ext'] . "'," . + (int)$post['w'] . "," . + (int)$post['h'] . "," . + (int)$post['tn_w'] . "," . + (int)$post['tn_h'] . "," . + "'" . $post['tim'] . "'," . + (int)$post['time'] . "," . + (int)$post['time'] . "," . + "'" . $post['md5'] . "'," . + (int)$post['fsize'] . "," . + $root_time . "," . + $new_resto . "," . + "'" . $post['capcode'] . "'," . + "'" . $post['4pass_id'] . "'," . + (int)$post['since4pass'] . "," . + (int)$post['filedeleted'] . "," . + "'" . $post['tmd5'] . "'," . + "''," . + "'" . $flag_val . "')"; + + $res = mysql_board_call($query); + + if (!$res) { + if ($new_resto === 0) { + mysql_board_call('ROLLBACK'); + return 'Database Error (5)'; + } + + $post['ext'] = null; + + continue; + } + + $new_pid = mysql_board_insert_id(); + + if ($new_resto === 0) { + $new_resto = $new_pid; + } + + $from_pids[] = ">>{$post['no']}"; + $to_pids[] = ">>$new_pid"; + + $post['new_id'] = $new_pid; + } + + unset($post); + + mysql_board_call('COMMIT'); + + // Log the action + $thread = $posts[0]; + + $log_com = "From /$board/$thread_id to /$to_board/$new_resto"; + + if ($thread['com'] !== '') { + $log_com = $thread['com'] . '

    ' . $log_com; + } + + $action_log_post = array( + 'no' => $thread['no'], + 'name' => $thread['name'], + 'sub' => $thread['sub'], + 'com' => $log_com, + 'filename' => $thread['filename'], + 'ext' => $thread['ext'] + ); + + log_mod_action(6, $action_log_post); + + // Copy files + // If the file already exists, update the database and set it as "deleted" + $to_img_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', IMG_ROOT) . $to_board . '/'; + $to_thumb_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', THUMB_ROOT) . $to_board . '/'; + + $dup_pids = array(); + + foreach ($posts as $post) { + if (!$post['ext'] || $post['filedeleted']) { + continue; + } + + $src_thumb = THUMB_DIR . $post['tim'] . 's.jpg'; + $dest_thumb = $to_thumb_dir . $post['tim'] . 's.jpg'; + + if (file_exists($dest_thumb)) { + $dup_pids[] = $post['new_id']; + continue; + } + + $src_img = IMG_DIR . $post['tim'] . $post['ext']; + $dest_img = $to_img_dir . $post['tim'] . $post['ext']; + + copy($src_thumb, $dest_thumb); + copy($src_img, $dest_img); + } + + if (!empty($dup_pids)) { + $dup_clause = implode(',', $dup_pids); + $query = "UPDATE `$to_board` SET filedeleted = 1, ext = '' WHERE no IN($dup_clause)"; + $res = mysql_board_call($query); + } + + // Insert notification post if deletion is not requested + if (!$delete) { + $msg = sprintf(S_THREAD_MOVED, ">>>/$to_board/$new_resto"); + + $post_time = $_SERVER['REQUEST_TIME']; + $tim = generate_tim(); + + $query = "INSERT INTO `$board`(now,name,sub,com,host,pwd,filename,ext,w, +h,tn_w,tn_h, tim,time,last_modified,md5,fsize,resto,capcode, +4pass_id,tmd5,id) +VALUE (" . + "'" . date('m/d/y(D)H:i:s', $post_time) . "'," . + "'" . S_ANONAME . "'," . + "''," . + "'" . mysql_real_escape_string($msg) . "'," . + "'', '', '', '', 0, 0, 0, 0, '" . $tim . "'," . $post_time . "," . + $post_time . ", '',0," . $thread_id . ", 'mod', '', '', '')"; + + $res = mysql_board_call($query); + } + + // Ask the destination board to build the new thread + remote_rebuild_live_thread($to_board, $new_resto); + + // Delete source thread if deletion is requested + if ($delete) { + // id, pwd, imgonly, auto, die + delete_post($thread_id, 'trim', 0, 2, 1, 0); + } + // Archive source thread if archiving is enabled + else if (ENABLE_ARCHIVE) { + archive_thread($thread_id); + + if (!STATIC_REBUILD && ENABLE_JSON_THREADS) { + generate_board_archived_json(); + } + } + // Rebuild source thread and indexes if archiving is disabled + else { + updatelog($thread_id, 1); + } + + // Rebuild indexes + if (!STATIC_REBUILD) { + updatelog(0, 0); + } + + return array($to_board, $new_resto); +} + +function remote_rebuild_live_thread($board, $pid) { + $post = array( + 'mode' => 'rebuildadmin', + 'no' => $pid + ); + + rpc_start_request("https://sys.int/$board/imgboard.php", $post, $_COOKIE, true); + + return true; +} + +function archive_thread($thread_id) { + global $log; + + $thread_id = (int)$thread_id; + + $board = mysql_real_escape_string(BOARD_DIR); + + if (!$thread_id) { + return; + } + + // Regenerate the user ID before clearing the IP + $uid = ''; + + if (DISP_ID && DISP_ID_PER_THREAD && !DISP_ID_RANDOM) { + $th = null; + + if (!IS_REBUILDD && isset($log[$thread_id])) { + $th = $log[$thread_id]; + } + else { + $query = 'SELECT id, host FROM `' . BOARD_DIR . "` WHERE no = $thread_id"; + + $res = mysql_board_call($query); + + if ($res) { + $th = mysql_fetch_assoc($res); + } + } + + if ($th && $th['id'] !== '' && $th['host']) { + $uid = generate_uid($thread_id, $_SERVER['REQUEST_TIME'], $th['host']); + $uid = ", id = '" . mysql_real_escape_string($uid) . "'"; + } + } + + // Update the OP. "root" is used for archive pruning. + $query = <<(ID: $id)"; +} + +function emailencode( $str ) +{ + return str_replace( "%40", "@", rawurlencode( $str ) ); +} + +function renderPostHtml($no, $in_thread, $sorted_replies = null, $reply_count = null, $shown_replies = null, $is_archived = false) { + global $log, $board_flags_array; + + extract($log[$no]); + + $namestyle = ''; + + if( JANITOR_BOARD == 1 ) { + $namestyle = broomcloset_style( $name ); + $name = broomcloset_name( $name ); + } + + $mname = $name; + $mname_truncated = ''; + if( $capcode == 'none' && mb_strlen( $name ) > 30 ) { + $mname = explode( '', $name, 2 ); + if( mb_strlen( $mname[0] ) > 30 ) { + $mname[0] = htmlspecialchars_decode($mname[0], ENT_QUOTES); + $mname[0] = mb_substr( $mname[0], 0, 30 ) . '(...)'; + $mname[0] = htmlspecialchars($mname[0], ENT_QUOTES); + $mname_truncated = ' data-tip data-tip-cb="mShowFull"'; + } + + $mname = implode( '', $mname ); + } + + $hasCapcode = $capcode === 'none' ? '' : ' capcode'; + + // NEW CAPCODE STUFF + switch( $capcode ) { + case 'admin': + $capcodeStart = ' ## Admin'; + $capcode_class = ' capcodeAdmin'; + + $capcode = ' Admin Icon'; + $highlight = ''; + break; + + case 'founder': + $capcodeStart = ' ## Founder'; + $capcode_class = ' capcodeFounder'; + + $capcode = ' Founder Icon'; + $highlight = ''; + break; + + case 'admin_highlight': + $capcodeStart = ' ## Admin'; + $capcode_class = ' capcodeAdmin'; + + $capcode = ' Admin Icon'; + $highlight = ' highlightPost'; + break; + + case 'mod': + $capcodeStart = ' ## Mod'; + $capcode_class = ' capcodeMod'; + + $capcode = ' Mod Icon'; + $highlight = ''; + break; + + case 'developer': + $capcodeStart = ' ## Developer'; + $capcode_class = ' capcodeDeveloper'; + + $capcode = ' Developer Icon'; + $highlight = ''; + break; + + case 'manager': + $capcodeStart = ' ## Manager'; + $capcode_class = ' capcodeManager'; + + $capcode = ' Manager Icon'; + $highlight = ''; + break; + + case 'verified': + $capcodeStart = ' ## Verified'; + $capcode_class = ' capcodeVerified'; + + $capcode = ''; + $highlight = ''; + break; + + default: + $capcode = $capcodeStart = $highlight = $capcode_class = ''; + break; + } + + $spoiler = 0; + + if( strpos( $sub, 'SPOILER<>' ) === 0 ) { + $sub = substr( $sub, strlen( 'SPOILER<>' ) ); + $spoiler = 1; + } + + // Only OPs have subjects + if ($sorted_replies !== null) { + $subshortm = $sub; + if( mb_strlen( $sub ) > 30 ) { + $sub = str_replace( array(','), ',', $sub ); + $cutsub = htmlspecialchars_decode( $sub, ENT_QUOTES ); + $cutsub = mb_substr( $cutsub, 0, 30 ); + $cutsub = htmlspecialchars( $cutsub, ENT_QUOTES ); + + $subshortm = '' . $cutsub . '(...)'; + } + + $subshortm = '' . $subshortm . ' '; + $sub = '' . $sub . ' '; + } + else { + $sub = $subshortm = ''; + } + + $com = auto_link( $com, $in_thread ); + + if( !$in_thread ) { + list( $com, $abbreviated ) = abbreviate( $com, MAX_LINES_SHOWN ); + } + + if( isset( $abbreviated ) && $abbreviated ) { + $com .= '

    Comment too long. Click here to view the full text.'; + } + + // Image tag creation + $file = ''; + if( $ext ) { + $img = IMG_DIR . $tim . $ext; + $displaysrc = IMG_DIR2 . $tim . $ext; + + //if ($ext !== ".swf" && BOARD_DIR !== 'j') { + //if ($no % 100 >= 74) { + //$displaysrc = "//is2.4chan.org/" . BOARD_DIR . "/" . $tim . $ext; + //} + //} + + $linksrc = ( ( USE_SRC_CGI == 1 ) ? ( str_replace( '.cgi', '', IMG_DIR2 ) . $tim . $ext ) : $displaysrc ); + + if( defined( 'INTERSTITIAL_LINK' ) ) { + $linksrc = str_replace( INTERSTITIAL_LINK, '', $linksrc ); + } + + // Original filename truncation + $unescaped_filename = htmlspecialchars_decode($filename, ENT_QUOTES); + if( mb_strlen( $unescaped_filename, 'UTF-8' ) > 30 ) { + $shortname = mb_substr($unescaped_filename, 0, 25, 'UTF-8'); + $shortname = htmlspecialchars($shortname, ENT_QUOTES). '(...)' . $ext; + $longname = $filename . $ext; + $need_file_tooltip = true; + } + else { + $shortname = $longname = $filename . $ext; + $need_file_tooltip = false; + } + + if( THREAD_AD == 1 ) { + if( defined( 'THREAD_AD_TXT' ) && THREAD_AD_TXT ) { + $ad = text_link_ad( THREAD_AD_TXT ); + if( $ad ) + $dat .= "" . S_ADNAME . " : $ad
    "; + } + } + + $s_src = IMG_DIR . $tim . $ext; + + // 32>24 byte ascii>base64 conversion + $shortmd5 = base64_encode( pack( 'H*', $md5 ) ); + if( $fsize >= 1048576 ) { + $size = round( ( $fsize / 1048576 ), 2 ) . ' M'; + } + elseif( $fsize >= 1024 ) { + $size = round( $fsize / 1024 ) . ' K'; + } + else { + $size = $fsize . ' '; + } + + $ftype = strtoupper( substr( $ext, 1 ) ); + $mFileInfo = '
    ' . $size . 'B ' . $ftype . '
    '; + + if( !$tn_w && !$tn_h && $ext == '.gif' ) { + $tn_w = $w; + $tn_h = $h; + } + + $class = ''; + if( $spoiler ) { + $class = ' imgspoiler'; + // Replace 3 image tags with one, makes it easier to change in the future + $imgthumb_src = SPOILER_THUMB; + $tn_w = '100'; + $tn_h = '100'; + } + else { + //$imgthumb_src = thumb_url() . $tim . 's.jpg'; + $imgthumb_src = '//' . THUMB_DIR2_PART . $tim . 's.jpg'; + } + + if (MOBILE_IMG_RESIZE && $m_img) { + $m_img_attr = ' data-m'; + } + else { + $m_img_attr = ''; + } + + $imgsrc = '' . $size . 'B' . $mFileInfo . ''; + + if( $filedeleted ) { + $fileinfo = 'File deleted.'; + $imgsrc = ''; + } + else { + $dimensions = ( $ext == '.pdf' ) ? 'PDF' : $w . 'x' . $h; + if( !$spoiler ) { + $fileinfo = '
    ' . S_PICNAME . ': ' . $shortname . ' (' . $size . 'B, ' . $dimensions . ')
    '; + } + else { + $fileinfo = '
    ' . S_PICNAME . ': Spoiler Image (' . $size . 'B, ' . $dimensions . ')
    '; + } + } + + $file = << + $fileinfo + $imgsrc + +HTML; + } + + /** + * OP specific html + */ + if ($sorted_replies !== null) { + if ($in_thread) { + $href = ''; + $postinfo_extra = ''; + } + else { + $href = RES_DIR2 . $no . PHP_EXT2; + + if ($semantic_url !== '') { + $semantic_url = "/$semantic_url"; + } + + $postinfo_extra = '   [' . S_REPLY . ']'; + } + + $oldtext = ''; + $extra = ''; + + // Marked for deletion (old) + if (isset($log[$no]['old'])) { + $oldtext .= '' . S_OLD . '
    '; + } + + $postInfo = ''; + + // Count omitted replies and images + if (!$in_thread) { + $s = $reply_count - $shown_replies; + + if ($shown_replies) { + $t = 0; + $total_t = 0; + + $cur = 1; + + while ($s >= $cur) { + list($row) = each($sorted_replies); + + if ($log[$row]['fsize'] && !$log[$row]['filedeleted']) { + $t++; + } + + $cur++; + } + + $total_t = $t; + + while ($reply_count >= $cur) { + list($row) = each($sorted_replies); + + if ($log[$row]['fsize'] && !$log[$row]['filedeleted']) { + $total_t++; + } + + $cur++; + } + + if ($reply_count != 0) { + reset($sorted_replies); + } + } + else { + $total_t = $t = $imgreplycount; + } + + // desktop + $posts = ( $s < 2 ) ? ' reply' : ' replies'; + $replies = ( $t < 2 ) ? ' image' : ' images'; + + if (( $s > 0 ) && ( $t == 0 )) { + $extra .= '' . $s . $posts . ' omitted. Click here to view.'; + } + elseif (( $s > 0 ) && ( $t > 0 )) { + $extra .= '' . $s . $posts . ' and ' . $t . $replies . ' omitted. Click here to view.'; + } + + // mobile + $posts = ( $reply_count < 2 ) ? ' Reply' : ' Replies'; + $replies = ( $total_t < 2 ) ? ' Image' : ' Images'; + + if (( $reply_count > 0 ) && ( $total_t == 0 )) { + // Text replies only + $info = '' . $reply_count . $posts . ''; + } + elseif (( $reply_count > 0 ) && ( $total_t > 0 )) { + // Image replies + $info = '' . $reply_count . $posts . ' / ' . $total_t . $replies . ''; + } + else { + // nothing + $info = ''; + } + + $postInfo = << + + $info + + + View Thread + +HTML; + } + else { + $s = 0; + } + + // Sticky - Closed + $threadmodes = ''; + + if ($sticky == 1) { + $threadmodes .= ' Sticky'; + } + + if ($closed == 1) { + if ($archived) { + $threadmodes .= ' Archived'; + } + else { + $threadmodes .= ' Closed'; + } + } + + // Staff replies indicator + if (META_BOARD) { + $posts = meta_is_thread_flagged($sorted_replies); + + // admin = 0 + // dev = 1 + // mod = 2 + // manager = 3 + + // array (css_class, text_name) + $larr = array( + 0 => array('Admin', 'Administrator'), + 1 => array('Developer', 'Developer'), + 2 => array('Mod', 'Moderator'), + 3 => array('Manager', 'Manager') + ); + + if ($posts[0] || $posts[1] || $posts[2] || $posts[3]) { + $com .= '

    '; + + foreach ($posts as $key => $postlist) { + if (!$postlist) { + continue; + } + + $postlist = explode(',', $postlist); + + $replies = count($postlist) > 1 ? 'Replies' : 'Reply'; + $com .= '' . $larr[$key][1] . ' ' . $replies . ': '; + + foreach ($postlist as $postnum) { + $com .= '>>' . $postnum . ' '; + } + + $com .= '
    '; + } + + $com .= '
    '; + + } + } + + if (DISP_ID && DISP_ID_PER_THREAD && !$is_archived && !DISP_ID_RANDOM && $id !== '') { + $id = generate_uid($no, $time, $host); + } + + $reply_file = ''; + $op_file = $file; + $post_class = 'op'; + $sidearrows = ''; + } + /** + * Reply + */ + else { + $href = $in_thread ? '' : RES_DIR2 . $resto . PHP_EXT2; + $threadmodes = $postinfo_extra = $oldtext = $postInfo = $extra = $op_file = ''; + $reply_file = $file; + $post_class = 'reply'; + $sidearrows = '
    >>
    '; + + $sub = ''; + } + + $dispuid = ''; + $dispuid = display_uid( $id, $capcode ); + if( $dispuid == '' || $capcode != '' ) { + $dispuid = $capcode; + } + else { + $capcodeStart = ''; + if (FORCED_ANON) { + $capcode_class = ''; + } + } + + $countryFlag = ''; + if ($capcode == '') { + if (ENABLE_BOARD_FLAGS && $board_flag != '' && isset($board_flags_array[$board_flag])) { + $cname = board_flag_code_to_name($board_flag); + $countryFlag = ' '; + } + else if (SHOW_COUNTRY_FLAGS) { + $cname = country_code_to_name($country); + $countryFlag = ' '; + } + } + + $quote = $in_thread ? 'javascript:quote(\'' . $no . '\');' : $href . '#q' . $no; + + $postM = ''; + + // Forced anon on meta boards + if (META_BOARD && $capcode_class != ' capcodeAdmin') { + $name = $mname = S_ANONAME; + } + + if (FORCE_COM && !$capcode) { + $com = FORCE_COM_TEXT; + } + + $display_no = display_no($no); + + if ($since4pass && $capcode == '' && $since4pass < 10000) { + $since4passTag = " "; + } + else { + $since4passTag = ''; + } + + // April 2024 + if ($since4pass && $capcode == '' && $since4pass >= 10000) { + $since4passTag = april_2024_get_name_badge($since4pass); + $_xa24_post_cls = april_2024_get_post_cls($since4pass); + } + else { + $_xa24_post_cls = ''; + } + + return <<$sidearrows +
    + + + $op_file + + + $reply_file +
    $com
    +
    + $postInfo + $oldtext + + $extra +HTML; +} + +// deletes a post from the database +// imgonly: whether to just delete the file or to delete from the database as well +// automatic: always delete regardless of password/admin (for self-pruning) +// children: whether to delete just the parent post of a thread or also delete the children +// die: whether to die on error +// careful, setting children to 0 could leave orphaned posts. +function delete_post($resno, $pwd, $imgonly = 0, $automatic = 0, $children = 1, $die = 1, $lazy_rebuild = false, $archived_deletion = false, $tool = null, $user_is_known = true) +{ + global $log; + + $resno = intval( $resno ); + log_cache( 0, $resno, $archived_deletion ? 1 : 0 ); + + $post_exists = true; + + if (!isset( $log[$resno])) { + if ($die) { + //error( "Can't find the post $resno." ); + updating_index(); + die(); + } + else { + if ($automatic) { + return 0; + } + + $post_exists = false; + } + } + + $row = $log[$resno]; + + if (!$automatic && !has_level('janitor')) { + $cant_del = $cant_del_old = false; + + if ($row['resto']) { + $cant_del = NO_DELETE_REPLY; + } + else { + $cant_del = NO_DELETE_OP; + } + + if ($_SERVER['REQUEST_TIME'] - $log[$resno]['time'] >= RENZOKU_DEL_CANT_AFTER) { + $cant_del = true; + $cant_del_old = true; + } + + if ($cant_del) { + error($cant_del_old ? S_RENZOKU_DEL_CANT_AFTER : S_MAYNOTDEL); + } + } + + // if (!$row['pwd'] && BOARD_DIR=='c') { + // echo ""; + // } + + if ($archived_deletion) { + // Only authed users can delete archived posts + $pass_ok = false; + $host_ok = false; + } + else { + $pass_ok = $pwd && $pwd === $row['pwd']; + $host_ok = $row['host'] == $_SERVER['REMOTE_ADDR']; + } + + $admin_ok = has_level() || can_delete( $resno ); + $can_flood_delete = has_level( 'janitor' ); + + $can_delete = $automatic || $pass_ok || $host_ok || $admin_ok; + + // quick_log_to( "/www/perhost/del.log", date( "r" ) . " deletion of #$resno by " . $_SERVER['REMOTE_ADDR'] . " pwd \"$pwd\" auto " . (int)$automatic . " adminok " . (int)$admin_ok . " hostok " . (int)$host_ok . " passok " . (int)$pass_ok . " orig ip " . $row['host'] . " pwd " . $row['pwd'] . " com " . substr( $row["com"], 0, 50 ) . " " . ( $can_delete ? "succeeded" : "failed" )."\n" ); + + if( !$can_delete ) error( S_BADDELPASS ); + + if ($row['sticky']) { + if (has_level() && !has_level('admin') && !$automatic && !$archived_deletion) { + if ($die) { + error(S_MAYNOTDELSTICKY); + } + else { + return false; + } + } + + if (!has_level()) { + if ($die) { + error(S_MAYNOTDEL); + } + else { + return false; + } + } + } + + // FIXME: remove this and set NO_DELETE_OP on vg + if( BOARD_DIR == 'vg' && !$row['resto'] && !$admin_ok && !$archived_deletion ) error( S_MAYNOTDEL ); + + if( !$row['resto'] && !$automatic && !$admin_ok ) { + foreach( $row['children'] as $child => $unused ) { + if( $log[$child]['capcode'] != 'none' ) error( S_MAYNOTDEL ); + } + } + + if( !$automatic && !$admin_ok && !$can_flood_delete ) { + if ($user_is_known) { + $_renzoku_del = RENZOKU_DEL; + } + else { + $_renzoku_del = 600; // FIXME: 10 minutes + } + if( ( time() - (int)$row['time'] ) < $_renzoku_del ) { + error(S_RENZOKU_DEL); + } + } + + // User is authed staff + if ($admin_ok && !IS_REBUILDD) { + $auser = $_COOKIE['4chan_auser']; + + // Use POSTed IP instead of the local one if the deletion was triggered via RPC + if (isset($_POST['remote_addr']) && is_local()) { + $remote_addr = $_POST['remote_addr']; + } + else { + $remote_addr = $_SERVER['REMOTE_ADDR']; + } + + // Authed user is deleting a post that isn't his + if (!$pass_ok && !$host_ok) { + $adfsize = ( $row['fsize'] > 0 ) ? 1 : 0; + $adname = str_replace( ' !', '#', $row['name'] ); + if( $imgonly ) { + $imgonly = 1; + } else { + $imgonly = 0; + } + + if (isset($_POST['template_id'])) { + $template_id = (int)$_POST['template_id']; + } + else { + $template_id = 0; + } + + if ($post_exists && (!$automatic || $automatic === 2)) { + validate_admin_cookies(); + + if (!$tool || !in_array($tool, array('search', 'ban', 'ban-req', 'autopurge', 'threadban'))) { + $tool = ''; + } + + mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,admin_ip,template_id,tool) values('%s',%d, %d,'%s','%s','%s','%s','%s','%s','%s', '%s', %d, '%s')", $imgonly, $resno, $row['resto'], SQLLOG, $adname, $row["sub"], $row["com"], $adfsize, $row["filename"].$row["ext"], $auser, $remote_addr, $template_id, $tool ); + } + + // Clear the report queue if only the file is deleted + if ($imgonly) { + $query = "DELETE FROM reports WHERE board = '" . SQLLOG . "' AND no = " . (int)$resno; + + $res = mysql_global_call($query); + + $query = "DELETE FROM reports_for_posts WHERE board = '" . SQLLOG . "' AND postid = " . (int)$resno; + + $res = mysql_global_call($query); + } + } + // Staff member is deleting a post that is his, or other type of deletions + else if (!$automatic || $automatic === 2) { + if ($auser) { + log_staff_event('staff_self_del', $auser, $remote_addr, $pwd, BOARD_DIR, $row); + } + else { + log_staff_event('staff_auto_del', 'Auto-ban', $remote_addr, $pwd, BOARD_DIR, $row); + } + } + } + + $delete_children = $row['resto'] == 0 && $children && !$imgonly; + + $restoq = $delete_children ? "OR (archived = 0 and resto=$resno) OR (archived = 1 and resto=$resno)" : ''; + + if (UPLOAD_BOARD) { + $up_col = ',filename'; + } + else { + $up_col = ''; + } + + if (MOBILE_IMG_RESIZE) { + $up_col .= ',m_img'; + } + + $result = mysql_board_call( "select no,resto,tim,ext$up_col from `" . SQLLOG . "` where no=$resno $restoq" ); + + // Array of threads to update after one or more replies were deleted. + $updated_threads = array(); + // Array of post number for report and xff clearing + $deleted_threads = array(); + $deleted_replies = array(); + + $purge_files = array(); + + $img_webroot = 'http://i.4cdn.org/' . BOARD_DIR . '/'; + + while( $delrow = mysql_fetch_array( $result ) ) { + // delete + if( $delrow['ext'] ) { + if (UPLOAD_BOARD) { + $delfile = IMG_DIR . $delrow['filename'] . $delrow['ext']; //path to delete + @unlink($delfile); // delete image + if( CLOUDFLARE_PURGE_ON_DEL ) { + cloudflare_purge_url($img_webroot . rawurlencode($delrow['filename']) . $delrow['ext'], true); + } + } + else { + $delfile = IMG_DIR . $delrow['tim'] . $delrow['ext']; //path to delete + $delthumb = THUMB_DIR . $delrow['tim'] . 's.jpg'; + @unlink( $delfile ); // delete image + @unlink( $delthumb ); // delete thumb + + if (ENABLE_OEKAKI_REPLAYS && file_exists(IMG_DIR . $delrow['tim'] . '.tgkr')) { + unlink(IMG_DIR . $delrow['tim'] . '.tgkr'); + + if (CLOUDFLARE_PURGE_ON_DEL) { + $purge_files[] = $img_webroot . $delrow['tim'] . '.tgkr'; + } + } + + if (MOBILE_IMG_RESIZE && $delrow['m_img']) { + @unlink(IMG_DIR . $delrow['tim'] . 'm.jpg'); // delete mobile + } + + if (CLOUDFLARE_PURGE_ON_DEL) { + $purge_files[] = $img_webroot . $delrow['tim'] . $delrow['ext']; + $purge_files[] = $img_webroot . $delrow['tim'] . 's.jpg'; + + if (MOBILE_IMG_RESIZE && $delrow['m_img']) { + $purge_files[] = $img_webroot . $delrow['tim'] . 'm.jpg'; + } + + } + } + } + if( $imgonly ) { + mysql_board_call( "UPDATE `" . SQLLOG . "` SET filedeleted=1,root=root,last_modified=%d WHERE no=%d", $_SERVER['REQUEST_TIME'], $delrow['no'] ); + $log[$delrow['no']]['filedeleted'] = TRUE; + + if ($delrow['resto']) { + mysql_board_call( "UPDATE `" . SQLLOG . "` SET root=root,last_modified=%d WHERE no=%d", $_SERVER['REQUEST_TIME'], $delrow['resto'] ); + if (isset($log[$delrow['resto']])) + $log[$delrow['resto']]['last_modified'] = (int)$_SERVER['REQUEST_TIME']; + } + + //cloudflare_purge_by_basename(BOARD_DIR, $delrow['tim'] . $delrow['ext']); + } + else { + // Thread + if (!$delrow['resto']) { + $thread_key = @array_search( $delrow['no'], $log['THREADS'] ); + if( $thread_key !== false ) { + unset( $log['THREADS'][$thread_key] ); + } + + if( USE_GZIP == 1 ) { + @unlink( RES_DIR . $delrow['no'] . PHP_EXT . '.gz' ); + @unlink( RES_DIR . $delrow['no'] . '.json.gz' ); + } + else { + @unlink( RES_DIR . $delrow['no'] . PHP_EXT ); + @unlink( RES_DIR . $delrow['no'] . '.json' ); + } + + update_json_tail_deletion($delrow['no'], true); + + $deleted_threads[] = (int)$delrow['no']; + } + // Reply. Thread's last_modified field will need to be updated + else if (!isset($updated_threads[$delrow['resto']])) { + $updated_threads[$delrow['resto']] = true; + + $deleted_replies[] = (int)$delrow['no']; + } + + unset( $log[$delrow['no']] ); + } + } + + if (!empty($purge_files)) { + cloudflare_purge_url($purge_files, true); + } + + // Updating last_modified field (threads) + foreach ($updated_threads as $thread_id => $true) { + mysql_board_call("UPDATE `".SQLLOG."` set root=root,last_modified=%d where no=%d", $_SERVER['REQUEST_TIME'], $thread_id); + + if (isset($log[$thread_id])) + $log[$thread_id]['last_modified'] = (int)$_SERVER['REQUEST_TIME']; + + unset( $log[$thread_id]['children'][$delrow['no']] ); + } + + // Clearing reports and xff + if ($deleted_replies) { + $in_clause = 'IN(' . implode(',', $deleted_replies) . ')'; + mysql_global_do("DELETE FROM reports WHERE board='" . BOARD_DIR . "' AND no " . $in_clause); + mysql_global_do("DELETE FROM reports_for_posts WHERE board='" . BOARD_DIR . "' AND postid " . $in_clause); + + if (SAVE_XFF) { + mysql_global_do("UPDATE xff SET is_live = 0 WHERE board='" . BOARD_DIR . "' AND postno " . $in_clause); + } + } + + if ($deleted_threads) { + $in_clause = 'IN(' . implode(',', $deleted_threads) . ')'; + mysql_global_do("DELETE FROM reports WHERE board='" . BOARD_DIR . "' AND (no $in_clause OR resto $in_clause)"); + mysql_global_do("DELETE FROM reports_for_posts WHERE board='" . BOARD_DIR . "' AND (postid $in_clause OR threadid $in_clause)"); + + if (SAVE_XFF) { + mysql_global_do("UPDATE xff SET is_live = 0 WHERE board='" . BOARD_DIR . "' AND postno " . $in_clause); + } + } + + // Halloween 2017 + /* + if ($tool && defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky2017') { + if ($tool === 'ban') { + decrease_halloween_score($resno); + } + else if ($tool === 'ban-req') { + decrease_halloween_score($resno, 0.90); + } + } + */ + + //delete from DB + if( $delete_children ) // delete thread and children + $result = mysql_board_call( "delete from `" . SQLLOG . "` where no=$resno or resto=$resno" ); + elseif( !$imgonly ) // just delete the post + $result = mysql_board_call( "delete from `" . SQLLOG . "` where no=$resno" ); + + rpc_task(); + if( $imgonly && $row['resto'] == 0 ) { + return $resno; // return thread number to stop deletion silliness + } + + return $row['resto']; // so the caller can know what pages need to be rebuilt +} + +function rebuild_deletions($rebuild, $lazy_rebuild = false) +{ + global $log; + + foreach( $rebuild as $key => $val ) { + log_cache( 0, $key ); + if (!isset($log[$key]['children'])) { + internal_error_log("rebuild_deletions", "missing children for OP /" . BOARD_DIR . "/$key"); + continue; + } + updatelog( $key, 1 ); // leaving the second parameter as 0 rebuilds the index each time! + update_json_tail_deletion($key); + } + + if( STATIC_REBUILD ) return; + + updatelog(0, 0, $lazy_rebuild); // update the index page last +} + +/** + * Removes old archived posts + */ +function trim_archive() { + if (STATIC_REBUILD && !IS_REBUILDD) { + return; + } + + if (!ARCHIVE_MAX_AGE) { + return; + } + + $interval = (int)ARCHIVE_MAX_AGE; + + $query = << 0 ) ? ( PAGE_MAX * DEF_PAGES ) : 0; + + $threads = array(); + + $rebuild_archive_list = false; + + $rebuild_archive_json = false; + + if (ENABLE_ARCHIVE) { + if (IS_REBUILDD) { + clearstatcache(true, INDEX_DIR . 'archive' . PHP_EXT . '.gz'); + } + + if (filemtime(INDEX_DIR . 'archive' . PHP_EXT . '.gz') < time() - (int)ARCHIVE_REBUILD_DELAY) { + $rebuild_archive_list = true; + } + } + + // New max-page method + if( $maxthreads ) { + $exp_order = 'no'; + if( EXPIRE_NEGLECTED == 1 ) $exp_order = 'root'; + //logtime( 'trim_db before select threads' ); + $result = mysql_board_call( "SELECT no FROM `" . SQLLOG . "` WHERE archived=0 AND sticky=0 AND undead=0 AND resto=0 ORDER BY $exp_order ASC" ); + //logtime( 'trim_db after select threads' ); + $threadcount = mysql_num_rows( $result ); + + if (!$threadcount && $rebuild_archive_list) { + $rebuild_archive_list = false; + } + + while( $row = mysql_fetch_array( $result ) and $threadcount > $maxthreads ) { + if (ENABLE_ARCHIVE) { + $rebuild_archive_json = true; + archive_thread($row['no']); + } + else { + delete_post( $row['no'], 'trim', 0, 1 ); // imgonly=0, automatic=1, children=1 + } + $threads[$row['no']] = 1; + $threadcount--; + } + + mysql_free_result( $result ); + + if (ENABLE_ARCHIVE) { + if ($rebuild_archive_list) { + rebuild_archive_list(); + } + + if ($rebuild_archive_json && ENABLE_JSON_THREADS) { + generate_board_archived_json(); + } + } + + // Original max-posts method (note: cleans orphaned posts later than parent posts) + } else { + // make list of stickies + $stickies = array(); // keys are stickied thread numbers + $undead = array(); + // COMBINE FOR MAXIMUM EFFICIENCY! + $result = mysql_board_call( "SELECT no from `" . SQLLOG . "` where (sticky=1 OR undead=1) and resto=0" ); + while( $row = mysql_fetch_array( $result ) ) { + if( $row['sticky'] ) $stickies[$row['no']] = 1; + if( $row['undead'] ) $undead[$row['no']] = 1; + } + + // FIXME these if ... continue checks need to be SQL conditions! + $result = mysql_board_call( "SELECT no,resto,sticky FROM `" . SQLLOG . "` ORDER BY no ASC" ); + $postcount = mysql_num_rows( $result ); + while( $row = mysql_fetch_array( $result ) and $postcount >= $maxposts ) { + // don't delete if this is a sticky thread or is undeletable + if( $row['sticky'] == 1 || $row['undead'] == 1 ) continue; + // don't delete if this is a REPLY to a sticky or is in an undeletable thread + if( $row['resto'] != 0 && ( $stickies[$row['resto']] == 1 || $undead[$row['resto']] == 1 ) ) continue; + delete_post( $row['no'], 'trim', 0, 1, 0 ); // imgonly=0, automatic=1, children=0 + $threads[$row['no']] = 1; + $postcount--; + } + mysql_free_result( $result ); + } +} + +// FIXME archives +// debug function, deletes all archived threads +function purge_archive() { + $query = "SELECT no FROM `test` WHERE archived = 1 AND resto = 0"; + + $res = mysql_board_call($query); + + if (!$res) { + return; + } + + while ($thread = mysql_fetch_assoc($res)) { + echo "Deleting {$thread['no']}
    "; + delete_post((int)$thread['no'], '', 0, 0, 1, true, false, true); + } +} + +function rebuild_archived_thread($thread_id) { + global $log; + + log_cache(0, $thread_id, 1); + + if (!isset($log[$thread_id])) { + return false; + } + + // Build the JSON + if (ENABLE_JSON) { + $tailSize = get_json_tail_size($thread_id); + + if ($tailSize) { + generate_thread_json($thread_id, false, false, false, $tailSize); + } + else { + update_json_tail_deletion($thread_id); + } + + generate_thread_json($thread_id); + } + + // Build the HTML + $dat = ''; + + head($dat, $thread_id); + form($dat, $thread_id); + + $dat .= '
    +
    +
    +'; + + $reply_count = $log[$thread_id]['replycount']; + + // Open thread tag and render OP + $sorted_replies = $log[$thread_id]['children']; + ksort($sorted_replies); + + $dat .= '
    ' + . renderPostHtml($thread_id, $thread_id, $sorted_replies, $reply_count, null, true); + + // Render replies + $repCount = 0; + + while (list($resrow) = each($sorted_replies)) { + if (!$log[$resrow]['no']) { + break; + } + + $dat .= renderPostHtml($resrow, $thread_id, null, null, null, true); + + $repCount++; + } + + // Close thread tag + $dat .= ' +
    +
    +'; + + $dat .= '
    '; + + // Close board tag + $lang = S_FORM_REPLY; + + $dat .= ' + +
    '; + + $dat .= '
    '; + + /** + * ADS + */ + + if (defined('AD_ADGLARE_BOTTOM') && AD_ADGLARE_BOTTOM) { + $dat .= '

    '; + } + + if (defined('AD_ADGLARE_BOTTOM_MOBILE') && AD_ADGLARE_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + if (defined('AD_RC_BOTTOM') && AD_RC_BOTTOM) { + $dat .= '

    '; + } + + if (defined('AD_BSA_BOTTOM') && AD_BSA_BOTTOM) { + $dat .= AD_BSA_BOTTOM; + } + + if (defined('AD_RC_BOTTOM_MOBILE') && AD_RC_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + if (defined('AD_ADNIUM_BOTTOM_MOBILE') && AD_ADNIUM_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) { + $dat .= '

    '; + } + /* + if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM) { + $dat .= '
    '; + } + else if (defined('AD_TERRA_BOTTOM_DESKTOP') && AD_TERRA_BOTTOM_DESKTOP) { + $_ad_info = explode(',', AD_TERRA_BOTTOM_DESKTOP); + $dat .= '
    '; + } + */ + if (defined('ADS_DANBO') && ADS_DANBO) { + $dat .= '

    '; + } + if (defined('AD_CUSTOM_BOTTOM') && AD_CUSTOM_BOTTOM) { + $dat .= '
    ' . AD_CUSTOM_BOTTOM . '
    '; + } + + $resredir = ''; + + // deletion mode is "arcdel" instead of "usrdel" + $dat .= '
    ' + . S_REPDEL . $resredir . ' [' + . S_DELPICONLY . '] '; + + if (!defined('CSS_FORCE')) { + $dat .= 'Style: + + '; + } + + $dat .= '
    '; + + foot($dat); + + // Write the page + print_page(RES_DIR . $thread_id . PHP_EXT, $dat); +} + +function calculate_indexes_to_rebuild( $updated_thread ) +{ + global $index_rbl; + $query = mysql_board_call( "SELECT COUNT(no) FROM `%s` WHERE archived = 0 AND root > (SELECT root FROM `%s` WHERE no=%d)", SQLLOG, SQLLOG, $updated_thread ); + $index_rbl = floor( mysql_result( $query, 0, 0 ) / DEF_PAGES ); +} + +function rebuild_indexes_daemon() +{ + global $index_rbl, $index_last_thread, $index_last_post, $log; + static $index_arr = array(); + + $index_rbl = PAGE_MAX; + + // Get latest thread + $query = mysql_board_call( "SELECT max(no) last_post, max(resto) last_thread FROM `%s` WHERE archived = 0", SQLLOG ); + $q = mysql_fetch_assoc( $query ); + + $latest_thread = $q['last_thread']; + $latest_post = $q['last_post']; + + if( $index_last_thread != $latest_thread ) { + // cry :( + $index_last_thread = $latest_thread; + $index_last_post = $latest_post; + + updatelog(); + + if (ENABLE_JSON_THREADS && ENABLE_ARCHIVE) { + generate_board_archived_json(); + } + + return; + } + + if( $index_last_post != $latest_post ) { + $index_last_thread = $latest_thread; + $index_last_post = $latest_post; + + $post_arr = $log['THREADS']; + + // Now we know we're not going to have identical arrays. + $count = count( $post_arr ); + $last_seen_thread = 0; + + for( $i = 0; $i < $count; $i++ ) { + if( $post_arr[$i] != $index_arr[$i] ) $last_seen_thread = $i; + } + + $index_rbl = floor( $last_seen_thread / DEF_PAGES ); + $index_arr = $post_arr; + + updatelog(); + + return; + } + + // YAY NOTHING TO UPDATE! + return; +} + +function style_group() +{ + return ( DEFAULT_BURICHAN == 1 ) ? "ws_style" : "nws_style"; +} + +function rebuildd_stats() +{ + if (!IS_REBUILDD) return ""; + + global $update_avg_secs; + global $rpc_chs; + + $avgtime = $update_avg_secs; + + $memuse = (int)(memory_get_usage(true) / 1024); + $peakmemuse = (int)(memory_get_peak_usage(true) / 1024); + $rpccount = count($rpc_chs); + + return ""; +} + +// Changes relative board urls to absolute //board.4chan.org urls +// mostly for /j/ and error pages on sys.4chan +function fix_board_nav($nav, $fix_protocol = false) { + if ($fix_protocol) { + $protocol = (stripos($_SERVER["HTTP_REFERER"], "https") === 0) ? 'https:' : 'http:'; + } + else { + $protocol = ''; + } + + return preg_replace('/href="\/([a-z0-9]+)\/"/', "href=\"$protocol//boards." . L::d(BOARD_DIR) . "/$1/\"", $nav); +} + +// Same but for /archive lmao +function fix_board_nav_archive($nav) { + $nav = preg_replace('/href="\/([a-z0-9]+)\/"/', 'href="/$1/archive"', $nav); + $nav = preg_replace('/href="\/f\/archive"/', 'href="/f/"', $nav); + $nav = preg_replace('/href="\/b\/archive"/', 'href="/b/"', $nav); + + return $nav; +} + +function head( &$dat, $res, $error = 0, $page = 0, $npages = 0, $is_arclist = false ) +{ + //( $dat, 0, 0, 0, 0, true ) + global $log, $thread_unique_ips; + + $titlepart = $rta = $favicon = $css = $rss = $subtitle = $extra = ''; + + $includenav = file_get_contents_cached(NAV_TXT); + + if( JANITOR_BOARD == 1 ) { + $dat .= broomcloset_head( $dat ); + $includenav = fix_board_nav($includenav); + } + else if ($error) { + $includenav = fix_board_nav($includenav, true); + } + else if ($is_arclist) { + $includenav = fix_board_nav_archive($includenav); + } + + if( TITLE_IMAGE_TYPE == 1 ) { + $titleimg = rand_from_flatfile( YOTSUBA_DIR, 'title_banners.txt' ); + //$titleimg = STATIC_SERVER . 'image/title/' . $titleimg; + + $titlepart .= '
    '; + } elseif( TITLE_IMAGE_TYPE == 2 ) { + $titlepart .= ''; + } + + if( defined( 'SUBTITLE' ) ) { + $subtitle = '
    ' . SUBTITLE . '
    '; + } + + // CSS Workings + $cssVersion = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION; + $defaultcss = ( DEFAULT_BURICHAN == 1 ) ? 'yotsubluenew' : 'yotsubanew'; + $mobilecss = ( ( DEFAULT_BURICHAN == 1 ) ? 'yotsublue' : 'yotsuba' ) . 'mobile.' . $cssVersion . '.css'; + + $styles = array( + 'Yotsuba New' => "yotsubanew.$cssVersion.css", + //'Yotsuba' => "yotsuba.$cssVersion.css", + 'Yotsuba B New' => "yotsubluenew.$cssVersion.css", + 'Futaba New' => "futabanew.$cssVersion.css", + 'Burichan New' => "burichannew.$cssVersion.css", + 'Photon' => "photon.$cssVersion.css", + 'Tomorrow' => "tomorrow.$cssVersion.css" + ); + + // /j/ versioning fix + if( BOARD_DIR == 'j' ) { + $css = ''; + $extra = << + document.addEventListener('mousedown', function(e) { + var t = e.target; + if (t === document) { + return; + } + if (/^>>>\//.test(t.textContent) && /sys\.4chan/.test(t.href)) { + t.href = t.href.replace('sys.4chan', 'boards.4chan'); + } + }, false); + +JJS; + } else { + if( defined( 'CSS_FORCE' ) ) { + foreach( $styles as $style => $stylecss ) { + $rel = ( $style == 'Yotsuba New' ) ? 'stylesheet' : 'alternate stylesheet'; + $css .= ''; + } + } + else { + $dcssl = $defaultcss . '.' . $cssVersion . '.css'; + $css .= ''; + foreach( $styles as $style => $stylecss ) { + $css .= ''; + } + + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) { + $css .= ''; + } + } + + // Christmas 2021 + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'tomorrow') { + $extra = << + + +JJS; + } + + // Halloween spooky.css + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky') { + $extra = << + function fc_skelrot(e) { + var el, idx, thres, max; + if (e && e.detail && e.detail.count) { + thres = 0.33; + } + else { + thres = 0.0; + } + max = 23; + if (Math.random() < thres) { + return; + } + if (el = document.getElementById('skellington')) { + el.parentNode.removeChild(el); + } + idx = 1 + Math.floor(Math.random() * max); + el = document.createElement('img'); + el.id = 'skellington'; + el.className = 'desktop' + (Math.random() < 0.25 ? ' topskel' : ''); + el.alt = ''; + if (Math.random() < 0.01) { + el.src = '//s.4cdn.org/image/temp/dinosaur.gif'; + } + else { + el.src = '//s.4cdn.org/image/skeletons/' + idx + '.gif'; + } + document.body.insertBefore(el, document.body.firstElementChild); + } + function fc_spooky_init() { + if (window.matchMedia && window.matchMedia('(min-width: 481px)').matches) { + document.addEventListener('4chanThreadUpdated', fc_skelrot, false); + window.dark_captcha = true; + fc_skelrot(); + } + } + function fc_spooky_cleanup() { + var el = document.getElementById('skellington'); + window.dark_captcha = false; + document.removeEventListener('4chanThreadUpdated', fc_skelrot, false); + el && el.parentNode.removeChild(el); + } + +JJS; + } + } + + $css .= ''; + + // April 2024 + $css .= ''; + + if (SHOW_COUNTRY_FLAGS) { + $css .= ''; + } + + if (ENABLE_BOARD_FLAGS) { + $_flags_type = (defined('BOARD_FLAGS_TYPE') && BOARD_FLAGS_TYPE) ? BOARD_FLAGS_TYPE : BOARD_DIR; + $css .= ''; + } + + if( CODE_TAGS ) { + $css .= ''; + } + + // Various optional tags + if( USE_RSS == 1 ) { + $rss = ''; + } + + if( RTA == 1 ) { + $rta = ''; + } + + if( defined( 'FAVICON' ) ) { + $favicon = ''; + } + + $thread_unique_ips = 0; + $jsUniqueIps = ''; + + if (SHOW_THREAD_UNIQUES) { + if ($res) { + $thread_unique_ips = get_unique_ip_count($res); + } + + if ($thread_unique_ips) { + $jsUniqueIps = 'var unique_ips = ' . $thread_unique_ips . ';'; + } + } + + // js tags + $jsVersion = TEST_BOARD ? JS_VERSION_TEST : JS_VERSION; + $comLen = MAX_COM_CHARS; + $styleGroup = style_group(); + $maxFilesize = MAX_KB * 1024; + $maxLines = MAX_LINES; + $jsCooldowns = json_encode(array( + 'thread' => RENZOKU3, + 'reply' => RENZOKU, + 'image' => RENZOKU2 + )); + + $tailSizeJs = ''; + + if ($res) { + $tailSize = get_json_tail_size($res); + + if ($tailSize) { + $tailSizeJs = ",tailSize = $tailSize"; + } + } + + $title = TITLE; + + $scriptjs = << +var style_group = "$styleGroup", +cssVersion = $cssVersion, +jsVersion = $jsVersion, +comlen = $comLen, +maxFilesize = $maxFilesize, +maxLines = $maxLines, +clickable_ids = 1, +cooldowns = $jsCooldowns +$tailSizeJs; +$jsUniqueIps +JS; + + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) { + $scriptjs .= 'var css_event = "' . CSS_EVENT_NAME . '";'; + + if (defined('CSS_EVENT_VERSION')) { + $_css_event_version = (int)CSS_EVENT_VERSION; + } + else { + $_css_event_version = 1; + } + + $scriptjs .= 'var css_event_v = ' . $_css_event_version . ';'; + } + + if ((int)MAX_WEBM_FILESIZE < (int)MAX_KB) { + $scriptjs .= 'var maxWebmFilesize = ' . (MAX_WEBM_FILESIZE * 1024) . ';'; + } + + $_is_archived = false; + + if (ENABLE_ARCHIVE) { + $scriptjs .= 'var board_archived = true;'; + + if ($res && $log[$res]['archived']) { + $scriptjs .= 'var thread_archived = true;'; + $_is_archived = true; + } + } + + if (DISP_ID) { + $scriptjs .= 'var user_ids = true;'; + } + + if (JSMATH) { + $scriptjs .= 'var math_tags = true;'; + } + + if (SJIS_TAGS) { + $scriptjs .= 'var sjis_tags = true;'; + } + + if (SPOILERS) { + $scriptjs .= 'var spoilers = true;'; + } + + if (CAPTCHA_TWISTER) { + $scriptjs .= 'var t_captcha = true;'; + } + + if( $res && $log[$res]['bumplimit'] ) { + $scriptjs .= 'var bumplimit = 1;'; + } + + if( $res && $log[$res]['imagelimit'] ) { + $scriptjs .= 'var imagelimit = 1;'; + } + + if( AD_PLEA ) $scriptjs .= 'var check_for_block = ' . (int)AD_PLEA . ';'; + + if( $error ) $scriptjs .= 'is_error = "true";'; + + // Danbo ads + if (defined('ADS_DANBO') && ADS_DANBO) { + if (DEFAULT_BURICHAN) { + $scriptjs .= "var danbo_rating = '__SFW__';"; + } + else { + $scriptjs .= "var danbo_rating = '__NSFW__';"; + } + + // Set up fallbacks + $_danbo_fallbacks = []; + + if (defined('AD_BIDGEAR_TOP') && AD_BIDGEAR_TOP) { + $_danbo_fallbacks['t_bg'] = AD_BIDGEAR_TOP; + } + else { + if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + $_danbo_fallbacks['t_abc_d'] = AD_ABC_TOP_DESKTOP; + } + + if (defined('AD_ABC_TOP_MOBILE') && AD_ABC_TOP_MOBILE) { + $_danbo_fallbacks['t_abc_m'] = AD_ABC_TOP_MOBILE; + } + } + + if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM) { + $_danbo_fallbacks['b_bg'] = AD_BIDGEAR_BOTTOM; + } + else if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) { + $_danbo_fallbacks['b_abc_m'] = AD_ABC_BOTTOM_MOBILE; + } + + if (!$_danbo_fallbacks) { + $_danbo_fallbacks = 'null'; + } + else { + $_danbo_fallbacks = json_encode($_danbo_fallbacks); + } + + $scriptjs .= 'var danbo_fb = ' . $_danbo_fallbacks . ';'; + + // Tag closed further below + $scriptjs .= ''; + + // PubFuture + if (DEFAULT_BURICHAN) { + $scriptjs .= ''; + } + + $testjs = ( TEST_BOARD ) ? 'test/core-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'core.min.' . JS_VERSION_CORE . '.js'; + $testextra = ( TEST_BOARD ) ? 'test/extension-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'extension.min.' . JS_VERSION_EXT . '.js'; + + $scriptjs .= ''; + + if( !$error ) $scriptjs .= ''; + + // April 2022 + //$scriptjs .= ''; + + if (TEST_BOARD) { + $stylejs = ''; + } + else { + $stylejs = ''; + } + + if (ENABLE_PAINTERJS && $_GET['mode'] != 'oe_finish') { + if (TEST_BOARD) { + $scriptjs .= ''; + $css .= ''; + } + else { + $scriptjs .= ''; + $css .= ''; + } + } + /* + if (!$is_arclist && defined('CSS_MATERIAL') && CSS_MATERIAL) { + $css .= ''; + } + */ + if( !$res ) { + $prev = ( $page - DEF_PAGES ) / DEF_PAGES; + $next = ( $page + DEF_PAGES ) / DEF_PAGES; + + if( $prev == 0 ) { + $prev_link = SELF_PATH2; + } else if( $prev > 0 ) { + $prev_link = $prev . PHP_EXT2; + } + + // maybe >= ? + if( ( $npages - $page ) > DEF_PAGES ) { + $next_link = $next . PHP_EXT2; + } + } + + if ($is_arclist) { + $canonical = ''; + } + else if (!$res) { + if ($page > 0) { + $canonical = ''; + } + else { + $canonical = ''; + } + } + elseif ($res) { + $href_context = $log[$res]['semantic_url']; + + if ($href_context !== '') { + $href_context = "/$href_context"; + } + + $canonical = ''; + } + else { + $canonical = ''; + } + + $clean_title = strip_tags(TITLE); + + if ($res) { + $page_metatags = generate_page_metatags($log[$res]['sub'], $log[$res]['com']); + + if ($page_metatags) { + $page_description = $page_metatags[0] . ' - ' . META_DESCRIPTION; + $page_keywords = META_KEYWORDS . $page_metatags[1]; + } + else { + $page_description = META_DESCRIPTION; + $page_keywords = META_KEYWORDS; + } + + $page_title = generate_page_title($res, $log[$res]['sub'], $log[$res]['com']) + . ' - ' . preg_replace('/^[\[\/][a-z0-9]+[\]\/] - /i', '', $clean_title); + } + else { + $page_description = META_DESCRIPTION; + $page_keywords = META_KEYWORDS; + + $page_title = $clean_title; + + if ($is_arclist) { + $page_title .= ' - Archive'; + } + else if ($page > 0) { + $page_title .= ' - Page ' . (($page / DEF_PAGES) + 1); + } + } + + $page_title .= ' - 4chan'; + + if (!$_is_archived) { + $_delegate_ch = ''; + } + else { + $_delegate_ch = ''; + } + + $dat .= ' + + + + + + + +' . $rta . ' +' . $favicon . ' +' . $css . ' +' . $canonical . ' +' . $rss . $_delegate_ch . ' +' . $page_title . '' . $scriptjs . $extra; + + $embedearly = EMBEDEARLY; + + $adembedearly = AD_EMBEDEARLY; + + if (AD_ADBLOCK_TEXT && (DEFAULT_BURICHAN || BOARD_DIR === 'pol' || BOARD_DIR === 'bant')) { + $adembedearly .= file_get_contents_cached(AD_ADBLOCK_TEXT); + } + + $board_class = 'board_' . BOARD_DIR; + + if (!$res) { + if ($is_arclist) { + $board_class = 'is_arclist ' . $board_class; + } + else { + $board_class = 'is_index ' . $board_class; + } + } + else { + $board_class = 'is_thread ' . $board_class; + } + + if (TEXT_ONLY) { + $board_class = 'text_only ' . $board_class; + } + + if (!$is_arclist) { + $abovePostForm = '
    '; + } + else { + $abovePostForm = ''; + } + + $dat .= << + + + +$stylejs +$includenav + +
    + $titlepart +
    $title
    + $subtitle +
    +$abovePostForm +HTML; + + if (!$error && !$is_arclist) { + /* + if (defined('ADS_BIDGLASS_TOP_MOBILE') && ADS_BIDGLASS_TOP_MOBILE) { + $dat .= ''; + } + else if (defined('AD_ABC_TOP_MOBILE') && AD_ABC_TOP_MOBILE) { + $dat .= '

    '; + }*/ + } +} + +function delete_uploaded_files() +{ + global $upfile_name, $upfile, $dest, $pchfile; + if( $dest || $upfile ) { + @unlink( $dest ); + @unlink( $upfile ); + } +} + +/* Footer */ +function foot( &$dat, $error = false, $is_arclist = false ) +{ + global $update_avg_secs; + + $includenav = file_get_contents_cached(NAV2_TXT); + + $dat .= $includenav; + $dat .= rebuildd_stats(); + + if( CODE_TAGS ) { + $dat .= ''; + } + + $dat .= EMBEDLATE . ''; +} + +function error($mes, $unused = '') { + global $mode; + + if (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json') { + error_json($mes); + } + + if( $mode == "report" ) fancydie( $mes ); + + delete_uploaded_files(); + + head( $dat, 0, 1 ); + + $protocol = (stripos($_SERVER["HTTP_REFERER"], "https") === 0) ? 'https:' : 'http:'; + + $dat .= '
    ' . $mes . '

    [" . S_RELOAD . "]



    "; + + foot( $dat, true ); + + if (TEST_BOARD==1) { + internal_error_log("post error", $mes); + } + + die( $dat ); +} + +function error_json($msg) { + delete_uploaded_files(); + + header('Content-Type: application/json'); + + echo json_encode(['error' => $msg]); + + die(); +} + +function error_redirect($mes, $redirect, $timeout = 3000) { + delete_uploaded_files(); + head( $dat, 0, 1 ); + $dat .= << + setTimeout(function() { window.location = "$redirect"; }, $timeout); + +HTML; + $dat .= '
    ' . $mes . '

    [" + . S_RELOAD . "]



    "; + foot( $dat ); + + if (TEST_BOARD==1) { + internal_error_log("post error", $mes); + } + + die( $dat ); +} + +/* Auto Linker */ +function normalize_link_cb( $m ) +{ + + $subdomain = $m[1]; + $original = $m[0]; + $board = strtolower( $m[2] ); + $m[0] = $m[1] = $m[2] = ''; + + $count = count( $m ) - 1; + for( $i = $count; $i > 2; $i-- ) { + if( $m[$i] ) { + $no = $m[$i]; + break; + } + } + + if( $subdomain != 'boards') { + return $original; + } + + if( stripos( $no, 'catalog' ) === 0 ) { + + if( ( $pos = stripos( $no, '#s=' ) ) !== false ) { + $term = substr( $no, $pos + 3 ); + } else { + $term = 'catalog'; + } + + return ">>>/$board/$term"; + } + + if( $board == BOARD_DIR && $no && $no != 'catalog' ) { + return ">>$no"; + } else { + return ">>>/$board/$no"; + } +} + +function normalize_links( $proto ) +{ + // change http://xxx.4chan.org/board/res/no links into plaintext >># or >>>/board/# + $proto = preg_replace_callback( '@https?://([a-z]*)[.](?:4chan|4channel)[.]org/(\w+)/(?:(res|thread)/(\d+)(?:\/[-a-z0-9]+)?(?:#[qp]?(\d*))?|(catalog(?:#s=[a-z0-9+]+)?)|\w+.php[?]res=(\d+)(?:#[qp]?(\d*))?|)(?=[\s. false); + return false; + } + $r = (int)mysql_fetch_row($q)[0]; + $log[$no] = array('resto' => $r); + return $r; + + return false; +} + +// FIXME +// This might not be used anywhere anymore +function intraboard_link_cb( $m ) +{ + global $intraboard_cb_resno, $log; + $no = $m[1]; + $resno = $intraboard_cb_resno; + $resto = post_resto( $no ); // doesn't like assignment in condition + if( $resto !== false ) { + $resdir = ( $resno ? '' : RES_DIR2 ); + $ext = PHP_EXT2; + $id = NEW_HTML ? "p$no" : "$no"; + if( $resno && $resno == $resto ) // linking to a reply in the same thread + return ">>$no"; + elseif( $resto == 0 ) // linking to a thread + return ">>$no"; else // linking to a reply in another thread + return ">>$no"; + } + + return '' . $m[0] . ''; +} + +// FIXME +// This might not be used anywhere anymore, see parse_intraboard_link() +function intraboard_links( $proto, $resno ) +{ + global $intraboard_cb_resno; + + $intraboard_cb_resno = $resno; + + $proto = preg_replace_callback( '/>>([0-9]+)/', 'intraboard_link_cb', $proto ); + + return $proto; +} + +function other_board_resto( $board, $resno ) +{ + // this function is a little slow + // board requirements - either is the current board (for /test/) or is public (in boardlist) + // returns - + // FALSE if resno does not exist + // 0 if resno is a thread + // an id if resno is a reply to a thread + + static $boardlist = array(); + + if( !$boardlist ) + $boardlist = array_flip( mysql_column_array( mysql_global_call( "select sql_cache dir from boardlist" ) ) ); + + if( $board != BOARD_DIR && !isset( $boardlist[$board] ) ) + return false; + + $q = mysql_board_call( "select resto from `%s` where no=%d", $board, $resno ); + if( !mysql_num_rows( $q ) ) + return false; + $r = mysql_result( $q, 0 ); + + return $r; +} + +function interboard_link_cb( $m ) +{ + // on one hand, we can link to imgboard.php, using any old subdomain, + // and let apache & imgboard.php handle it when they click on the link + // on the other hand, we can use the database to fetch the proper subdomain + // and even the resto to construct a proper link to the html file (and whether it exists or not) + + // for now, we'll assume there's more interboard links posted than interboard links visited. + $board = '/'; + $otherboard = mb_strtolower( $m[1] ); + if( $m[2] ) { + $resto = other_board_resto( $otherboard, $m[2] ); + $id = "#p"; + + if( $resto === false ) + $url = ""; + else if( $resto ) + $url = $board . $otherboard . '/thread/' . $resto . $id . $m[2]; + else + $url = $board . $otherboard . '/thread/' . $m[2] . $id . $m[2]; + } else $url = $board . $otherboard . '/'; + + $b = $m[1]; + $mlp_hack = BOARD_DIR == 'mlp' && ( $b == 'b' || $b == 'co' ); + $original = mb_strtolower( $m[0] ); + if( !$url || $mlp_hack ) + return '' . $original . ''; + else + return "{$original}"; +} + +function interboard_catalog_link_cb( $m ) +{ + $board = $m[1]; + if( $board == 'f' ) return $m[0]; + + $lsearchquery = strtolower( urlencode( urldecode( $m[2] ) ) ); + $original = mb_strtolower( str_replace( ">", "> ", $m[0] ) ); + + if( $lsearchquery == "catalog" ) { + return "$original"; + } elseif( $lsearchquery == 'rules' ) { + return '' . $original . ''; + } else { + return "$original"; + } +} + +function boards_matching_arr() +{ + static $boards_matching_arr; + global $valid_boards; + + if( empty( $boards_matching_arr ) ) $boards_matching_arr = explode( '|', $valid_boards ); + + return $boards_matching_arr; +} + +// Normalize and linkify internal and non-quote links +// before inserting the post into the database. +function normalize_and_linkify($proto) { + + if (strpos($proto, "4chan") !== false || strpos($proto, "4cdn.org") !== false) { + // normalize long links + $proto = normalize_links($proto); + + // linkify other internal links + if ((strpos($proto, "4chan") !== false && strpos($proto, "/derefer") === false) || strpos($proto, "4cdn.org") !== false) { + $proto = preg_replace_callback( '/(https?:\/\/(?:[A-Za-z]*\.)?)(4chan|4channel|4cdn)(\.org)(\/[\w\-\.,@?^=%&;:\/~\+#\(\)]*[\w\-\@?^=%&;\/~\+#])?/i', 'clean_internal_link', $proto ); + $proto = preg_replace( '/([<][^>]*?)\\2<\/a>([^<]*?[>])/i', '\\1\\3\\4\\5\\6\\7\\8', $proto ); + } + } + + if (strpos($proto, '>>>') !== false) { + $proto = preg_replace_callback('#>>>/([a-z0-9]+)/([a-z0-9+/,l\-]*)#', 'auto_link_static_cb', $proto); + } + + return $proto; +} + +// Removes >> links from internal 4chan.org links +function clean_internal_link($matches) { + $link = preg_replace('/>>>|>>/', '', $matches[0]); + return "$link"; +} + +function auto_link_static_cb($matches) { + $post = $matches[0]; + $inter_board = $matches[1]; + $no = $matches[2]; + + $boards_matching_arr = boards_matching_arr(); + + $full_link = $post; + + $inter_board = strtolower( $inter_board ); + + $is_board_link = ($no == ''); + + $resno = $no; + + // Text boards + // Catalog, rules, and /rs/ links + if (!is_numeric( $resno ) || $is_board_link) { + $url = urlencode( urldecode( $resno ) ); + + $target = ''; + + if( strpos( $resno, 'rules' ) === 0 ) { + $ruleno = ''; + $ruleloc = strpos( $resno, '/' ); + + if( $ruleloc !== false ) { + $ruleno = substr( $resno, $ruleloc + 1 ); + } + + $parsed_link = '//www.' . L::d($inter_board) . "/rules#$inter_board$ruleno"; + $target = ' target="_blank"'; + } + else if (in_array($inter_board, $boards_matching_arr)) { + $parsed_link = '//boards.' . L::d($inter_board) . "/$inter_board/"; + + if( $inter_board == 'f' && $url == 'catalog' ) return $full_link; + + if( !$is_board_link ) { + $parsed_link .= ($url == 'catalog' ? 'catalog' : "catalog#s=$url"); + } + } + else { + return $full_link; + } + + return '' . $full_link . ''; + } + + return $full_link; +} + +function auto_link( $proto, $resno ) +{ + global $current_resno; + static $has_gen = 0; + + if( !$has_gen ) { + boards_matching_arr(); + $has_gen = 1; + } + + // The majority of posts don't contain links, so don't go there and waste time on preg junk + if( strpos( $proto, '>>' ) !== false ) { + $current_resno = $resno; + $proto = preg_replace_callback( '#(>>[0-9]+|>>>/[a-z0-9]+/[a-z0-9+/-]*)#', 'auto_link_cb', $proto ); + } + + return $proto; +} + +function auto_link_cb( $post ) +{ + global $current_resno; + + //var_dump($post); + $is_inter = ( strpos( $post[0], '>>>/' ) === 0 ); + $post = $post[0]; + + if( $is_inter ) { + preg_match( '#>>>/([a-z0-9]+)/(.*)#', $post, $match ); + + return parse_interboard_link( $match[0], $match[1], $match[2] ); + } else { + $no = explode( '>>', $post ); + + return parse_intraboard_link( $post, $no[1], $current_resno ); + } +} + +function parse_intraboard_link( $post, $no, $resno ) +{ + $full_link = $post; + //$no = substr( $post, $i - $in_link_char, $in_link_char ); + $resto = post_resto( $no ); + + if( $resto === false ) { + $parsed_link = '' . $full_link . ''; + + return $parsed_link; + } + + $ext = PHP_EXT2; + $id = NEW_HTML ? "p$no" : "$no"; + + // linking to a reply or the OP in the same thread + if ($resno && ($resno == $resto || $resno == $no)) { + $parsed_link = ">>$no"; + } + // linking to an OP in another thread or from indexes + elseif ($resto == 0) { + $parsed_link = ">>$no"; + } + // linking to a reply in another thread or from indexes + else { + $parsed_link = ">>$no"; + } + + return $parsed_link; +} + +function parse_interboard_link( $post, $inter_board, $no ) +{ + global $valid_boards; + + // make sure to account for > not > + $full_link = $post; + $inter_board = strtolower( $inter_board ); + + // Are we a board link? + $is_board_link = ( $no == '' ); + + // ... to get resno! + $resno = $no; + + $valid_rule = $inter_board == 'global' && strpos( $resno, 'rules' ) !== false; + + // Skip static links (boards, catalog) + if ($is_board_link + || $valid_rule + || !is_numeric($resno) + || !in_array($inter_board, boards_matching_arr()) + ) { + return $full_link; + } + + // Valid board, now check post number + $resto = other_board_resto( $inter_board, $resno ); + + if( $resto === false ) { // dead link + $url = ''; + } elseif( $resto ) { // different thread + $url = '//boards.' . L::d($inter_board) . "/{$inter_board}/thread/$resto#p$resno"; + } else { // same thread + $url = '//boards.' . L::d($inter_board) . "/{$inter_board}/thread/$resno#p$resno"; + } + + $disable = BOARD_DIR == 'mlp' && ( $inter_board == 'b' || $inter_board == 'co' ); + if( !$url || $disable ) { + $parsed_link = '' . $full_link . ''; + } else { + $parsed_link = "$full_link"; + } + + return $parsed_link; +} + +function trans_same_board_links( &$com ) +{ + $match = '>>>/' . BOARD_DIR . '/'; + $len = strlen( $match ); + $i = stripos( $com, $match ); + $boardlen = strlen( BOARD_DIR ) - 1; + $dir = BOARD_DIR; + $com .= '~'; + + while( isset( $com{$i} ) ) { + + if( is_numeric( $com{$i + $len} ) ) { + // Match, replace out + $com = substr_replace( $com, '>>', $i, $len ); + + } + + $i = stripos( $com, $match, $i + $len ); + if( $i === false ) { + $com = substr( $com, 0, strlen( $com ) - 1 ); + + return; + } + } +} + +function auto_link_parser( $post, $resno ) +{ + $post .= '<'; + + $i = 0; + $in_link = false; + $in_link_char = 0; + + $is_inter = false; + $inter_found_board = false; + $inter_board = ''; + $inter_is_catalog = false; + + $is_intra = false; + $gt_count = 0; + $mbcl = 10; // forgot about dis links :( + + $dbg = ""; + + while( isset( $post{$i} ) ) { + $seen_gt_this = false; + $c = $post{$i}; + + if( !$in_link ) { + // Not in a link, find > + + if( $c == '&' ) { + if( $post{$i + 1} == 'g' && $post{$i + 2} == 't' && $post{$i + 3} == ';' ) { + if( $gt_count < 3 ) $gt_count++; + + $i = $i + 4; + continue; + } + } + + if( ( $c == '/' && $gt_count == 3 ) || ( $gt_count > 1 && is_numeric( $c ) ) ) { + $in_link_char = 0; + $in_link = true; + $i--; // shift us back a char to get the right match... + } + + $gt_count = 0; + } else { + // We can be sure we have a valid character for our link + if( $in_link_char == 0 ) { + + if( $c == '/' ) { + $is_inter = true; + $is_intra = false; + } else { + $is_intra = true; + $is_inter = false; + } + } + + if( $is_inter ) { + + if( $in_link_char == 0 ) { + $in_link_char++; + $i++; + continue; + } + + if( $in_link_char > $mbcl && $c != '/' && !$inter_found_board ) { + // yup :( + + $in_link = false; + $is_inter = false; + + $i++; + continue; + } + + if( !$inter_found_board && $c == '/' ) { + $inter_board = substr( $post, $i - ( $in_link_char - 1 ), $in_link_char - 1 ); + + $inter_found_board = true; + $in_link_char++; + $i++; + continue; + } + + if( $inter_found_board ) { + $match = ( + ctype_alnum( $c ) || + $c == '+' || + $c == '/' || + $c == '-' + ); + + if( $match ) { + $in_link_char++; + $i++; + continue; + } + } + + if( !$inter_found_board ) { + $in_link_char++; + $i++; + continue; + } + + + parse_interboard_link( $post, $i, $in_link_char, $inter_board ); + + $is_inter = false; + $in_link = false; + $inter_found_board = false; + } + + if( $is_intra ) { + if( is_numeric( $c ) ) { + $in_link_char++; + } else { + // reached the end + parse_intraboard_link( $post, $i, $in_link_char, $resno ); + + $in_link = false; + $is_intra = false; + } + } + } + + $i++; + } + + return substr( $post, 0, strlen( $post ) - 1 ) . $dbg; +} + +/** + * New version of check_blacklist() + * $post must be an array with the following fields: + * resto, filename, name, tripcode, password, 4pass_id + */ +function check_md5_blacklist($md5, $original_md5, $post, $dest) { + if (!$md5) { + return false; + } + + $board = BOARD_DIR; + + if (DEFAULT_BURICHAN) { + $ws_clause = " OR boardrestrict = '_ws_'"; + } + else { + $ws_clause = ''; + } + + $sql =<< 0) { + $private_reason = "DMCA complaint from {$row['description']} (blacklist ID: {$row['id']})"; + auto_ban_poster($ban_name, 3, 1, $private_reason, S_DMCABANREASON, true, $pwd, $pass_id); + error_redirect(S_BANNED, 'https://www.' . L::d(BOARD_DIR) . '/banned'); + } + else { + $query = "INSERT INTO user_actions (board,postno,ip,time,uploaded,action) VALUES ('%s', %d, %d, NOW(), 0, 'fail_dmca')"; + mysql_global_call($query, $board, 0, $ip); + } + + error(S_DMCAFAIL, $dest); + } + + if ($row['quiet']) { + show_post_successful_fake($resto); + die(); + } + + error(S_FAILEDUPLOAD, $dest); +} + +function check_blacklist($post, $dest, $file_ext = '', $resto = 0, $pwd = null, $pass_id = null) { + //if( has_level() ) return; + + $board = BOARD_DIR; + + if (DEFAULT_BURICHAN) { + $ws_clause = " OR boardrestrict = '_ws_'"; + } + else { + $ws_clause = ''; + } + + $querystr = "SELECT SQL_NO_CACHE * FROM blacklist WHERE active=1 AND (boardrestrict='' or boardrestrict='$board'$ws_clause) AND (0 "; + foreach( $post as $field => $contents ) { + if( $contents ) { + $contents = mysql_real_escape_string( html_entity_decode( $contents ) ); + $querystr .= "OR (field='$field' AND contents='$contents') "; + } + } + $querystr .= ") LIMIT 1"; + + $query = mysql_global_call( $querystr ); + if( mysql_num_rows( $query ) == 0 ) return false; + + $row = mysql_fetch_assoc( $query ); + $prvreason = "Blacklisted ${row['field']} - " . htmlspecialchars( $row['contents'] ); + + if ($row['field'] == 'md5') { + $prvreason .= ' - Filename: ' . htmlspecialchars($post['filename']) . $file_ext; + } + + if (!$row['ban']) { + if (TEST_BOARD) { + error( "Blacklisted: " . $prvreason, $dest ); + } + } + // Auto-ban + else if ($row['ban'] == '1') { + auto_ban_poster($post['trip'] ? $post['nametrip'] : $post['name'], $row['banlength'], 1, $prvreason, $row['banreason'], false, $pwd, $pass_id); + } + // Show error (DMCA requests) + else if ($row['ban'] == '2') { + $ip = ip2long($_SERVER['REMOTE_ADDR']); + + $query = "SELECT ip FROM user_actions WHERE action = 'fail_dmca' AND ip = %d AND time >= DATE_SUB(NOW(), INTERVAL 1 DAY)"; + + $res = mysql_global_call($query, $ip); + + if ($res && mysql_num_rows($res) > 0) { + $prvreason = "DMCA complaint from {$row['description']} (blacklist ID: {$row['id']})"; + + auto_ban_poster($post['trip'] ? $post['nametrip'] : $post['name'], 3, 1, $prvreason, S_DMCABANREASON, true, $pwd, $pass_id); + + error_redirect(S_BANNED, 'https://www.' . L::d(BOARD_DIR) . '/banned'); + } + else { + $query = "INSERT INTO user_actions (board,postno,ip,time,uploaded,action) VALUES ('%s',%d,%d,NOW(),0,'fail_dmca')"; + mysql_global_call($query, $board, 0, $ip); + } + + error(S_DMCAFAIL, $dest); + } + /* + { + $ip = $_SERVER['REMOTE_ADDR']; + quick_log_to("/www/perhost/blacklist.log", "IP $ip board /$board/: $prvreason"); + } + */ + + if ($row['quiet']) { + show_post_successful_fake($resto); + die(); + } + + error(S_FAILEDUPLOAD, $dest); +} + +// we've already failed the floodcheck, check if they're a repeat offender and ban them +function check_fail_floodcheck($info) +{ + $ip = ip2long($_SERVER['REMOTE_ADDR']); + mysql_global_call("insert into user_actions (ip,board,action,time) values (%d,'%s','fail_floodcheck',now())", $ip, ''); + $query = mysql_global_call("select count(*)>%d from user_actions where ip=%d and action='fail_floodcheck' and time >= subdate(now(), interval 1 hour)", LOGIN_FAIL_HOURLY, $ip); + quick_log_to("/www/perhost/floodchecks.log", $info); + if(mysql_result($query,0,0)) { + auto_ban_poster("Anonymous", 1, 1, "got a flood check warning 5 times in an hour", "Sending an excessive number of server requests"); + } +} + +// word-wrap without touching things inside of tags +function wordwrap2( $str, $cols, $cut ) +{ + // if there's no runs of $cols non-space characters, wordwrap is a no-op + if( mb_strlen( $str ) < $cols || !preg_match( '/[^ <>]{' . $cols . '}/', $str ) ) { + return $str; + } + $sections = preg_split( '/[<>]/', $str ); + $str = ''; + for( $i = 0; $i < count( $sections ); $i++ ) { + if( $i % 2 ) { // inside a tag + $str .= '<' . $sections[$i] . '>'; + } else { // outside a tag + $words = explode( ' ', $sections[$i] );/* + $exclude = array( + 'http://', + 'https://', + 'www.' + ); +*/ + foreach( $words as &$word ) {/* + foreach( $exclude as $match ) { + if( stripos( $word, $match ) === 0 && stripos( $word, '4chan.org' ) !== false ) continue 2; + }*/ + + $word = htmlspecialchars_decode( $word, ENT_QUOTES ); + $word = utf8_wordwrap( $word, $cols, $cut, true ); + $word = htmlspecialchars( $word, ENT_QUOTES ); + + } + + $str .= implode( ' ', $words ); + } + } + + return $str; +} + +function logtime( $desc ) +{ + static $run = -1; + if( !PROFILING ) return; + if( $run == -1 ) { + $run = getmypid_cached(); + } + $board = BOARD_DIR; + $time = microtime( true ); + mysql_global_call( "INSERT INTO profiling_times VALUES ('$board',$run,$time,'$desc')" ); +} + +function time_log($r) { + if (TEST_BOARD && $_SERVER['HTTP_ACCEPT'] !== 'application/json') { + echo "\n"; + } +} + +function is_bad_xff( $xff ) +{ + if ($xff === '8.8.8.8' || $xff === '62.210.138.29' || $xff === '212.129.0.228') { + return true; + } + + list( $xffs ) = post_filter_get( "xffwhitelist" ); + $ipnum = ip2long( $xff ); + + if( !$ipnum ) return true; // text in xff field + + return find_ipxff_in( 0, $ipnum, $xffs ); +} + +function has_doubles( $id ) +{ + if( $id % 1000 == 0 ) return false; + + $ones = $id % 10; + $tens = ( $id / 10 ) % 10; + + return $ones == $tens; +} + +function generate_uid($resto, $time, $ip = false) { + if (DISP_ID_RANDOM) { + $str = mt_rand(); + } + else { + $str = !$ip ? $_SERVER["REMOTE_ADDR"] : $ip; + + if (DISP_ID_PER_THREAD) { + $str .= $resto ? $resto : date( 'Ymd', $time ); + } else { + $str .= 'hats'; // we will put a hat on it to confuse people :) + } + } + + $salt = file_get_contents_cached( SALTFILE ); + $hash = base64_encode( pack( "H*", sha1( $str . $salt ) ) ); + + return substr( $hash, 0, 8 ); +} + +function parse_vip_capcode($capcode) { + // Flood check + $longip = ip2long($_SERVER['REMOTE_ADDR']); + + $query = <<= SUBDATE(NOW(), INTERVAL 1 HOUR) +SQL; + + $res = mysql_global_call($query, $longip); + + if (!$res) { + return false; + } + + $count = mysql_fetch_row($res)[0]; + + if ($count >= 3) { + return false; + } + + // Now check the capcode + list($_, $user_id, $user_key) = explode('!', $capcode, 3); + + if (!$user_id || !$user_key) { + return false; + } + + $query = "SELECT name, user_key FROM vip_capcodes WHERE active = 1 AND user_id = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $user_id); + + if (!$res) { + return false; + } + + $user = mysql_fetch_assoc($res); + + if ($user && password_verify($user_key, $user['user_key'])) { + $query = "UPDATE vip_capcodes SET last_used = %d, last_ip = '%s' WHERE user_id = '%s' LIMIT 1"; + mysql_global_call($query, $_SERVER['REQUEST_TIME'], $_SERVER['REMOTE_ADDR'], $user_id); + return $user['name']; + } + + // Log the failure + $query = <<getPwd(); + + if (!$pass) { + error(S_GENERICERROR, $dest); + } + + $resto = (int)$resto; + + // time + $time = $_SERVER['REQUEST_TIME']; + $tim = generate_tim(); + + $captcha_bypass_allow_credits = false; + + $memcached = null; + + if (isset($_POST['recaptcha_challenge_field'])) { + error('Legacy captcha is no longer supported.'); + } + else if (isset($_POST['g-recaptcha-response'])) { + if (CAPTCHA_TWISTER) { + error('reCAPTCHA v2 is no longer supported.'); + } + + // Recaptcha v2 + start_auth_captcha(); + + if (!$captcha_bypass) { + $_c_ret = end_recaptcha_verify(); + } + } + else { + if (valid_captcha_bypass() !== true) { + $memcached = create_memcached_instance(); + + $_unsolved_count = 0; + + // Captcha bypass credits + if ($resto && isset($_POST['t-challenge']) && $_POST['t-challenge'] === 'noop') { + if (use_twister_captcha_credit($memcached, $host, $userpwd) === false) { + error(S_CAPTCHATIMEOUT); + } + } + // Captcha verification failed + else if (is_twister_captcha_valid($memcached, $host, $userpwd, BOARD_DIR, $resto, $_unsolved_count) === false) { + // Silent captcha failure for new suspicious users + //$_bad_actor = spam_filter_is_bad_actor(); + //$_threat_score = spam_filter_get_threat_score($_SERVER['HTTP_X_GEO_COUNTRY'], !$resto, true); + if (isset($_SERVER['HTTP_X_BOT_SCORE'])) { + $_bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + } + else { + $_bot_score = 100; + } + + if (!$userpwd || $userpwd->isUserKnownOrVerified(1440, 1) === false) { + if ($_bot_score > 1 && $_bot_score < 80) { + $_meta = spam_filter_format_http_headers(htmlspecialchars($com), $_SERVER['HTTP_X_GEO_COUNTRY'], $upfile_name, $_threat_score); + if (isset($_POST['t-challenge']) && $_POST['t-response'] && isset($_COOKIE['_tcs'])) { + log_failed_captcha($host, $userpwd, BOARD_DIR, $resto, true, $_meta); + } + show_post_successful_fake($resto, false); + die(); + } + } + error(S_BADCAPTCHA); + } + // Captcha verification succeeded + else if ($_unsolved_count < 2 && CAPTCHA_ALLOW_BYPASS && spam_filter_is_bad_actor() === false) { + $captcha_bypass_allow_credits = true; + } + } + } + /* + if (spam_filter_is_bad_actor()) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('log_bad_actor', BOARD_DIR, $resto, $host, 1, $_bot_headers); + } + */ + if (PASS_POST_ONLY && !$captcha_bypass) { + error(S_PASS_POST_ONLY); + } + + // Validate 2FA if posting html + if ((isset($_POST['html']) && $_POST['html']) && (has_level('manager') || has_flag('html') || has_flag('developer'))) { + validate_otp(); + $log_html_post = $log_mod_action = true; + } + + $locked_time = $time; + // check closed + if( $resto ) { + if( !$cchk = mysql_board_call( "select closed,sticky,undead,archived,sub,com from `" . SQLLOG . "` where no=" . $resto ) ) { + echo S_SQLFAIL; + } + list( $closed, $sticky, $undead, $is_archived, $_thread_sub, $_thread_com ) = mysql_fetch_row( $cchk ); + if ($is_archived) { + error(S_MAYNOTREPLY, $upfile); + } + $is_undead_sticky = $sticky == 1 && $undead == 1; + if( $closed == 1 && !has_level() ) error( S_MAYNOTREPLY, $upfile ); + mysql_free_result( $cchk ); + + $sub = ''; + } + else { + $is_undead_sticky = false; + } + + $has_image = $upfile && file_exists( $upfile ); + + $md5 = null; + $original_md5 = null; // MD5 before exif and other metadata stripping + + if( $has_image ) { + if (UPLOAD_BOARD) { + if( file_exists( IMG_DIR . $upfile_name ) ) error( "Filename already exists.", $upfile ); + + $dest = $upfile; + + $upfile_name = sanitize_text( $upfile_name ); + if( !is_file( $dest ) ) error( S_FAILEDUPLOAD, $dest ); + if ($upfile_name[0] === '.') { + error('Error: Invalid filename (first character cannot be a period).', $dest); + } + $size = getimagesize( $dest ); + if( !is_array( $size ) ) error( S_NOREC, $dest ); + + $W = $size[0]; + $H = $size[1]; + $fsize = filesize( $dest ); + if( $fsize > MAX_KB * 1024 ) error( S_TOOLARGE, $dest ); + if( $size[2] == 6 || $size[2] == 5 ) { + error( S_FAILEDUPLOAD, $dest ); + } + switch( $size[2] ) { + case 4 : + case 13 : + $ext = ".swf"; + break; + default : + $ext = ".xxx"; + error( S_FAILEDUPLOAD, $dest ); + break; + } + + time_log( "sfpi" ); + rpc_task(); + + $len = strlen( $ext ); + } + else { + // NOT upload board + + // check image limit + if( $resto && !$sticky && !$undead && !has_level() ) { + if( !$result = mysql_board_call( "SELECT COUNT(*) FROM `" . SQLLOG . "` WHERE archived = 0 AND resto=$resto AND fsize!=0 AND filedeleted=0" ) ) { + echo S_SQLFAIL; + } + $countimgres = mysql_result( $result, 0, 0 ); + if( $countimgres >= MAX_IMGRES && !has_level() ) error(S_MAXIMAGESREACHED, $upfile ); + mysql_free_result( $result ); + } + + //upload processing + $dest = $upfile; + + // TODO: what does that preg_replace do? those are probably utf8 codes + $upfile_name = sanitize_text( preg_replace('/\xe2\x80(\xae|\xad|\x8f|\x8e)/', '', $upfile_name) ); + + if (!is_file($dest)) { + error(S_FAILEDUPLOAD, $dest); + } + + // Use filesize() later as it's possible to trick jpegtrans to generate a much bigger file + $fsize = $_FILES['upfile']['size']; + + if (!$fsize || $fsize > MAX_KB * 1024) { + error( S_TOOLARGE, $dest ); + } + + $webm_sar = null; + + // PDF processing + if( ENABLE_PDF == 1 && strcasecmp( '.pdf', substr( $upfile_name, -4 ) ) == 0 ) { + $ext = '.pdf'; + $W = $H = 1; + // run through ghostscript to check for validity + if( pclose( popen( "/usr/local/bin/gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=nullpage $dest", 'w' ) ) ) { + error( S_FAILEDUPLOAD, $dest ); + } + } + // Webm / MP4 + else if (ENABLE_WEBM && preg_match('/\.(webm|mp4)$/i', $upfile_name)) { + if ($fsize > MAX_WEBM_FILESIZE * 1024) { + error(S_TOOLARGE, $dest); + } + + $original_md5 = md5_file($dest); + + $ext = '.' . strtolower(pathinfo($upfile_name, PATHINFO_EXTENSION)); + + //if ($ext == '.mp4' && BOARD_DIR != 'test') { + // error(S_NOREC, $dest); + //} + + $size = validate_webm($dest, $ext); + $W = $size[0]; + $H = $size[1]; + $webm_sar = $size[2]; + } + // PNG, GIF, JPEG + else { + $size = getimagesize( $dest ); + if( !is_array( $size ) ) { + quick_log_to( "/www/perhost/bad-upload.log", "unrecognized file $upfile_name"); + + error( S_NOREC, $dest ); + } + + $W = $size[0]; + $H = $size[1]; + switch( $size[2] ) { + case 1 : + $ext = ".gif"; + break; + case 2 : + $ext = ".jpg"; + break; + case 3 : + $ext = ".png"; + break; + case 4 : + $ext = ".swf"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 5 : + $ext = ".psd"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 6 : + $ext = ".bmp"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 7 : + $ext = ".tiff"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 8 : + $ext = ".tiff"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 9 : + $ext = ".jpc"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 10 : + $ext = ".jp2"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 11 : + $ext = ".jpx"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 13 : + $ext = ".swf"; + error( S_FAILEDUPLOAD, $dest ); + break; + default : + $ext = ".xxx"; + error( S_FAILEDUPLOAD, $dest ); + break; + } + if (GIF_ONLY == 1 && $size[2] != 1 && $ext != '.webm') error(S_FAILEDUPLOAD, $dest); + } // end PDF processing -else + + // This doesn't seem to use the $md5 arg + if ($upfile_name === '') { + error('Blank file names are not supported.', $dest); + } + + time_log( "sfpi" ); + rpc_task(); + + // Picture reduction + if( !$resto ) { + $maxw = MAX_W; + $maxh = MAX_H; + } else { + $maxw = MAXR_W; + $maxh = MAXR_H; + } + if( defined( 'MIN_W' ) && MIN_W > $W ) error( S_TOOSMALL, $dest ); + if( defined( 'MIN_H' ) && MIN_H > $H ) error( S_TOOSMALL, $dest ); + if( defined( 'MAX_DIMENSION' ) ) + $maxdimension = MAX_DIMENSION; + else + $maxdimension = 5000; + if( $W > $maxdimension || $H > $maxdimension ) { + error( S_TOOLARGERES, $dest ); + } elseif( $W > $maxw || $H > $maxh ) { + $W2 = $maxw / $W; + $H2 = $maxh / $H; + ( $W2 < $H2 ) ? $key = $W2 : $key = $H2; + $TN_W = ceil( $W * $key ) + 1; + $TN_H = ceil( $H * $key ) + 1; + } + + // Strip metadata, exif, comments and other embeddded extra data + if ($ext === '.jpg') { + // Embed detection is done later below if STRIP_EXIF is disabled + if (STRIP_EXIF) { + $original_md5 = md5_file($dest); + + if (strip_jpeg_exif($dest) === false) { + error(S_FAILEDUPLOAD, $dest); + } + + clearstatcache(true, $dest); + } + } + else if ($ext === '.png') { + $original_md5 = md5_file($dest); + $_ret = strip_png_chunks($dest, MAX_KB * 1024); + if ($_ret < 0) { + if ($_ret === -2) { + error('APNG format not supported.', $dest); + } + else { + error(S_FAILEDUPLOAD, $dest); + } + } + else if ($_ret > 0) { + clearstatcache(true, $dest); + } + } + else if ($ext === '.gif') { + $original_md5 = md5_file($dest); + $_ret = strip_gif_extra_data($dest, $fsize); + if ($_ret < 0) { + error(S_FAILEDUPLOAD, $dest); + } + clearstatcache(true, $dest); + } + + // It should be safe to check the filesize now + // clearstatcache() must be called if the file was modified + $fsize = filesize($dest); + + if (!$fsize) { + error(S_TOOLARGE, $dest); + } + else if ($ext === '.webm' && $fsize > MAX_WEBM_FILESIZE * 1024) { + error(S_TOOLARGE, $dest); + } + else if ($fsize > MAX_KB * 1024) { + error(S_TOOLARGE, $dest); + } + + // Check for JPEG embedded data. jpegtran seems to remove unknown data + // so this only needs to be done if STRIP_EXIF is disabled + if (!STRIP_EXIF && $ext === '.jpg' && $fsize > 204800) { + validate_jpeg_size($dest, $fsize); + } + } + + $insfile = preg_replace('/\.[a-z0-9]+$/i', '', $upfile_name); + + $md5 = md5_file( $dest ); + $mes = $upfile_name . ' ' . S_UPGOOD; + } + + if( $_FILES["upfile"]["error"] > 0 ) { + if( $_FILES["upfile"]["error"] == UPLOAD_ERR_INI_SIZE ) + error( S_TOOLARGE, $dest ); + if( $_FILES["upfile"]["error"] == UPLOAD_ERR_FORM_SIZE ) + error( S_TOOLARGE, $dest ); + if( $_FILES["upfile"]["error"] == UPLOAD_ERR_PARTIAL ) + error( S_FAILEDUPLOAD, $dest ); + if( $_FILES["upfile"]["error"] == UPLOAD_ERR_CANT_WRITE ) + error( S_FAILEDUPLOAD, $dest ); + } + + if( $upfile_name && $_FILES["upfile"]["size"] == 0 ) { + error( S_TOOLARGEORNONE, $dest ); + } + + if( ENABLE_EXIF == 1 ) { + $exif = htmlspecialchars( shell_exec( "/usr/local/bin/exiftags $dest" ) ); + } + + $resto = (int)$resto; + if( $resto ) { + if( !mysql_result( mysql_board_call( "select count(no) from `" . SQLLOG . "` where root>0 and no=$resto" ), 0, 0 ) ) + error( S_NOTHREADERR, $dest ); + } + + $pass_is_bannable = !$userpwd->isNew(); + + // Most common errors checked, now check for post-block from ban requests + if (BLOCK_ON_BR && !has_level()) { + check_for_ban_request($host, $pass_is_bannable ? $pass : null); + } + + // Standardize new character lines + $com = str_replace( "\r\n", "\n", $com ); + $com = str_replace( "\r", "\n", $com ); + + $comlim = has_level() ? MAX_COM_CHARS_AUTHED : MAX_COM_CHARS; + $longlim = has_level() ? 255 : 100; + + if( mb_strlen( $com ) > $comlim ) error( S_TOOLONG, $dest ); + if( strlen( $name ) > $longlim ) error( S_TOOLONG, $dest ); + if( strlen( $email ) > $longlim ) error( S_TOOLONG, $dest ); + if( strlen( $sub ) > $longlim ) error( S_TOOLONG, $dest ); + if( strlen( $resto ) > 10 ) error( S_GENERICERROR, $dest ); + if( strlen( $url ) > 10 ) error( S_GENERICERROR, $dest ); + + $sub = normalize_content( $sub ); + + // start of some attempt to get rid of *all* zero width bollocks + if( BOARD_DIR != 'jp' && BOARD_DIR != 'a' && !SJIS_TAGS) { + $com = normalize_content( $com ); + $com = strip_zerowidth( $com ); + } + + // strip no break spaces and soft hyphens + $com = str_replace(array("\xC2\xAD", "\xC2\xA0"), '', $com); + + // name/subject too! + $sub = strip_zerowidth( $sub ); + $name = strip_zerowidth( $name ); + + // Strip unicode emoticons + $name = strip_emoticons($name, SJIS_TAGS); + + if ($sub !== '') { + $sub = strip_emoticons($sub, SJIS_TAGS); + } + + if ($com !== '') { + $com = strip_emoticons($com, SJIS_TAGS); + } + + // strip out ltr junk from name + $name = preg_replace( '#([\x{2000}-\x{200F}]|[\x{2028}-\x{202F}])#u', '', $name ); + + if ($sub !== '') { + $sub = strip_fake_capcodes($sub); + } + + if( !strlen( $name ) || preg_match( "/^[ | |]*$/", $name ) ) $name = ""; + if( !strlen( $com ) || preg_match( "/^[ | |\t]*$/", $com ) ) $com = ""; + if( !strlen( $sub ) || preg_match( "/^[ | |]*$/", $sub ) ) $sub = ""; + + //$name = str_replace( S_MANAGEMENT, "\"" . S_MANAGEMENT . "\"", $name ); + //$name = str_replace( S_DELETION, "\"" . S_DELETION . "\"", $name ); + + // Remove intra spoilers + if (SPOILERS && stripos($com, '[spoiler]') !== false) { + //$com = preg_replace( '/\[spoiler\]\s+\[\/spoiler\]/', '', $com ); + $com = preg_replace('/(\S)\[spoiler\](.*?)\[\/spoiler\](\S)/', '\\1\\2\\3', $com); + } + + //lol /b/ + $xff = get_request_xff(); + //if( is_bad_xff( $xff ) ) $xff = ""; + + $youbi = array(S_SUN, S_MON, S_TUE, S_WED, S_THU, S_FRI, S_SAT); + $yd = $youbi[date( "w", $time )]; + if( SHOW_SECONDS == 1 ) { + $now = date( "m/d/y", $time ) . "(" . (string)$yd . ")" . date( "H:i:s", $time ); + } else { + $now = date( "m/d/y", $time ) . "(" . (string)$yd . ")" . date( "H:i", $time ); + } + + $c_name = $name; + $c_email = $email; + + if (JANITOR_BOARD == 1) { + $name = get_hashed_mod_name($_COOKIE['4chan_auser']); + $email = ''; + } + + // April 2023 + //$_has_xa23_content = $com && preg_match('/^[^>]{8,}/m', $com) > 0; + + $com = preg_replace( '#>>>/' . BOARD_DIR . '/([0-9]+)#', '>>$1', $com ); + + $sub = sanitize_text( $sub ); + $sub = preg_replace( "/[\r\n]/", "", $sub ); + $sub = strip_private_unicode($sub); + $url = sanitize_text( $url ); + $url = preg_replace( "/[\r\n]/", "", $url ); + $resto = sanitize_text( $resto ); + $resto = preg_replace( "/[\r\n]/", "", $resto ); + $com = sanitize_text( $com, 1, true ); + $com = strip_private_unicode($com); + + if( FORCED_ANON == 1 ) { + if( !has_level('admin') ) $name = S_ANONAME; + $sub = ''; + } + + if (UPLOAD_BOARD) { + if( NO_TEXTONLY == 1 ) { + if( !$resto && !$has_image ) error( S_NOPIC, $dest ); + } else { + if( !$resto && !$textonly && !$has_image ) error( S_NOPIC, $dest ); + } + } else { + if (!TEXT_ONLY) { + if( NO_TEXTONLY == 1 && (!has_level() || $email === '') ) { + if( !$resto && !$has_image ) error( S_NOPIC, $dest ); + } else { + if( !$resto && !$textonly && !$has_image ) error( S_NOPIC, $dest ); + } + } + + if( REQUIRE_SUBJECT && !$resto && !strlen( $sub ) ) error( S_NOSUB, $dest ); + } + // Check for sage, nonoko and nonokosage + $is_sage = false; + + if (stripos($email, 'sage') !== false) { + $is_sage = true; + $email = str_ireplace('sage', '', $email); + } + + $email_lower = strtolower($email); + + if ($email_lower === 'nonoko') { + $is_nonoko = true; + } + + if( SPOILERS == 1 && $spoiler ) { + $sub = "SPOILER<>$sub"; + } + + if( !has_level() ) { + $match = array(); + if( substr_count( $com, "\n" ) > 6 ) preg_match_all( '#([^\n]+\n+)\1{5,}#', $com, $match ); + if( !empty( $match[0] ) ) { + foreach( $match[1] as $key => $var ) { + //auto_ban_poster( $name, 0, 1, 'Matched the same string 5 times or more in 1 post seperated by newlines (Matched: ' . $var . ')', 'Please do not spam.' ); + error( S_REJECTTEXTBAN ); + } + } + } + + // FIXME sanitize_text() replaces repeated \n too, is this duplicate? + // disable on code tag boards (we replace multiple brs instead) + if (!CODE_TAGS && !SJIS_TAGS) { + $com = preg_replace( "/\n(( | )*\n){3,}/", "\n", $com ); + } + + if( !has_level() && substr_count( $com, "\n" ) > MAX_LINES ) error(S_TOOMANYLINES, $dest ); + + if( ENABLE_EXIF == 1 && $exif ) { + //turn exif into a table + $exiflines = explode( "\n", $exif ); + $exif = ""; + foreach( $exiflines as $exifline ) { + list( $exiftag, $exifvalue ) = explode( ': ', $exifline ); + if( $exifvalue != '' ) + $exif .= ""; + else + $exif .= ""; + } + $exif .= '
    $exiftag$exifvalue
    $exiftag
    '; + $exiftext .= '

    ' . sprintf(S_EXIF_TOGGLE, $tim) . '
    '; + $exiftext .= "$exif"; + } + + $name = preg_replace( "/[\r\n]/", "", $name ); + + $names = mb_convert_encoding($name, 'CP932', 'UTF-8'); // convert to Windows Japanese #kami + + //start new tripcode crap + list ( $name ) = explode( "#", $name ); + + // Strip unicode point-of-interest and # characters + if (preg_match('/[\x{2318}\x{ff03}\x{FE5F}]/u', $name)) { + $name = preg_replace('/[\x{2318}\x{ff03}\x{FE5F}]/u', '', $name); + } + + $name = normalize_content( $name ); + $name = sanitize_text( $name ); + $name = strip_private_unicode($name); + + if (preg_match('/^\s+$/', $name)) { + $name = ''; + } + + $name = str_replace('!', '', $name); + + if( preg_match( "/\#+$/", $names ) ) { + $names = preg_replace( "/\#+$/", "", $names ); + } + if( preg_match( "/\#/", $names ) ) { + + $names = str_replace( "&#", "&&", htmlspecialchars( $names, ENT_COMPAT | ENT_HTML401, 'Shift_JIS' ) ); # otherwise HTML numeric entities screw up explode()! + + list ( $nametemp, $trip, $sectrip ) = str_replace( "&&", "&#", explode( "#", $names, 3 ) ); + + if ($sectrip != '') { + $trip = ''; + } + + $names = $nametemp; + if( STRIP_TRIPCODE == 0 ) $name .= "
    "; + + if ($trip != "" && STRIP_TRIPCODE == 0) { + $salt = strtr( preg_replace( "/[^\.-z]/", ".", substr( $trip . "H.", 1, 2 ) ), ":;<=>?@[\\]^_`", "ABCDEFGabcdef" ); + $trip = substr( crypt( $trip, $salt ), -10 ); + $name .= " !" . $trip; + } + + if( $sectrip != "" && STRIP_TRIPCODE == 0 ) { + $salt = file_get_contents_cached( SALTFILE ); + $sha = base64_encode( pack( "H*", sha1( $sectrip . $salt ) ) ); + $sha = substr( $sha, 0, 11 ); + $name .= " !!" . $sha; + } + } //end new tripcode crap + + // Check the length of the name field again to prevent truncation in the middle of tripcode HTML + if (strlen($name) > 255) { + error(S_TOOLONG, $dest); + } + + //Cookies + $cookie_domain = '.' . L::d(BOARD_DIR); + setrawcookie( "4chan_name", rawurlencode( $c_name ), $time + ( $c_name ? ( 7 * 24 * 3600 ) : -3600 ), '/', $cookie_domain ); + + if( !strlen( $name ) ) $name = S_ANONAME; + if( !strlen( $com ) ) $com = S_ANOTEXT; + if( !strlen( $sub ) ) $sub = S_ANOTITLE; + + /* since4pass */ + if ($captcha_bypass && $passid && $email && strpos(" $email ", ' since4pass ') !== false) { + $since4pass = get_since_4chan($passid); + } + else { + $since4pass = 0; + } + + // April 2024 + /* + if ($email) { + $_xa24_since4pass = april_2024_parse_email($email); + $since4pass = $_xa24_since4pass; + } + else { + $_xa24_since4pass = false; + } + */ + if (FORTUNE_TRIP == 1 && $email == 'fortune') { + $fortunes = array("Bad Luck", "Average Luck", "Good Luck", "Excellent Luck", "Reply hazy, try again", "Godly Luck", "Very Bad Luck", "Outlook good", "Better not tell you now", "You will meet a dark handsome stranger", "キタ━━━━━━(゚∀゚)━━━━━━ !!!!", "( ´_ゝ`)フーン ", "Good news will come to you by mail"); + // Christmas 2021 + /* + $fortunes = array("You're on the nice list!", "You're on the naughty list!", "Krampus is coming to your house!", "It's going to be a white Christmas!", "Merry Christmas!", "Happy Hanukkah!", "Happy Kwanzaa!", "Feliz Navidad!", "Happy Festivus!", "You're getting a lump of coal in your stocking!", "Blessed Yule!", "You're standing under the mistletoe!", "The poster above you has been very very naughty!", "You got an extra thick slice of fruitcake.", "You're on the Elf Watchlist.", "Your heart is two sizes too small.", "Your heart grew three sizes!", "Bah! Humbug."); + */ + $fortunenum = rand( 0, sizeof( $fortunes ) - 1 ); + $fortcol = "#" . sprintf( "%02x%02x%02x", + 127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) ), + 127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) + 2 / 3 * M_PI ), + 127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) + 4 / 3 * M_PI ) ); + $com .= "

    Your fortune: " . $fortunes[$fortunenum] . "
    "; + } + + if (DICE_ROLL == 1) { + if( $email ) { + if( preg_match( "/dice[ +](\\d+)[ d+](\\d+)(([ +-]+?)(-?\\d+))?/", $email, $match ) ) { + $dicetxt = S_DICE_PFX . ' '; + $dicenum = min( 25, $match[1] ); + $diceside = $match[2]; + $diceaddexpr = $match[3]; + $dicesign = $match[4]; + $diceadd = intval( $match[5] ); + + for( $i = 0; $i < $dicenum; $i++ ) { + $dicerand = mt_rand( 1, $diceside ); + if( $i ) $dicetxt .= ", "; + $dicetxt .= $dicerand; + $dicesum += $dicerand; + } + + if( $diceaddexpr ) { + if( strpos( $dicesign, "-" ) > 0 ) $diceadd *= -1; + $diceadd_formatted = ( $diceadd >= 0 ? " + " : " - " ) . abs( $diceadd ); + $dicetxt .= $diceadd_formatted; + $dicesum += $diceadd; + } + + if ($dicenum > 1) { + $dicetxt .= " = $dicesum"; + } + + $dicetxt .= " ({$dicenum}d{$diceside}" . ($diceaddexpr ? $diceadd_formatted : "") . ")

    "; + + $com = "$dicetxt" . $com; + } + } + } + + // fixme: this is needed for bypassing the r9k filter. $email gets reset below. + $options_field = $email; + + $emails = $email; + $admin_highlight = false; + $uid = null; + $capcode = 'none'; + $delay_refresh = false; + if (strpos($email, 'capcode_') === 0) { + // are we trying to capcode? + if (!has_level('admin') && !has_flag('capcodename')) { + $name = S_ANONAME; + } + // Only pass and authed users can use VIP capcodes + if ($captcha_bypass) { + $capcode = parse_capcode($email, $name); + } + else { + $capcode = parse_capcode($email); + } + + $ma = ( $capcode == 'admin_highlight') ? 'admin' : $capcode; + $ma = ucfirst( $ma ); + + if( DISP_ID == 1 && $ma != 'None' ) { + $uid = $ma; + } + + if( $ma != 'None' && STRIP_EXIF_ON_CAPCODE && $ext == ".jpg" ) { + system( "/usr/local/bin/jpegtran -copy none -outfile '$dest' '$dest'" ); + $md5 = md5_file( $dest ); + } + + if ($capcode !== 'none') { + $log_mod_action = $log_capcode_post = true; + } + + setcookie('options', $email, $time + (7 * 24 * 3600), '/', $cookie_domain); + } + else if (isset($_COOKIE['options'])) { + setcookie('options', null, $time -3600, '/', $cookie_domain); + } + + $email = ''; + + $nameparts = explode( '
    !', $name ); + + time_log( "trip" ); + + //logtime( "starting autoban checks" ); + + /** + * Ban check step + */ + $ban_fields = array(); + + if ($pass_is_bannable) { + $ban_fields['password'] = $pass; + } + /* + if ($nameparts[1]) { + $ban_fields['tripcode'] = $nameparts[1]; + } + */ + if ($passid) { + $ban_fields['4pass_id'] = $passid; + } + + $user_is_banned = check_for_ban($host, $ban_fields, $resto, $userpwd && $userpwd->verifiedLevel()); + + if ($user_is_banned) { + if (!$captcha_bypass) { + mysql_global_call("INSERT INTO user_actions (board,ip,time,action) VALUES ('%s',%d,from_unixtime(%d),'%s')", BOARD_DIR, ip2long($host), $time, 'is_banned'); + } + + // Log banned phone ips + if ($userpwd && $userpwd->isUserKnownOrVerified(10080) && $userpwd->postCount() > 20) { + if (preg_match('/Android|iPhone/', $_SERVER['HTTP_USER_AGENT'])) { + log_spam_filter_trigger('log_mobile_misban', BOARD_DIR, $resto, $host, 1); + } + } + + $redirect = 'https://www.' . L::d(BOARD_DIR) . '/banned'; + + if ($user_is_banned == 1) { + // Banned + error_redirect(S_BANNED, $redirect); + } + else if ($user_is_banned == 2) { + // Warned + error_redirect(S_WARNED, $redirect); + } + else { + // Ban evasion + error("Error: $user_is_banned"); + } + } + + /** + * Validate the maximum number of allowed threads per user + */ + if (!$resto) { + validate_user_thread_limit( + $_SERVER['REMOTE_ADDR'], + isset($ban_fields['password']) ? $pass : null, + $passid + ); + } + + /* + * Embedded data detection and banned re-post block + */ + if ($has_image) { + // Check if the file contains embedded data. + if (false && CLEANUP_UPLOADS) { + // Update the size if the file was modified + if (cleanup_uploaded_file($dest, $ext)) { + clearstatcache(true, $dest); + $fsize = filesize($dest); + if (!$fsize) { + error(S_TOOLARGE, $dest); + } + $md5 = md5_file($dest); + } + } + + // Check if the uploaded file should be blocked because of previous bans + if (check_for_banned_upload($md5)) { + //log_spam_filter_trigger('block_banned_reupload', BOARD_DIR, $resto, $host, 1, $md5); + error(S_FAILEDUPLOAD, $dest); + } + } + + // --- + + $autosage = false; + + // See bellow wordwrap2() + if (strpos($com, 'rep?~') !== false) { + $com = str_replace(array('~?rep?~', '~?erep?~'), '', $com); + } + + if (!has_level() || $capcode === 'none' || BOARD_DIR == 'test') { + $autosage = spam_filter_post_content_new(BOARD_DIR, $resto, $com, $sub, $name, $upfile_name, $pass, ($captcha_bypass && $passid) ? $passid : null); + spam_filter_post_ip($userpwd, $resto, $has_image); + } + + if (!$capcode || $capcode == 'none') { + check_md5_blacklist($md5, $original_md5, [ + 'resto' => $resto, + 'name' => $nameparts[0], + 'tripcode' => $nameparts[1], + 'filename' => "$insfile$ext", + 'password' => $pass, + '4pass_id' => ($captcha_bypass && $passid) ? $passid : null + ], $dest); + } + + spam_filter_post_trip( $name, $trip, $dest ); + + time_log( "ab" ); + + // Only process linebreaks for non-html posts + if (!$log_html_post) { + $com = nl2br($com, false); + $com = str_replace("\n", "", $com); //\n is erased + } + + $com .= $exiftext; // must be done after spam filter, since it has a javascript: link + + if (SJIS_TAGS) { + $com = sjis_parse($com); + } + + if( SPOILERS == 1 ) { + $com = spoiler_parse( $com ); + if (stripos( $com, '') !== false) { + $com = preg_replace('/(\s|
    |(?R))*<\/s>/', '', $com); + //$com = preg_replace('/(\S)(.*?)<\/s>(\S)/', '\\1\\2\\3', $com); + } + } + /* + if( JSMATH == 1 ) { + $com = jsmath_parse( $com ); + } + */ + if( CODE_TAGS ) { + $com = preg_replace( '#(\[code\](.{0,6})\[\/code\])#', '\\2', $com ); + + $com = code_parse( $com ); + + $com = str_replace( '

    ', '
    ', $com );
    +		$com = preg_replace( '#(
    ){4,}#', '


    ', $com ); + } + + if (OP_MARKUP && (!$resto || is_poster_op($host, $pass, $resto))) { + $com = parse_op_markup($com); + } + + // pull this down to here to get rid of any shenanigans + if (!$resto) { // new threads require subject or comment + if ($sub === '' && ($com === '' || preg_match('/^(?:
    |\s)+$/', $com))) { + if ($options_field === '' || !$has_image || !has_level()) { + error(S_NOTEXT_OP, $dest); + } + } + else if (TEXT_ONLY && $sub === '') { + error(S_NOSUB, $dest); + } + } + else if (!$has_image && ($com === '' || preg_match('/^(?:
    |\s)+$/', $com))) { // replies without image + error(S_NOTEXT, $dest); + } + + if( WORD_FILT && $word_filters_enabled ) { + $com = word_filter( $com, "com" ); + if( $sub ) + $sub = word_filter( $sub, "sub" ); + $namearr = explode( ' ', $name ); + if( strstr( $name, ' ' ) ) { + $nametrip = ' ' . $namearr[1]; + } else { + $nametrip = ""; + } + if( $namearr[0] != S_ANONAME ) + $name = word_filter( $namearr[0], "name" ) . $nametrip; + } + + /*if( $html != 1 || ( !has_level('manager') ) ) { + $com = wordwrap2( $com, 35, "{{w_br}}" ); + }*/ + + // April 2022 + /* + if (strpos($com, ':') !== false) { + $com = april_process_post_emotes($com, $_COOKIE['xa_sid']); + } + */ + $com = normalize_and_linkify($com); + + //$html = isset( $_POST['html'] ) && $_POST['html'] == 1; + $html = 0; + if( (!has_level('manager') && !has_flag('html')) || $html != 1 ) { + $com = wordwrap2( $com, 35, "{{w_br}}" ); + } + + $com = preg_replace( '#(>>>/[a-z0-9]+/[^ <$]*|>>[0-9]+)#', '~?rep?~\\1~?erep?~', $com ); + $com = preg_replace( "!(^|r>|r> )(>[^<]*)!", "\\1\\2", $com ); + $com = preg_replace( '#~?rep?~(.+?)~?erep?~#', '\\1', $com ); + + $com = str_replace( array( '~?rep?~', '~?erep?~' ), '', $com ); + + $com = str_replace( '{{w_br}}', '', $com ); + + $admin_style = "padding: 5px;margin-left: .5em;border-color: #faa;border: 2px dashed rgba(255,0,0,.1);border-radius: 2px"; //FIXME make it easier to edit css so this can go in it + if( $admin_highlight ) { + $com = "
    $com
    "; + } + + if( DISP_ID == 1 && !$uid ) { + $uid = $is_sage && !META_BOARD && !DISP_ID_NO_HEAVEN ? "Heaven" : generate_uid( $resto, $time ); + } + + // Validate comment for OPs by regex + if (!$resto && COM_REGEX) { + if (preg_match(COM_REGEX, $com) === 0) { + error(S_INVALID_COM); + } + } + + //post text is now completely created, thumbnail not + + if( !$silent_reject ) { + //logtime( "Before flood check" ); + $may_flood = has_level( 'janitor' ); + + if (!$may_flood || (!has_level() && (META_BOARD || $_POST['name'] != ''))) { + if( $com ) { + // Check for duplicate comments + $query = "select sql_no_cache max(time) from `%s` where com='%s' " . + "and host='%s' " . + "and time>%d"; + $result = mysql_board_call( $query, SQLLOG, $com, $host, $time - RENZOKU_DUPE ); + if( $ltime = mysql_result( $result, 0, 0 ) ) { + //check_fail_floodcheck($com); + $str = sprintf(S_RENZOKU_DUP, sec2hms( ( $ltime + RENZOKU_DUPE ) - $time, false, true ) ); + error( $str, $dest ); + } + mysql_free_result( $result ); + } + + /** + * Posting cooldowns + */ + if (!$resto) { + /** + * New threads + */ + $query = "select max(time) from `%s` where time>%d " . + "and host='%s' and root>0"; //root>0 == non-sticky + $result = mysql_board_call( $query, SQLLOG, ( $time - RENZOKU3 ), $host ); + if( $ltime = mysql_result( $result, 0, 0 ) ) { + $str = sprintf(S_RENZOKU3, sec2hms( ( $ltime + RENZOKU3 ) - $time, false, true ) ); + error( $str, $dest ); + } + mysql_free_result( $result ); + // Cross-board cooldown + $query = "SELECT 1 FROM user_actions WHERE ip = %d AND action = 'new_thread' AND board != '%s' AND time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)"; + $result = mysql_global_call($query, ip2long($host), BOARD_DIR); + if (mysql_num_rows($result) > 0) { + error( S_RENZOKU3, $dest ); // You must wait longer before posting another thread + } + } + else { + /** + * Replies + * Pass users have lower cooldowns + */ + + // Check for same image flood first + if ($has_image && $resto) { + $query = "SELECT time FROM `%s` WHERE host = '%s' AND md5 = '%s' AND resto != %d ORDER BY no DESC LIMIT 1"; + + $result = mysql_board_call($query, SQLLOG, $host, $md5, $resto); + + if ($flood_row = mysql_fetch_assoc($result)) { + $last_time = (int)$flood_row['time']; + + $cooldown = RENZOKU_DUPE; + $cooldown_error = S_RENZOKU2_DUP; + + if ($captcha_bypass) { + $cooldown = ceil((int)$cooldown / 2); + } + else { + $cooldown_error .= S_RENZOKU_PASS; + } + + if ($last_time > $time - $cooldown) { + error(sprintf($cooldown_error, sec2hms($last_time + $cooldown - $time, false, true)), $dest); + } + } + } + + // Now the standard cooldown + $query = "SELECT time, resto, fsize FROM `%s` WHERE host = '%s' AND resto > 0 ORDER BY no DESC LIMIT 1"; + + $result = mysql_board_call($query, SQLLOG, $host); + + if ($flood_row = mysql_fetch_assoc($result)) { + $last_time = (int)$flood_row['time']; + + if ($has_image) { + $cooldown = RENZOKU2; + $cooldown_error = S_RENZOKU2; + } + else { + $cooldown = RENZOKU; + $cooldown_error = S_RENZOKU; + } + + if ($captcha_bypass) { + $cooldown = ceil((int)$cooldown / 2); + } + else { + $cooldown_error .= S_RENZOKU_PASS; + } + + if ($last_time > $time - $cooldown) { + error(sprintf($cooldown_error, sec2hms($last_time + $cooldown - $time, false, true)), $dest); + } + } + } + /* + if (SAVE_XFF == 1 && $xff) { + // Check for multiple ips with same xff + $result = mysql_global_call( "select count(distinct ip)>2 from xff where xff='%s' and is_live=1", $xff ); + if( mysql_result( $result, 0, 0 ) ) { + auto_ban_poster( $name, 14, 1, "Detected 3 proxies for same IP", "Proxy/Tor exit node." ); + error( S_GENERICERROR, $dest ); + } + // Check for multiple xffs with same ip? + } + */ + // Check for OP bump limiting + if ($resto && RENZOKU_OP) { + $query = 'SELECT host, time FROM `%s` WHERE no = %d'; + $query = mysql_board_call($query, SQLLOG, $resto); + if ($query) { + $result = mysql_fetch_assoc($query); + // Poster is OP + if ($result && $result['host'] === $host) { + // OP can only bump his thread RENZOKU_OP_TIME seconds after its creation + if ($result['time'] > ($time - RENZOKU_OP_TIME)) { + $is_sage = 1; + } + // OP can only bump his thread every RENZOKU_OP_TIME2 seconds + else { + $query2 = "SELECT time FROM `%s` WHERE host = '%s' AND resto = %d ORDER BY no DESC LIMIT 1"; + $query2 = mysql_board_call($query2, SQLLOG, $host, $resto); + if ($query2) { + $result = mysql_fetch_assoc($query2); + if ($result && $result['time'] > ($time - RENZOKU_OP_TIME2)) { + $is_sage = 1; + } + } + mysql_free_result($query2); + } + } + } + mysql_free_result($query); + } + } + + // Minimal cooldowns for authed users (3s) + if ($may_flood) { + $query = "SELECT time FROM `%s` WHERE host = '%s' ORDER BY no DESC LIMIT 1"; + + $result = mysql_board_call($query, SQLLOG, $host); + + if ($flood_row = mysql_fetch_assoc($result)) { + $last_time = (int)$flood_row['time']; + + $cooldown = 5; + + if ($last_time > $time - $cooldown) { + error(sprintf(S_RENZOKU, sec2hms($last_time + $cooldown - $time, false, true)), $dest); + } + } + } + + time_log( "fc" ); + + $tensorchan_score = 0; + + // thumbnail + $image_path = ""; + $m_img = false; + if ($has_image) { + if( USE_THUMB && !UPLOAD_BOARD ) { + // Detect and block NSFW content + $_need_inference = tensorchan_is_needed($userpwd, $resto, $W, $H, $ext); + + if ($_need_inference) { + $_tensor_png = false; + } + else { + $_tensor_png = null; + } + + $ret = make_thumb( $dest, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5, $webm_sar, $_tensor_png); + + if (!$ret && $ext != ".pdf") { + error(S_IMGFAIL, $dest); + } + + if ($_need_inference && $_tensor_png) { + $tensorchan_score = tensorchan_check_nsfw($_tensor_png); + unset($_tensor_png); + } + } + + $name_part = UPLOAD_BOARD ? $insfile : $tim; + $image_path = IMG_DIR . $name_part . $ext; + if( move_uploaded_file( $dest, $image_path ) === false ) { + error( S_FAILEDUPLOAD, $dest ); + } + chmod( $image_path, 0664 ); + + if (MOBILE_IMG_RESIZE) { + $m_img = resize_mobile_image($image_path, $W, $H, $fsize, $tim, $ext); + } + + // Oekaki + if (ENABLE_PAINTERJS) { + // Replays + if (ENABLE_OEKAKI_REPLAYS) { + $oe_replay_path = null; + + if (isset($_FILES['oe_replay']) && $_FILES['oe_replay']['name'] === 'tegaki.tgkr' && $insfile === 'tegaki') { + if (oekaki_validate_replay($_FILES['oe_replay']) === true) { + $oe_replay_path = IMG_DIR . $tim . '.tgkr'; + + if (move_uploaded_file($_FILES['oe_replay']['tmp_name'], $oe_replay_path) === false) { + error(S_FAILEDUPLOAD); + } + + chmod($oe_replay_path, 0664); + } + } + + // Oekaki meta + if (isset($_POST['oe_time'])) { + if (isset($_POST['oe_src']) && $resto) { + $oe_src_pid = oekaki_get_valid_src_pid($_POST['oe_src'], BOARD_DIR, $resto); + } + else { + $oe_src_pid = null; + } + + $com .= oekaki_format_info( + $_POST['oe_time'], + $oe_replay_path ? $tim : null, + $oe_src_pid + ); + } + } + } + } + + //logtime( "Thumbnail created" ); + time_log( "t" ); + + // Infrequent flood check (dupe image) + if( $has_image && (!$capcode || $capcode === 'none')) { + if ($resto) { + $result = mysql_board_call("SELECT sql_no_cache `no`,`resto` FROM `" . SQLLOG . "` WHERE archived = 0 and (resto = %d OR no = %d) AND `md5`='%s' AND filedeleted=0 limit 1", $resto, $resto, $md5); + } + else { + $result = mysql_board_call("SELECT sql_no_cache `no`,`resto` FROM `" . SQLLOG . "` WHERE archived = 0 AND resto = 0 AND `md5`='%s' AND filedeleted=0 limit 1", $md5); + } + + if( mysql_num_rows( $result ) ) { + list( $dupeno, $duperesto ) = mysql_fetch_row( $result ); + if( !$duperesto ) $duperesto = $dupeno; + error( '' . S_DUPE . ' here.', $dest ); + } + + if ($resto && MAX_IMG_REPOST_COUNT > 0) { + $_query = 'SELECT COUNT(*) FROM `' . SQLLOG . "` WHERE archived = 0 AND resto != 0 AND `md5` = '%s' AND filedeleted = 0"; + + $result = mysql_board_call($_query, $md5); + + if ($result) { + $_count = (int)mysql_fetch_row($result)[0]; + + if ($_count >= MAX_IMG_REPOST_COUNT) { + error(S_DUPE); + } + } + } + + if ( defined('SQLLOGMD5') ) { + // TODO: There's a race here. This should just be INSERT and check for failure! + $result = mysql_board_call("SELECT sql_no_cache * FROM `%s` WHERE md5='%s' AND now > DATE_SUB(NOW(), INTERVAL 1 DAY) limit 1", SQLLOGMD5, $md5); + if ( mysql_num_rows( $result ) ) { + list( $dc_now, $dc_filename, $dc_md5p ) = mysql_fetch_row( $result ); + + if( $dc_now ) { + error('Error: You must wait longer before reposting this file.', $dest ); + } + } + } + } + + $rootpredicate = $resto ? "0" : "now()"; + + // ROBOT9000 + if (defined('ROBOT9000') && ROBOT9000) { + // Logged in uses can bypass r9k by using the "bypass_r9k" command in the Options field. + // Capcoded posts always bypass r9k. + if (($options_field !== 'bypass_r9k' || !has_level('janitor')) && $capcode === 'none') { + require_once 'plugins/robot9000.php'; + $r9k_status = r9k_process($com, $md5, ip2long($host)); + if ($r9k_status !== R9K_OK) { + error($r9k_status, $dest ); + } + } + } + + // FIX ME, comments with html get truncated and break the layout, but only mods and janitors can bypass the regular limits + if (strlen($com) > 65536) { + error(S_TOOLONG, $dest); + } + + //logtime( "Before insertion" ); + + //find sticky & autosage + // auto-sticky + //$sticky = false; + // autosagin is now done in spam_filter_post_content + //$autosage = spam_filter_should_autosage( $com, $sub, $name, $fsize, $resto, $W, $H, $dest, $insertid ); + + //old auto-sticky code -- disabled + // if(defined('AUTOSTICKY') && AUTOSTICKY) { + // $autosticky = preg_split("/,\s*/", AUTOSTICKY); + // if($resto == 0) { + // if($insertid % 1000000 == 0 || in_array($insertid,$autosticky)) + // $sticky = true; + // } + // } + + $flag_cols = ""; + $flag_vals = ""; + + if( $captcha_bypass ) { + $flag_cols .= ',4pass_id'; + $flag_vals .= ",'" . $passid . "'"; + } + + if ($since4pass) { + $flag_cols .= ',since4pass'; + $flag_vals .= ",$since4pass"; + } + /* + if( $sticky ) { + $flag_cols .= ",sticky"; + $flag_vals .= ",1"; + } + */ + //permasage just means "is sage" for replies + if( $resto ? $is_sage : $autosage ) { + $flag_cols .= ",permasage"; + $flag_vals .= ",1"; + } + + if( $capcode ) { + $flag_cols .= ',capcode'; + $flag_vals .= ",'$capcode'"; + } + + if ($m_img) { + $flag_cols .= ',m_img'; + $flag_vals .= ",1"; + } + + //$country = geoip_country_code_by_addr( $_SERVER['REMOTE_ADDR'] ); + //if( !$country ) $country = 'XX'; + $geo_data = GeoIP2::get_country($_SERVER['REMOTE_ADDR']); + + if ($geo_data && isset($geo_data['country_code'])) { + $country = $geo_data['country_code']; + + // FIXME: football cups + /* + if (BOARD_DIR === 'sp' && $country === 'GB' && isset($geo_data['sub_code'])) { + if ($geo_data['sub_code'] === 'WLS') { + $country = 'XW'; + } + else if ($geo_data['sub_code'] === 'SCT') { + $country = 'XS'; + } + else if ($geo_data['sub_code'] !== 'NIR') { + $country = 'XE'; + } + } + */ + } + else { + $country = 'XX'; + } + + // User Agent ID + $browser_id = spam_filter_get_browser_id(); + + // Request Signature + $_req_sig = spam_filter_get_req_sig(); + + /** + * Flood checks + */ + $_threat_score = 0; + + if (true || !$captcha_bypass) { + $_pwd_known = $userpwd && $userpwd->isUserKnownOrVerified(360); + $_pwd_trusted = $userpwd && ($userpwd->postCount() > 10 || $userpwd->verifiedLevel()); + $_pwd_verified = $userpwd && $userpwd->verifiedLevel(); + + if (!$_pwd_known) { + $_threat_score = spam_filter_get_threat_score($country, !$resto, true); + } + + // Sample the user agent of known users + if (false && $userpwd && $userpwd->isUserKnownOrVerified(4320) && $userpwd->postCount() > 10) { + if (mt_rand(0, 9999) < 2000) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('log_safe_req', BOARD_DIR, $resto, $host, 1, $_bot_headers); + } + } + + if (!$_pwd_known && ($_threat_score > 0.31)) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_txt_threat', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + if (false && !$_pwd_known && $resto == 18361689 && BOARD_DIR === 'fa' && mt_rand(0, 9) >= 1 && $country != 'US') { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_other', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'g' && strpos($_thread_sub, '/aicg/') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_aicg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_com, '/lolg/') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_lolg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + //show_post_successful_fake($resto); + //return; + } + + if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_com, '/overwatch') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_owg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + //show_post_successful_fake($resto); + //return; + } + + if (false && !$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'fa' && strpos($_thread_sub, 'Workwear General') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_denim', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + //show_post_successful_fake($resto); + //return; + } + + if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/bag/') !== false && ($_threat_score > 0.01 || $browser_id === '04d2237a2')) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_bag', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (false && !$_pwd_known && !$resto && (BOARD_DIR === 'co' || BOARD_DIR === 'a') && $country !== 'XX' && $browser_id === '02b99990d' && ($country == 'GB' || $country == 'DE' || $country == 'AU' || strpos($_COOKIE['_tcs'], $_SERVER['HTTP_X_TIMEZONE']) === false)) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_peridot', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/gbfg/') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_gbfg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + //show_post_successful_fake($resto); + //return; + } + + if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'v' && strpos($_thread_sub, 'gamesdonequick') !== false && $_threat_score >= 0.09 && mt_rand(0, 9) >= 1) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_adgq', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/zzz/') !== false && $_threat_score >= 0.09 && mt_rand(0, 9) >= 1) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_zzz', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + if (!$_pwd_verified && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/funkg/') !== false && $_threat_score >= 0.09) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_funkg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + if (!$has_image) { + if (!$_pwd_verified && $_threat_score >= 0.09 && mt_rand(0, 9) >= 5 && BOARD_DIR !== 'f') { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_pub_prox', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + } + else { + if (!$resto) { + $_thres = 3; + } + else { + $_thres = 4; + } + + if (!$_pwd_verified && $_threat_score >= 0.09 && mt_rand(0, 9) >= $_thres && BOARD_DIR !== 'f') { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_pub_prox', BOARD_DIR, $resto, $host, 1, $_bot_headers); + if (mt_rand(0, 1)) { + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + else { + show_post_successful_fake($resto); + return; + } + } + } + + if (false && BOARD_DIR === 'vg' && $country !== 'CO' && isset($_COOKIE['_tcs']) && strpos($_COOKIE['_tcs'], '.America/Bogota.') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('log_bag_co', BOARD_DIR, $resto, $host, 1, $_bot_headers); + } + + // FLOOD CHECK + $flood_status = 0;//spam_filter_is_post_flood($host, BOARD_DIR, $resto, null); + + if ($flood_status === 3) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + + log_spam_filter_trigger('block_flood_check', BOARD_DIR, $resto, $host, $flood_status, $_bot_headers); + + // Raw thread flood + if ($flood_status === 3) { + error(S_FAILEDUPLOAD); + } + else { + show_post_successful_fake($resto); + return; + } + } + + // Check Cloudflare's bot score and block using the lenient rangeban message + if ($userpwd && !$userpwd->isUserKnownOrVerified(1440) && isset($_SERVER['HTTP_X_BOT_SCORE'])) { + if (spam_filter_is_pwd_blocked($userpwd->getPwd(), 'block_bm_bot', 24)) { + log_spam_filter_trigger('block_bm_bot_pwd', BOARD_DIR, $resto, $host, $_SERVER['HTTP_X_BOT_SCORE']); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (spam_filter_is_likely_automated($memcached)) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + write_to_event_log('block_bm_bot', $host, [ + 'pwd' => $userpwd->getPwd(), + 'arg_num' => $_SERVER['HTTP_X_BOT_SCORE'], + 'board' => BOARD_DIR, + 'thread_id' => $resto, + 'meta' => $_bot_headers + ]); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + } + + // If the country has changed, log the pwd and then block it for the next 24h + if ($userpwd && !$userpwd->verifiedLevel() && $userpwd->postCount() < 20) { + if (spam_filter_has_country_changed($userpwd->getPwd())) { + log_spam_filter_trigger('block_country_changed', BOARD_DIR, $resto, $host, 1); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if ($userpwd->envChanged()) { + write_to_event_log('country_changed', $host, [ + 'pwd' => $userpwd->getPwd(), + 'arg_str' => $country, + 'board' => BOARD_DIR, + 'thread_id' => $resto + ]); + + log_spam_filter_trigger('block_country_changed', BOARD_DIR, $resto, $host, 1); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + } + } + + /** + * Custom flag selection + */ + if (ENABLE_BOARD_FLAGS) { + if ($_POST['flag'] === '0' || !isset($board_flags_array[$_POST['flag']])) { + $board_flag_code = ''; + + // FIXME: remove this eventually as we use localStorage now + if (isset($_COOKIE['4chan_flag'])) { + setcookie('4chan_flag', '', $time - 3600, '/', $cookie_domain); + } + } + else { + $board_flag_code = mysql_real_escape_string($_POST['flag']); + } + } + else { + $board_flag_code = ''; + } + + if ($board_flag_code) { + $board_flag_col = ',board_flag'; + $board_flag_val = ",'$board_flag_code'"; + } + else { + $board_flag_col = ''; + $board_flag_val = ''; + } + + if( $resto ) calculate_indexes_to_rebuild( $resto ); + + // Remove old replies if the thread is sticky+undead + if ($is_undead_sticky && STICKY_CAP > 1) { + $query = "SELECT MIN(no) FROM (SELECT no FROM `" . BOARD_DIR . "` WHERE resto = $resto ORDER BY no DESC LIMIT " . (STICKY_CAP - 1) . ") as subsel"; + $result = mysql_board_call($query); + if ($result) { + $prune_row = mysql_fetch_row($result); + + mysql_free_result($result); + + $min_no = (int)$prune_row[0]; + + if ($min_no > $resto) { + $query = "SELECT no FROM `" . BOARD_DIR . "` WHERE resto = $resto AND no < $min_no"; + $result = mysql_board_call($query); + + if ($result) { + while ($prune_row = mysql_fetch_assoc($result)) { + delete_post((int)$prune_row['no'], '', 0, 1, 1, 0); + } + + mysql_free_result($result); + } + } + } + } + + // April 2024 + /* + if ($_xa24_since4pass && !UPLOAD_BOARD && !JANITOR_BOARD) { + $name = april_2024_get_name(); + + if ($emails == '$DESU' && strlen($com) < 10000) { + $com .= '
    '; + } + } + */ + $user_meta = encode_user_meta($browser_id, substr($_req_sig, 0, 8), $userpwd); + + $insert_tries = 2; + do { + if( SKIP_DOUBLES == 1 ) mysql_board_call( "START TRANSACTION" ); + $query = "insert into `" . SQLLOG . "` (now,name,sub,com,host,pwd,email,filename,ext,w,h,tn_w,tn_h,tim,time,last_modified,md5,fsize,root,resto$flag_cols,tmd5,id,country$board_flag_col) values (" . + "'" . $now . "'," . + "'" . mysql_real_escape_string( $name ) . "'," . + mysql_nullify( mysql_real_escape_string( $sub ) ) . "," . + "'" . mysql_real_escape_string( $com ) . "'," . + "'" . mysql_real_escape_string( $host ) . "'," . + "'" . mysql_real_escape_string( $pass ) . "'," . + "'" . mysql_real_escape_string($user_meta) . "'," . + "'" . mysql_real_escape_string( $insfile ) . "'," . + mysql_nullify( $ext ) . "," . + (int)$W . "," . + (int)$H . "," . + (int)$TN_W . "," . + (int)$TN_H . "," . + "'" . $tim . "'," . + (int)$time . "," . + (int)$time . "," . + mysql_nullify( $md5 ) . "," . + (int)$fsize . "," . + $rootpredicate . "," . + (int)$resto . + $flag_vals . "," . + mysql_nullify( $tmd5 ) . "," . + mysql_nullify( $uid ) . "," . + "'$country'$board_flag_val)"; + + if( !$result = mysql_board_call( $query ) ) { + echo S_SQLFAIL; + } //post registration + time_log( "i" ); + + $insertid = mysql_board_insert_id(); + if( SKIP_DOUBLES == 1 ) { + if( has_doubles( $insertid ) ) { + mysql_board_call( "ROLLBACK" ); + // retry + } else { + mysql_board_call( "COMMIT" ); + $insert_tries = 0; + } + } else { + $insert_tries = 0; + } + } while( $insert_tries-- ); + + // Captcha bypass token + if ($captcha_bypass_allow_credits && $_threat_score < 0.15) { + set_twister_captcha_credits($memcached, $host, $userpwd, $time); + } + /* + if (!$captcha_bypass) { + if (mt_rand(0, 99) === 0) { + write_to_event_log('known_sample', $host, [ + 'board' => BOARD_DIR, + 'thread_id' => $resto, + 'arg_num' => $userpwd->isUserKnown(), + ]); + } + } + */ + + $_can_upa = false; + + if ($userpwd && $userpwd->verifiedLevel()) { + $_can_upa = true; + } + else if ($captcha_bypass) { + $_can_upa = true; + } + else if ($_threat_score < 0.09) { + $_can_upa = true; + } + + if ($_can_upa) { + $userpwd->updatePostActivity(!$resto, $has_image); + } + + $userpwd->setCookie($cookie_domain); + + // April 2019 + /* + if (defined('LIKE_MAX_LIKES') && LIKE_MAX_LIKES > 0) { + like_update_post_score(); + } + */ + // Halloween 2017 + /* + if ($resto && defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky2017') { + process_halloween_score($com, $resto, $passid, $pass, $pass_is_bannable); + } + */ + // April 2018 + //update_april_team_scores(); + + // Log mod action if posted with html + if ($log_mod_action) { + $action_log_post = array( + 'no' => $insertid, + 'name' => $name, + 'sub' => $sub, + 'com' => $com, + 'filename' => $insfile, + 'ext' => $ext + ); + + if ($log_html_post) { + $action_log_post['com'] = htmlspecialchars($com, ENT_QUOTES); + log_mod_action(4, $action_log_post); + } + // capcode posting + if ($log_capcode_post) { + $action_log_post['name'] .= ' ## ' . ucfirst($capcode); + log_mod_action(5, $action_log_post, $capcode === 'verified'); + } + } + + if( $resto ) { //sage or age action + $resline = mysql_board_call( "select count(no) from `" . SQLLOG . "` where archived=0 and resto=" . $resto ); + $countres = mysql_result( $resline, 0, 0 ); + + $permasage_hours = (int)PERMASAGE_HOURS; + + if ($permasage_hours > 0) { + $time_col = 'time,'; + } + else { + $time_col = ''; + } + + // FIXME: a similar query is done at line ~4723 + $resline = mysql_board_call( "select {$time_col}sticky,permasage,permaage,root from `" . SQLLOG . "` where no=" . $resto ); + $resline = mysql_fetch_assoc($resline); + + if ($resline['sticky'] || $resline['permasage']) { + $root_col = ''; + } + else if ($resline['permaage']) { + $root_col = 'root=now(),'; + } + else if ($is_sage || $countres >= MAX_RES) { + $root_col = ''; + } + else if ($permasage_hours && ($time - ($permasage_hours * 3600) >= $resline['time'])) { + $root_col = ''; + } + else { + $root_col = 'root=now(),'; + + if (!$captcha_bypass && BOARD_DIR === 'jp') { + if (!spam_filter_can_bump_thread($resline['root'])) { + $root_col = ''; + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('necrobump', BOARD_DIR, $resto, $host, 1, $_bot_headers); + } + } + } + + mysql_board_call("update `" . SQLLOG . "` set {$root_col}last_modified=%d where no=%d", $_SERVER['REQUEST_TIME'], $resto); + } + + if( defined( 'AUTOSTICKY' ) && AUTOSTICKY ) { + $autosticky = preg_split( "/,\s*/", AUTOSTICKY ); + if( $resto == 0 ) { + if( $insertid % 1000000 == 0 || in_array( $insertid, $autosticky ) ) { + $sticky = true; + mysql_board_call( "update " . SQLLOG . " set sticky=1,root=root where no=$insertid" ); + } + } + } + + if( SAVE_XFF == 1 && $xff ) { + mysql_global_do( "INSERT INTO xff (tim,board,xff,ip,postno,is_live) VALUES ('%s','%s','%s',%d,%d,1)", $tim, BOARD_DIR, $xff, ip2long( $host ), $insertid ); + } + + if (UPLOAD_BOARD && $md5 ) { + $result = mysql_board_call( "insert ignore into `%s` (filename,md5) values('%s','%s')", SQLLOGMD5, $insfile, $md5 ); + } + + // determine url to redirect to + $proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:"; + if( !$is_nonoko && !$resto ) { + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $insertid . PHP_EXT2; + } else if( !$is_nonoko ) { + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $resto . PHP_EXT2 . '#p' . $insertid; + } else { + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . '/'; + } + + // To let the JavaScript thread watcher know the newly created thread ID + if (!$resto && isset($_POST['awt'])) { + setcookie('4chan_awt', $insertid, 0, '/' . BOARD_DIR . '/', $cookie_domain); + } + + show_post_successful( $mes, $com, $insertid, $resto, $redirect, $delay_refresh ); + + $static_rebuild = ( STATIC_REBUILD == 1 ); + //logtime( "Before trim_db" ); + + // trim database + if (!$resto) { + + if (!$static_rebuild) { + trim_db(); + + if (ENABLE_ARCHIVE && ARCHIVE_MAX_AGE) { + trim_archive(); + } + } + } + + //logtime( "After trim_db" ); + //time_log( "tr" ); + + $_need_updatelog = true; + + if (AUTOARCHIVE_CAP && $resto && !$sticky && !$undead && ENABLE_ARCHIVE) { + if (count_thread_replies(BOARD_DIR, $resto) >= AUTOARCHIVE_CAP) { + $_need_updatelog = false; + archive_thread($resto); + } + } + + // update html + if ($_need_updatelog) { + updatelog( $resto ? $resto : $insertid ); + } + //logtime( "Pages rebuilt" ); + //time_log( "r" ); + + // late tasks happen below here + iplog_add( BOARD_DIR, $insertid, $host, $time, $resto == 0, $tim, $has_image ); + + // Auto-report possibly nsfw post + if ($tensorchan_score && $tensorchan_score > 0.5) { + tensorchan_log(BOARD_DIR, $insertid, $resto, $tim, $ext, $tensorchan_score); + } + } else { + // silent reject + $insertid = 0; + $noko = 0; + } + /* + if( STATS_USER_JS ) { + mysql_global_do( "UPDATE `user_stats` SET `count` = `count`+1 WHERE name='%s'", $stats_ok ); + } + */ +} + +// Redirects to the most rcently created thread +// This is to confuse spambots +function show_post_successful_fake($resto = 0, $captcha_passed = true) { + $thread_id = (int)$resto; + $insert_id = 0; + + if (!$resto) { + $query = 'SELECT resto FROM `' . BOARD_DIR . '` WHERE resto != 0 ORDER BY resto DESC LIMIT 1'; + $res = mysql_board_call($query); + if ($res) { + $row = mysql_fetch_row($res); + $insert_id = (int)$row[0]; + } + } + else { + $query = 'SELECT no FROM `' . BOARD_DIR . '` ORDER BY no DESC LIMIT 1'; + $res = mysql_board_call($query); + if ($res) { + $row = mysql_fetch_row($res); + $insert_id = (int)$row[0] + 1; + } + } + + if (!$thread_id) { + $redirect = 'https://boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $insert_id . PHP_EXT2; + } + else { + $redirect = 'https://boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $thread_id . PHP_EXT2 . '#p' . $insert_id; + } + + $cookie_domain = '.' . L::d(BOARD_DIR); + + $now = $_SERVER['REQUEST_TIME']; + + // Name cookie + $c_name = $_POST['name']; + setrawcookie('4chan_name', rawurlencode($c_name), $now + ($c_name ? (7 * 24 * 3600) : -3600), '/', $cookie_domain); + + // Password cookie + $userpwd = UserPwd::getSession(); + + if ($userpwd) { + if ($captcha_passed) { + $userpwd->setCookie($cookie_domain); + } + else if (!$userpwd->isFake() && !$userpwd->isNew()) { + $userpwd->setCookie($cookie_domain); + } + else { + UserPwd::setFakeCookie($now, $cookie_domain); + } + } + + show_post_successful(null, null, $insert_id, $thread_id, $redirect); +} + +function show_post_successful( $mes, $com, $insertid, $resto, $redirect, $delay_refresh = false ) { + if (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json') { + return show_post_successful_json($insertid, $resto); + } + + if( !$mes ) $mes = S_POSTING_DONE; + $time_to_refresh = ( $delay_refresh ) ? 10 : 1; + $script = ""; + if( defined( 'POST_SUCCESSFUL_FILE' ) ) { + $file = file_get_contents( POST_SUCCESSFUL_FILE ); + $success = str_replace( "@REDIRECT@", $redirect, $file ); + } else { + // FIXME templating + $icon = DEFAULT_BURICHAN ? 'favicon-ws.ico' : 'favicon.ico'; + $defaultcss = DEFAULT_BURICHAN ? 'yotsubluenew' : 'yotsubanew'; + $cssVersion = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION; + $sg = style_group(); + + $styles = array( + 'Yotsuba New' => "yotsubanew.$cssVersion.css", + 'Yotsuba B New' => "yotsubluenew.$cssVersion.css", + 'Futaba New' => "futabanew.$cssVersion.css", + 'Burichan New' => "burichannew.$cssVersion.css", + 'Photon' => "photon.$cssVersion.css", + 'Tomorrow' => "tomorrow.$cssVersion.css" + ); + + $css = ''; + + if( isset( $_COOKIE[$sg] ) ) { + if( isset( $styles[$_COOKIE[$sg]] ) ) { + $css = ''; + } + } else { + $dcssl = $defaultcss . '.' . $cssVersion . '.css'; + $css = ''; + } + + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) { + $css = ''; + } + + if( BOARD_DIR == 'j' ) { + $css = ''; + } + + + $script .= ''; + $success = "$script" . S_POSTING_DONE . "$css

    $mes

    "; + } + echo $success; + + if ($resto) { + fastcgi_finish_request(); + } +} + +function show_post_successful_json($post_id, $thread_id) { + header('Content-Type: application/json'); + + echo '{"tid":' . $thread_id . ',"pid":' . $post_id . '}'; + + if ($thread_id) { + fastcgi_finish_request(); + } +} + +function resredir( $res, $delete = 0, $no_exit = false ) { + if (!$_SERVER["HTTP_REFERER"]) { + $proto = 'https:'; + } + else { + $proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:"; + } + + $res = (int)$res; + //mysql_board_lock( true ); + if( !$redir = mysql_board_call( "select no,resto from `" . SQLLOG . "` where no=" . $res ) ) { + echo S_SQLFAIL; + } + list( $no, $resto ) = mysql_fetch_row( $redir ); + + // if we're deleting and no post/resto (thread gone) + if( !$no && $delete ) { + // send us back to the board + updating_index(); + //mysql_board_unlock(); + if (!$no_exit) { + die; + } + else { + return; + } + } + + if( !JANITOR_BOARD ) { + header("Cache-Control: public, max-age=2"); + } + + if( !$no ) { + // If no < max(no) then this could be 410 Gone. + http_response_code(404); + error( S_NOTHREADERR, $dest ); + } + + if( $resto == "0" ) { // thread + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $no . PHP_EXT2 . '#p' . $no; + } else { + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $resto . PHP_EXT2 . '#p' . $no; + } + + $redirect = JANITOR_BOARD ? str_replace( 'boards.', 'sys.', $redirect ) : $redirect; + + header("Location: $redirect", true, 301); + echo ""; + //mysql_board_unlock(); +} + +function tensorchan_is_needed($userpwd, $resto, $W, $H, $ext) { + // Inference is disabled + if (!defined('TENSORCHAN_MODE') || !TENSORCHAN_MODE) { + return false; + } + + // Can't check + if (!$userpwd) { + return false; + } + + // User is known or verified + if ($userpwd->isUserKnownOrVerified(240)) { // 4 hours + return false; + } + + // OPs only but the post is a reply + if (TENSORCHAN_MODE == 1 && $resto) { + return false; + } + + if ($W < 150 || $H < 150 || $ext == '.pdf') { + return false; + } + + return true; +} + +function tensorchan_check_nsfw($tensor_png) { + if (!$tensor_png) { + return false; + } + + $tensor_res = tensorchan_predict($tensor_png); + + if (!$tensor_res) { + return false; + } + + if (isset($tensor_res['error'])) { + write_to_event_log('tensor_err', $_SERVER['REMOTE_ADDR'], [ + 'board' => BOARD_DIR, + 'meta' => htmlspecialchars($tensor_res['error']) + ]); + + return false; + } + else { + if (!isset($tensor_res['nsfw'])) { + return false; + } + + return (float)$tensor_res['nsfw']; + } +} + +function tensorchan_log($board, $post_id, $thread_id, $file_id, $file_ext, $score) { + $post_id = (int)$post_id; + $thread_id = (int)$thread_id; + $score = (float)$score; + + $sql =<< $_err]; + } + + $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($resp_status >= 300) { + return ["error" => "HTTP $resp_status: $resp"]; + } + + curl_close($curl); + + if ($resp[0] == '{') { + $resp = json_decode($resp, true); + } + else { + return ["error" => "Not a JSON response"]; + } + + return $resp; +} + +function background_color( $im, $is_thread ) +{ + if( DEFAULT_BURICHAN == 0 ) { + if( $is_thread ) { + list( $r, $g, $b ) = array(0xFF, 0xFF, 0xEE); + } else { + list( $r, $g, $b ) = array(0xF0, 0xE0, 0xD6); + } + } else { + if( $is_thread ) { + list( $r, $g, $b ) = array(0xEE, 0xF2, 0xFF); + } else { + list( $r, $g, $b ) = array(0xD6, 0xDA, 0xF0); + } + } + + return imagecolorallocate( $im, $r, $g, $b ); +} + +function optimize_thumb($tmppath) { + system("/usr/local/bin/jpegoptim -q --strip-all '$tmppath' >/dev/null 2>&1"); +} + +// Calculates perceptual hash for a thumbnail +// $img is a reference to a GD resource +function get_thumb_dhash(&$img, $width, $height) { + if (!$img) { + return false; + } + + $data = imagecreatetruecolor(9, 8); + imagecopyresampled($data, $img, 0, 0, 0, 0, 9, 8, $width, $height); + imagefilter($data, IMG_FILTER_GRAYSCALE); + + $hash = 0; + $bit = 1; + + for ($y = 0; $y < 8; $y++) { + $previous = imagecolorat($data, 0, $y) & 0xFF; + + for ($x = 1; $x < 9; $x++) { + $current = imagecolorat($data, $x, $y) & 0xFF; + + if ($previous > $current) { + $hash |= $bit; + } + + $bit = $bit << 1; + $previous = $current; + } + } + + imagedestroy($data); + + return sprintf("%016x", $hash); +} + +//thumbnails +function make_thumb( $fname, $tim, $ext, $resto, &$TN_W, &$TN_H, &$tmd5, $webm_sar = null, &$tensor_png = null ) +{ + $thumb_dir = THUMB_DIR; //thumbnail directory + $outpath = $thumb_dir . $tim . 's.jpg'; + if( !$resto ) { + $width = MAX_W; //output width + $height = MAX_H; //output height + $jpeg_quality = 50; + } else { + $width = MAXR_W; //output width (imgreply) + $height = MAXR_H; //output height (imgreply) + $jpeg_quality = 40; + } + + if( ENABLE_PDF == 1 && $ext == '.pdf' ) { + // create jpeg for the thumbnailer + $pdfjpeg = IMG_DIR . $tim . '.pdf.tmp'; + @exec( "/usr/local/bin/gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=jpeg -sOutputFile=$pdfjpeg $fname" ); + if( !file_exists( $pdfjpeg ) ) unlink( $fname ); + $fname = $pdfjpeg; + } + else if (ENABLE_WEBM && ($ext == '.webm' || $ext == '.mp4')) { + $webm_thumb = thumb_webm($fname, $ext); + + if (!$webm_thumb) { + unlink($fname); + return false; + } + + $fname = $webm_thumb; + } + + $size = @GetImageSize($fname); + + if ($size === false) { + return; + } + + // File size needs to be checked again because of cleanup_uploaded_file + if (defined('MAX_DIMENSION') && $ext == '.gif') { + if ($size[0] > MAX_DIMENSION || $size[1] > MAX_DIMENSION) { + error(S_TOOLARGERES); + } + } + + $memory_limit_increased = false; + $maybe_transparent = true; + // Don't increase memory limit on CLI so that the user can do it with a CLI parameter instead + if( $size[0] * $size[1] > 3000000 && isset($_SERVER['REMOTE_ADDR']) ) { + $memory_limit_increased = true; + ini_set( 'memory_limit', memory_get_usage() + $size[0] * $size[1] * 15 ); // for huge images + } + switch( $size[2] ) { + case 1 : + $im_in = ImageCreateFromGIF( $fname ); + if (!$im_in) { + return; + } + break; + case 2 : + $im_in = ImageCreateFromJPEG( $fname ); + if( !$im_in ) { + return; + } + $maybe_transparent = false; + break; + case 3 : + $im_in = ImageCreateFromPNG( $fname ); + if( !$im_in ) { + return; + } + break; + default : + return; + } + + $source_w = $size[0]; + $source_h = $size[1]; + + if ($webm_sar) { + if ($webm_sar > 1) { + $size[1] = round($size[1] / $webm_sar); + + if ($size[1] == 0) { + $size[1] = 1; + } + } + else { + $size[0] = round($size[0] * $webm_sar); + + if ($size[0] == 0) { + $size[0] = 1; + } + } + } + + // Resizing + if( $size[0] > $width || $size[1] > $height ) { + $key_w = $width / $size[0]; + $key_h = $height / $size[1]; + ( $key_w < $key_h ) ? $keys = $key_w : $keys = $key_h; + $out_w = floor( $size[0] * $keys ); + $out_h = floor( $size[1] * $keys ); + } else { + $out_w = $size[0]; + $out_h = $size[1]; + } + + // the thumbnail is created + $im_out = ImageCreateTrueColor( $out_w, $out_h ); + if( !$im_out ) return; + if( $maybe_transparent ) { + $background = background_color( $im_out, $resto == 0 ); + ImageFill( $im_out, 0, 0, $background ); + } + // copy resized original + ImageCopyResampled( $im_out, $im_in, 0, 0, 0, 0, $out_w, $out_h, $source_w, $source_h ); + $tmppath = tempnam( ini_get( "upload_tmp_dir" ), "thumb" ); + // thumbnail saved + ImageJPEG( $im_out, $tmppath, $jpeg_quality ); + + // Generate a perceptual hash from the original image + $tmd5 = Phash::hash($im_in, $source_w, $source_h); + + if ($tmd5 === false) { + $tmd5 = ''; + } + + // Create the PNG file for inference + if ($tensor_png !== null) { + $tensor_png_dim = TENSORCHAN_DIM; + $tensor_png_out = ImageCreateTrueColor($tensor_png_dim, $tensor_png_dim); + ImageCopyResampled($tensor_png_out, $im_in, + 0, 0, 0, 0, + $tensor_png_dim, $tensor_png_dim, $source_w, $source_h + ); + $stream = fopen('php://memory','r+'); + imagepng($tensor_png_out, $stream); + rewind($stream); + $tensor_png = stream_get_contents($stream); + fclose($stream); + ImageDestroy($tensor_png_out); + } + + // Cleanup + ImageDestroy( $im_in ); + ImageDestroy( $im_out ); + + optimize_thumb($tmppath); + + //$tmd5 = md5_file( $tmppath ); + rename_across_device( $tmppath, $outpath ); + + // if PDF was thumbnailed delete the orig jpeg + if (isset($pdfjpeg)) { + unlink($pdfjpeg); + } + // delete original webm frame + else if (isset($webm_thumb)) { + unlink($webm_thumb); + } + + if( $memory_limit_increased ) + ini_restore( 'memory_limit' ); + + $TN_W = $out_w; + $TN_H = $out_h; + + return $outpath; +} + +/* text plastic surgery */ +// you can call with skip_bidi=1 if cleaning a paragraph element (like $com) +function sanitize_text( $str, $skip_bidi = 0, $allow_html = false ) +{ + global $admin, $html; + // stupid unicode-hack removal + if( BOARD_DIR != 'jp' && BOARD_DIR != 'a' && BOARD_DIR != 'b' && !SJIS_TAGS ) { + $str = preg_replace( '#[\x{00A0}\x{3000}]#u', ' ', $str ); + + } + + if( !CODE_TAGS && !SJIS_TAGS) { + $str = preg_replace( "/([ \t\f]|\xE2\x80\x8B|\xE2\x80\xA9)+/", " ", $str ); //collapse multiple spaces like HTML does + } else { + // fix tabs for html compression + $str = str_replace( "\t", " ", $str ); + } + + $str = trim( $str ); //blankspace removal + if( get_magic_quotes_gpc() ) { //magic quotes is deleted (?) + $str = stripslashes( $str ); + } + + if ($allow_html && $html == 1 && (has_level('manager') || has_flag('html') || has_flag('developer'))) { + $str = purify_html($str); + } else { + $str = htmlspecialchars( $str, ENT_QUOTES ); + } + + if( $skip_bidi == 0 ) { + // fix malformed bidirectional overrides - insert as many PDFs as RLOs + //RLO + $str .= str_repeat( "\xE2\x80\xAC", substr_count( $str, "\xE2\x80\xAE" /* U+202E */ ) ); + $str .= str_repeat( "‬", substr_count( $str, "‮" ) ); + $str .= str_repeat( "‬", substr_count( $str, "‮" ) ); + //RLE + $str .= str_repeat( "\xE2\x80\xAC", substr_count( $str, "\xE2\x80\xAB" /* U+202B */ ) ); + $str .= str_repeat( "‬", substr_count( $str, "‫" ) ); + $str .= str_repeat( "‬", substr_count( $str, "‫" ) ); + } + + return $str; +} + +// TODO: rewrite this to use less SQL queries +function report() { + global $captcha_bypass; + + $host = $_SERVER['REMOTE_ADDR']; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($host, MAIN_DOMAIN, $_COOKIE['4chan_pass']); + } + else { + $userpwd = new UserPwd($host, MAIN_DOMAIN); + } + + if (BOARD_DIR === 'test') { + require_once('forms/report-test.php'); + require_once('modes/report-test.php'); + } + else { + require_once('forms/report.php'); + require_once('modes/report.php'); + } + + if (!CAN_REPORT_POSTS) { + fancydie(S_CANNOTREPORTPOSTS); + } + + $no = (int)$_GET['no']; + + if ($no <= 0) { + fancydie(S_POST_DEAD); + } + + $post = report_check_post(BOARD_DIR, $no); + + if (!isset($_COOKIE['4chan_auser']) && !isset($_COOKIE['pass_enabled'])) { + $no_captcha = report_can_bypass_captcha($host, $userpwd, $post); + } + else { + $no_captcha = false; + } + + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + header( 'Cache-Control: private, no-cache, must-revalidate' ); + header( 'Expires: -1' ); + + // Doesn't check bans here + report_check_ip( BOARD_DIR, $no, false); + + form_report(BOARD_DIR, $no, $no_captcha); + } + else { + if (valid_captcha_bypass() !== true && $no_captcha !== true) { + if (CAPTCHA_TWISTER) { + $_m = create_memcached_instance(); + + if (isset($_POST['t-challenge']) && $_POST['t-challenge'] === 'noop') { + if (use_twister_captcha_credit($_m, $host, $userpwd) === false) { + error(S_CAPTCHATIMEOUT); + } + } + else if (is_twister_captcha_valid($_m, $host, $userpwd, BOARD_DIR, 1) === false) { + error(S_BADCAPTCHA); + } + } + else { + start_recaptcha_verify(); + + if (!$captcha_bypass) { + end_recaptcha_verify(); + } + } + } + + // Also checks for bans + report_check_ip(BOARD_DIR, $no, true); + + if (!isset($_POST['cat']) && !isset($_POST['cat_id'])) { + fancydie('Invalid category selected.'); + } + + if ($_POST['cat']) { + $cat_id = (int)$_POST['cat']; + } + else if ($_POST['cat_id']) { + $cat_id = (int)$_POST['cat_id']; + } + else { + $cat_id = null; + } + + if (!$cat_id) { + fancydie('Invalid category selected.'); + } + /* + if ($no_captcha) { + write_to_event_log('skip_rep_captcha', $host, [ + 'board' => BOARD_DIR, + 'thread_id' => $post['resto'] ? $post['resto'] : $post['no'], + 'post_id' => $post['no'] + ]); + } + */ + report_submit(BOARD_DIR, $no, $cat_id); // script dies here + } + + die( '' ); +} + +/** + * Archive deletion function + * only works on archived posts, for authed users + */ +function arcdel($no, $redirect = false, $redirect_res = null) { + global $onlyimgdel; + + $delno = array(); + $time = $_SERVER['REQUEST_TIME']; + reset( $_POST ); + + while ($item = each($_POST)) { + if ($item[1] == 'delete') { + $delno[] = $item[0]; + } + } + + $numdeletions = count($delno); + + if (!$numdeletions) { + return; + } + + $rebuild_archive_json = false; + + $rebuild = array(); + + for ($i = 0; $i < $numdeletions; $i++) { + $resto = delete_post($delno[$i], '', $onlyimgdel, 0, 1, $numdeletions == 1, false, true); + if ($resto) { + $rebuild[$resto] = true; + } + else if (!$onlyimgdel) { + $rebuild_archive_json = true; + } + } + + if (!has_level('janitor')) { + mysql_global_call("INSERT INTO user_actions (ip,board,action,postno,time) VALUES (%d,'%s','delete',%d,now())", ip2long( $_SERVER["REMOTE_ADDR"] ), BOARD_DIR, $delno[0]); + } + + if ($redirect) { + if ($redirect_res) { + resredir($redirect_res, 1, true); + } + else { + updating_index(); + } + + fastcgi_finish_request(); + } + + foreach ($rebuild as $thread_id => $true) { + rebuild_archived_thread($thread_id); + } + + if ($rebuild_archive_json && ENABLE_JSON_THREADS) { + generate_board_archived_json(); + } +} + +function user_delete( $no, $pwd, $redirect = false, $redirect_res = null ) +{ + global $pwdc, $onlyimgdel, $captcha_bypass; + + if (UPLOAD_BOARD && $onlyimgdel) { + error("It doesn't make any sense to do a file-only delete on a file board!"); + } + + $delno = array(); + $time = $_SERVER['REQUEST_TIME']; + $delflag = false; + reset( $_POST ); + + while( $item = each( $_POST ) ) { + if( $item[1] == 'delete' ) { + array_push( $delno, $item[0] ); + $delflag = true; + } + } + + $user_is_known = false; + + if ($pwdc) { + $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], MAIN_DOMAIN, $pwdc); + + if ($userpwd) { + $pwd = $userpwd->getPwd(); + $user_is_known = $userpwd->maskLifetime() >= 900; + } + else { + $pwd = null; + } + } + else { + $pwd = null; + } + + $numdeletions = count( $delno ); + if( !$numdeletions ) return; + $flag = false; + + if( !has_level( 'janitor' ) ) { + $n = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='delete' and time >= subdate(now(), interval 1 hour)", RENZOKU_DEL_HOURLY, ip2long( $_SERVER['REMOTE_ADDR'] ) ); + list( $h ) = mysql_fetch_row( $n ); + + if( $h ) { + //check_fail_floodcheck($no); + error(S_FLOOD_DEL); + } + + $n = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='delete' and time >= subdate(now(), interval 1 day)", RENZOKU_DEL_DAILY, ip2long( $_SERVER['REMOTE_ADDR'] ) ); + list( $h ) = mysql_fetch_row( $n ); + + if( $h ) { + //check_fail_floodcheck($no); + error(S_FLOOD_DEL); + } + } + + $rebuild = array(); // keys are pages that need to be rebuilt (0 is index, of course) + + $lazy_rebuild = false; + + if (isset($_POST['tool']) && $_POST['tool']) { + $tool = $_POST['tool']; + } + else { + $tool = null; + } + + // Manual, single post deletion. Only rebuilds one page if deleting a reply. + if ($numdeletions == 1) { + $resto = delete_post( $delno[0], $pwd, $onlyimgdel, 0, 1, $numdeletions == 1, false, false, $tool, $user_is_known ); + if ($resto) { + $rebuild[$resto] = 1; + calculate_indexes_to_rebuild($resto); + $lazy_rebuild = true; + } + } + // Other (multi, automatic, etc...) + else { + for( $i = 0; $i < $numdeletions; $i++ ) { + $resto = delete_post( $delno[$i], $pwd, $onlyimgdel, 0, 1, $numdeletions == 1, false, false, $tool, $user_is_known ); + if( $resto ) { + $rebuild[$resto] = 1; + } + } + } + + if (!has_level('janitor')) { + mysql_global_call("INSERT INTO user_actions (ip,board,action,postno,time) VALUES (%d,'%s','delete',%d,now())", ip2long( $_SERVER["REMOTE_ADDR"] ), BOARD_DIR, $delno[0]); + } + + if ($redirect) { + if ($redirect_res) { + resredir($redirect_res, 1, true); + } + else { + updating_index(); + } + + fastcgi_finish_request(); + } + + rebuild_deletions($rebuild, $lazy_rebuild); +} + +function updatelog_remote( $no, $noidx ) +{ + if( !has_level() ) die( '' ); // anti dos + $no = intval( $no ); + $noidx = !!$noidx; + // FIXME, running this when $noidx is true breaks cross-thread quotelinks. + updatelog( $no, $noidx ); +} + +function _print( $s, $echo = 1 ) +{ + if( $echo ) { + echo $s; + + return; + } + + ob_flush(); + flush(); + + echo $s; + echo str_repeat( ' ', 256 ) . "\n"; + + ob_flush(); + flush(); +} + +function fancystyle() +{ + $style = << +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 12pt; +} + +h1 { + margin: 0; + padding: 0; +} + +HTML; + + + return $style; +} + +function rebuild_catalog( $shutup = false ) +{ + if( !has_level() ) die(); + if( !$shutup ) { + echo fancystyle(); + echo '

    Rebuilding catalog...

    '; + } + + $start = microtime( true ); + generate_catalog(); + $time = round( microtime( true ) - $start, 6 ); + + if( !$shutup ) { + echo 'Done!

    Rebuilding took ' . $time . ' seconds.

    Redirecting to catalog...

    '; + die(); + } +} + +function rebuild_boards_json() +{ + if( !has_level() ) die(); + echo fancystyle(); + echo '

    Rebuilding boards.json...

    '; + + $start = microtime( true ); + $query = mysql_global_call( "SELECT dir as board,name as title FROM boardlist ORDER BY board ASC" ); + + $boards = array(); + + $host = 'https://sys.int'; + + $post = array( + 'mode' => 'cataloginfo' + ); + + while( $row = mysql_fetch_assoc( $query ) ) { + if( $row['board'] == 'vp' ) $row['title'] = 'Pokémon'; + //cataloginfo + $url = "$host/{$row['board']}/imgboard.php"; + + $ch = rpc_start_request($url, $post, null, true); + + $response = rpc_finish_request($ch, $error, $httperror); + + if (!$response) { + die( 'Could not generate info for /' . $row['board'] . '/; ' . $error ); + } + + $json = json_decode($response, true); + + foreach( $json as $key => $val ) { + if( $key != 'board' && ctype_digit( $val ) ) $val = (int)$val; + $row[$key] = $val; + } + + $boards['boards'][] = $row; + } + + // Dump resulting json on /test/ + if (BOARD_DIR === 'test') { + echo '
    '; + echo json_encode($boards, JSON_HEX_AMP | JSON_PRETTY_PRINT); + echo '
    '; + } + else { + $_json = json_encode($boards, JSON_HEX_AMP); + print_page(BOARDS_ROOT . 'boards.json', $_json); + } + + $time = round( microtime( true ) - $start, 6 ); + echo 'Done!

    Rebuilding took ' . $time . ' seconds.

    No redirect here boss. Off you go.'; + die(); +} + +function rebuild( $all = 0 ) +{ + global $rebuildall, $fwritetimer; + if( !has_level() ) die( '' ); // anti dos + + if (has_flag('developer')) { + error_reporting(E_ALL); + } + + header( "Pragma: no-cache" ); + _print(fancystyle()); + $l = $all ? 'all' : 'missing'; + + _print( "Rebuilding $l replies and pages... Go back

    \n" ); + log_cache(); + trim_db(); + trim_archive(); + mysql_board_lock( true ); + $starttime = microtime( true ); + $query = "SELECT no, resto FROM `" . SQLLOG . "` WHERE resto = 0 AND archived = 0 ORDER BY root DESC"; + $treeline = mysql_board_call($query); + if (!$treeline) { + echo S_SQLFAIL; + } + mysql_board_unlock(); + _print( "Writing...
    \n" ); + if( $all ) { + while( list( $no, $resto ) = mysql_fetch_row( $treeline ) ) { + if( !$resto ) { + _print( "Writing No.$no... " ); + updatelog( $no, 1 ); + $ext = TEST_BOARD ? "rp cache: ".realpath_cache_size() : ""; + _print( "DONE!
    $ext\n" ); + } + } + _print( "Writing index pages... " ); + updatelog(); + _print( "DONE!
    " ); + + if (ENABLE_CATALOG) { + _print( "Writing catalog..." ); + generate_catalog( true ); + _print( "DONE!
    " ); + } + + if (ENABLE_ARCHIVE) { + _print( "Writing archive..." ); + rebuild_archive_list(); + _print( "DONE!
    " ); + } + } + + $totaltime = microtime( true ) - $starttime; + $proctimer = $totaltime - $fwritetimer; + + $peakmem = memory_get_peak_usage(true) / (1024*1024.0); + $usedmem = memory_get_usage(true) / (1024*1024.0); + + $redir = '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . '/'; + +echo <<Total running time (lock excluded): $totaltime seconds. +
    Composed of: +
    Time spent writing files: $fwritetimer seconds. +
    Time spent processing: $proctimer seconds. +
    Memory: Peak: $peakmem MB Final: $usedmem MB +
    Pages created. +

    +END; +/* +if (!TEST_BOARD) { +echo << +END; +} +*/ +} + +function rebuild_after_deletion( $no ) +{ + if( !has_level() ) die(); + + mysql_board_lock( true ); + + if( !$treeline = mysql_board_call( "SELECT no FROM `" . SQLLOG . "` WHERE no = %d", $no ) ) { + mysql_board_unlock(); + die( S_POSTGONE ); + } + + log_cache( 0, $no ); + mysql_board_unlock(); + + updatelog( $no, 1 ); + + die( $no . ' Rebuilt OK!' ); +} + +function updating_index() +{ + $proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:"; + echo "" + . S_UPDATING_INDEX . "
    " + . S_UPDATING_INDEX . "
    "; +} + +function require_request_method( $method ) +{ + if (!isset($_SERVER['REMOTE_ADDR'])) return; + $umethod = $_SERVER["REQUEST_METHOD"]; + //$req = htmlspecialchars( $_REQUEST["mode"] ); + if( $umethod == "OPTIONS" || ( $umethod == "HEAD" && $method == "GET" ) ) return; + if( $umethod != $method ) { + error( S_REJECTTEXTBAN ); + } +} + +function get_catalog_info() { + $arr = array( + 'ws_board' => (int)(CATEGORY == 'ws'), + 'per_page' => (int)DEF_PAGES, + 'pages' => (int)PAGE_MAX, + 'max_filesize' => ((int)MAX_KB) * 1024, + 'max_webm_filesize' => ((int)MAX_WEBM_FILESIZE) * 1024, + 'max_comment_chars' => (int)MAX_COM_CHARS, + 'max_webm_duration' => (int)MAX_WEBM_DURATION, + 'bump_limit' => (int)MAX_RES, + 'image_limit' => (int)MAX_IMGRES, + 'cooldowns' => array( + 'threads' => (int)RENZOKU3, + 'replies' => (int)RENZOKU, + 'images' => (int)RENZOKU2 + ) + ); + + if (defined('META_DESCRIPTION')) { + $arr['meta_description'] = META_DESCRIPTION; + } + + if (SPOILERS) { + $arr['spoilers'] = 1; + if (SPOILER_NUM) { + $arr['custom_spoilers'] = (int)SPOILER_NUM; + } + } + + if (DISP_ID) { + $arr['user_ids'] = 1; + } + + if (ENABLE_ARCHIVE) { + $arr['is_archived'] = 1; + } + + if (CODE_TAGS) { + $arr['code_tags'] = 1; + } + + if (SJIS_TAGS) { + $arr['sjis_tags'] = 1; + } + + if (JSMATH) { + $arr['math_tags'] = 1; + } + + if (SHOW_COUNTRY_FLAGS) { + $arr['country_flags'] = 1; + } + + if (ENABLE_BOARD_FLAGS) { + $arr['board_flags'] = get_board_flags_selector(); + } + + if (ENABLE_WEBM_AUDIO) { + $arr['webm_audio'] = 1; + } + + if (MIN_W > 1) { + $arr['min_image_width'] = (int)MIN_W; + } + + if (MIN_H > 1) { + $arr['min_image_height'] = (int)MIN_H; + } + + if (TEXT_ONLY) { + $arr['text_only'] = 1; + $arr['require_subject'] = 1; + } + + if (FORCED_ANON) { + $arr['forced_anon'] = 1; + } + + if (REQUIRE_SUBJECT) { + $arr['require_subject'] = 1; + } + + if (ENABLE_PAINTERJS) { + $arr['oekaki'] = 1; + } + + die(json_encode($arr)); +} + +/** + * Generates context to append to thread links. + */ +function generate_href_context($sub, $com) { + if (JANITOR_BOARD) { + return ''; + } + + $context = ''; + + if (strpos($sub, 'SPOILER<>') === 0) { + $sub = substr($sub, 9); + } + + if ($sub !== '') { + $context = cleanup_context_string($sub); + } + + if ($context === '' && $com !== '') { + $context = $com; + + if (strpos($context, '
    ') !== false) { + $context = str_replace('
    ', "\n", $context); + $has_br = true; + } + else { + $has_br = false; + } + + $context = preg_replace('/(^|\s)https?:\/\/[^\s]{4,}/', '', $context); + + if (strpos($context, '') !== false) { + $context = preg_replace('/.*<\/table>/', '', $context); // ??? + } + if (strpos($context, ']+>.*<\/strong>/', '', $context); // ??? + } + + if ($has_br) { + $context = ltrim($context); + $context = explode("\n", $context)[0]; + } + + $context = preg_replace('/<[^>]+>/', ' ', $context); + $context = cleanup_context_string($context); + } + + + return $context; +} + +/** + * Generates page title from subjects and comments + * params must be already html-escaped. + */ +function generate_page_title($thread_id, $sub, $com) { + if (JANITOR_BOARD) { + return strip_tags(TITLE); + } + + if (UPLOAD_BOARD) { + $sub = preg_replace('/^(\d+)\|/', '', $sub); + } + + $context = ''; + + if (strpos($sub, 'SPOILER<>') === 0) { + $sub = substr($sub, 9); + } + + if ($sub !== '') { + $context = $sub; + } + + if ($context === '' && $com !== '') { + if (SJIS_TAGS && strpos($com, '/', '[SJIS]', $com); + } + $context = str_replace('
    ', ' ', $com); + $context = htmlspecialchars_decode($context, ENT_QUOTES); + $context = mb_substr(strip_tags($context), 0, 50); + $context = htmlspecialchars($context, ENT_QUOTES); + } + + if ($context === '') { + $context = 'No.' . $thread_id; + } + + if (BOARD_DIR === 's4s') { + return '[' . BOARD_DIR . '] - ' . $context; + } + else { + return '/' . BOARD_DIR . '/ - ' . $context; + } +} + +/** + * Generates metatags from subjects and comments + * params must be already html-escaped. + */ +function generate_page_metatags($sub, $com) { + if (JANITOR_BOARD) { + return null; + } + + $context = ''; + + if (strpos($sub, 'SPOILER<>') === 0) { + $sub = substr($sub, 9); + } + + if ($sub !== '') { + $context = $sub; + } + + $ell = ''; + + if ($context === '' && $com !== '') { + $context = preg_replace('/(
    |\s)+/', ' ', $com); + $context = htmlspecialchars_decode(strip_tags($context), ENT_QUOTES); + + if (mb_strlen($context) > 100) { + $ell = '...'; + $context = mb_substr($context, 0, 100); + } + } + else { + $context = htmlspecialchars_decode($context, ENT_QUOTES); + } + + if (empty($context)) { + return null; + } + else { + $words = preg_split('/[[:punct:]\s]+/', $context); + $keywords = ''; + foreach ($words as $word) { + if (strlen($word) > 3) { + $keywords .= ',' . $word; + } + } + } + + $context = htmlspecialchars($context, ENT_QUOTES); + $keywords = htmlspecialchars($keywords, ENT_QUOTES); + + return array($context.$ell, $keywords); +} + +function cleanup_context_string($context) { + $context = htmlspecialchars_decode($context, ENT_QUOTES); + + $context = strtolower(preg_replace('/[^a-zA-Z0-9\s]+/', '', $context)); + + $length = 0; + + $words = explode(' ', $context); + + $context = array(); + + foreach ($words as $word) { + if ($word === '') { + continue; + } + + $length += strlen($word) + 1; + + if ($length > 50) { + break; + } + + $context[] = $word; + } + + $context = implode('-', $context); + + return htmlspecialchars($context, ENT_QUOTES); +} + +/** + * Embedded data detection + * Dies if the PNG or JPG file contains embedded data. + * Returns true if the GIF file was modified, otherwise returns false. + */ +function cleanup_uploaded_file($file, $type) { + $full_size = filesize($file); + + // 50KB + $max_delta = 51200; + + switch ($type) { + case '.png': + $clean_size = get_clean_png_size($file); + break; + case '.jpg': + if ($full_size < 204800) { + return false; + } + $clean_size = get_clean_jpg_size($file); + break; + case '.gif': + if ($full_size < 204800) { + return false; + } + $clean_size = get_clean_gif_size($file); + break; + case '.swf': + return true; // Why? + default: + return false; + } + + if ($clean_size === false) { + return false; + } + + // PNGs can still fail the check even if no size delta is found + if ($clean_size === -1) { + if ($type === '.gif') { + $file = escapeshellcmd($file); + $res = system("/usr/local/bin/gifsicle --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1"); + if ($res !== false) { + return true; + } + else { + return false; + } + } + else { + error(S_IMGCONTAINSFILE, $file); + } + } + + $delta_size = $full_size - $clean_size; + + if ($delta_size > $max_delta) { + if ($type === '.gif') { + $file = escapeshellcmd($file); + $res = system("/usr/local/bin/gifsicle --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1"); + if ($res !== false) { + return true; + } + } + else { + error(S_IMGCONTAINSFILE, $file); + } + } + + return false; +} + +// Returns the size of critical data or -1 if the file contains extensions. +function get_clean_gif_size($file) { + $file = escapeshellcmd($file); + + $binary = '/usr/local/bin/gifsicle'; + + $res = shell_exec("$binary --sinfo \"$file\" 2>&1"); + + if ($res !== null) { + $size = 0; + + if (preg_match('/ extensions [0-9]+/', $res)) { + return -1; + } + + if (preg_match_all('/compressed size ([0-9]+)/', $res, $m)) { + foreach ($m[1] as $frame_size) { + $size += (int)$frame_size; + } + + return $size; + } + } + + return false; +} + +// Returns the number of bytes in critical chunks or -1 if too many IDAT chunks are found +// Returns false on error +function get_clean_png_size($file) { + $file = escapeshellcmd($file); + + $binary = '/usr/local/bin/pngcrush'; + + $res = shell_exec("$binary -m 1 -n -v \"$file\" 2>&1 1>/dev/null"); + + if ($res !== null) { + if (preg_match('/Reading (?:iTXt|tEXt|zTXt) chunk,/', $res, $m)) { + if (preg_match('/ [a-z]oo: /i', $res)) { + return -1; + } + else if (preg_match('/ (?:Software|Creation Time): ([=a-zA-Z0-9]{10,})\n/', $res, $ct)) { + $_b64 = base64_decode($ct[0]); + if ($_b64 && preg_match('/[0-9]/', $_b64)) { + write_to_event_log('png_link', $_SERVER['REMOTE_ADDR'], [ + 'board' => BOARD_DIR, + 'meta' => htmlspecialchars($res) + ]); + return -1; + } + } + } + + if (preg_match('/in critical chunks\s+=\s+([0-9]+)/', $res, $m)) { + return (int)$m[1]; + } + } + + return false; +} + +function get_clean_jpg_size($file) { + $eof = false; + + $img = fopen($file, 'rb'); + + $data = fread($img, 2); + + if ($data !== "\xff\xd8") { + fclose($img); + return false; + } + + while (!feof($img)) { + $data = fread($img, 1); + + if ($data !== "\xff") { + continue; + } + + while (!feof($img)) { + $data = fread($img, 1); + + if ($data !== "\xff") { + break; + } + } + + if (feof($img)) { + break; + } + + $byte = unpack('C', $data)[1]; + + if ($byte === 217) { + $eof = ftell($img); + break; + } + + if ($byte === 0 || $byte === 1 || ($byte >= 208 && $byte <= 216)) { + continue; + } + + $data = fread($img, 2); + + $length = unpack('n', $data)[1]; + + if ($length < 1) { + break; + } + + fseek($img, $length - 2, SEEK_CUR); + } + + fclose($img); + + return $eof; +} + +/** + * Generates a "Pass User Since YEAR" string for pass users + */ +function get_since_4chan($pass_id) { + $query = "SELECT UNIX_TIMESTAMP(purchase_date) FROM pass_users WHERE user_hash = '%s' ORDER BY purchase_date ASC LIMIT 1"; + + $res = mysql_global_call($query, $pass_id); + + if (!$res) { + return 0; + } + + $row = mysql_fetch_row($res); + + $ts = (int)$row[0]; + + if (!$ts) { + return 0; + } + + $ts = date('Y', $ts); + + if (!$ts) { + return 0; + } + + return (int)$ts; +} +/* +// Halloween 2017 +function get_halloween_score($pass_id) { + $query = "SELECT score FROM halloween_tricks WHERE user_hash = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $pass_id); + + if (!$res) { + return 0; + } + + $row = mysql_fetch_row($res); + + if (!$row) { + return 0; + } + + return (int)$row[0]; +} + +// Halloween 2017 +/* +function get_halloween_dummy_pass($name) { + if (!$name) { + return ''; + } + + $hashed_bits = hash_hmac('sha1', $name, 'CIaPCUkJq9n3fskmKC06tquCV/2edWqbgBeY9pk7RlQ', true); + + $hashed_name = base64_encode($hashed_bits); + + if (!$hashed_name) { + return ''; + } + + return '_' . substr($hashed_name, 0, 9); +} + +// Halloween 2017 +function process_halloween_score($com, $thread_id, $this_pass_id, $this_pwd, $pass_is_bannable) { + if ($com === '') { + return; + } + + $thread_id = (int)$thread_id; + + if (!$thread_id) { + return; + } + + if (preg_match_all('/>>([0-9]{4,})/', $com, $m) === 1) { + $post_id = (int)$m[1][0]; + + if (!$post_id || $post_id == $thread_id) { + return; + } + + $query = 'SELECT host, pwd, 4pass_id FROM `' . SQLLOG . '` WHERE no = ' . $post_id . ' AND resto = ' . $thread_id; + + $res = mysql_board_call($query); + + if (!$res) { + return; + } + + $row = mysql_fetch_assoc($res); + + if (!$row) { + return; + } + + // Check if same person + if (!$row['4pass_id'] || $row['4pass_id'] == $this_pass_id || $row['pwd'] == $this_pwd || $row['host'] == $_SERVER['REMOTE_ADDR']) { + return; + } + + $long_ip = ip2long($_SERVER['REMOTE_ADDR']); + + if (!$long_ip) { + return; + } + + // Check if already gave points + $query = "SELECT 1 FROM `halloween_votes` WHERE long_ip = $long_ip AND board = '" . BOARD_DIR . "' AND post_id = $post_id"; + + $res = mysql_global_call($query); + + if (!$res) { + return; + } + + if (mysql_num_rows($res) > 0) { + return; + } + + // Check if the user is known + if (!spam_filter_is_user_known($long_ip, BOARD_DIR, $pass_is_bannable ? $this_pwd : null)) { + return; + } + + // Good to go + $query = <<= 1000) { + return " n-jol-6"; + } + if ($trick_count >= 500) { + return " n-jol-5"; + } + if ($trick_count >= 200) { + return " n-jol-4"; + } + if ($trick_count >= 100) { + return " n-jol-3"; + } + if ($trick_count >= 50) { + return " n-jol-2"; + } + if ($trick_count >= 25) { + return " n-jol-1"; + } + return ''; +} +*/ + +/** + * Checks if the user has a recent ban request. + * dies with an error if user needs to be blocked. + */ +function check_for_ban_request($ip, $pwd = null) { + $time_lim = BLOCK_ON_BR_LEN; + + $clauses = []; + + $clauses[] = "host = '" . mysql_real_escape_string($ip) . "'"; + + if ($pwd) { + $clauses[] = "pwd = '" . mysql_real_escape_string($pwd) . "'"; + } + + $board_sql = mysql_real_escape_string(BOARD_DIR); + + $clauses = implode(' OR ', $clauses); + + $query = << DATE_SUB(NOW(), $time_lim) OR ban_template IN (1, 2, 123, 126)) +LIMIT 1 +SQL; + + $res = mysql_global_call($query); + + if (!$res || mysql_num_rows($res) !== 1) { + return false; + } + + $row = mysql_fetch_assoc($res); + + $time = (int)$row['diff']; + + $tpl_name = rtrim(preg_replace('/\[[^\]]+\]/', '', $row['tpl_name'])); + + // Non-expiring blocks for some global templates + if ($time < 0) { + error(sprintf(S_BRBLOCKED_2, $tpl_name)); + } + + $str = $time <= 1 ? '1 minute' : "$time minutes"; + + error(sprintf(S_BRBLOCKED, $tpl_name, $str)); +} + +/** + * Checks if the IP is banned, also checks for ban evasion + * return 0 for not banned, 1 for banned, 2 for warned + * If the ban has expired, delete the banned thumbnail and de-activate the ban + */ +function check_for_ban($ip, $fields = array(), $thread_id = 0, $user_verified = false) { + global $captcha_bypass; + + // Skip IP bans when the user has a valid 4chan Pass + $skip_ip_bans = isset($fields['4pass_id']) && $captcha_bypass; + + if (!$skip_ip_bans) { + $fields['host'] = $ip; + } + + $expired = []; + + $is_banned = 0; + + foreach ($fields as $key => $value) { +$query =<< ['pipe', 'w'], 2 => ['pipe', 'w'] ]; + + $pipes = []; + + $proc = proc_open($cmd, $desc, $pipes); + + if ($proc === false || !is_resource($proc)) { + error(S_FAILEDUPLOAD, $file); + } + + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + $status = proc_close($proc); + + if ($status !== 0) { + error(S_FAILEDUPLOAD, $file); + } + + // Check stderr + if (stripos($stderr, 'invalid') !== false) { + error(S_NOREC, $file); + } + + // Check stdout + $res = json_decode($stdout, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + error(S_FAILEDUPLOAD, $file); + } + + //print_r($res); + + if ($res['format']['format_name'] === 'matroska,webm') { + $format = 'webm'; + } + else if ($res['format']['format_name'] === 'mov,mp4,m4a,3gp,3g2,mj2') { + $format = 'mp4'; + } + else { + error(S_NOREC, $file); + } + + // container duration, can be forged but we remux the file to fix this + $duration = (float)$res['format']['duration']; + + if ($duration <= 0 || $duration > MAX_WEBM_DURATION) { + error(S_VIDEOTOOLONG, $file); // Duration too long + } + + $has_audio = false; + $video_dims = false; + + foreach ($res['streams'] as $stream) { + $type = $stream['codec_type']; + + if ($type === 'audio') { + if (!ENABLE_WEBM_AUDIO) { + error(S_AUDIODISABLED, $file); // Audio streams are not allowed + } + + // Vorbis or Opus for webm audio + if ($format === 'webm') { + if ($stream['codec_name'] !== 'vorbis' && $stream['codec_name'] !== 'opus') { + error(S_BADAUDIO, $file); // Bad audio stream + } + } + // AAC for mp4 audio + else if ($format === 'mp4') { + if ($stream['codec_name'] !== 'aac') { + error(S_BADAUDIO, $file); // Bad audio stream + } + } + else { + error(S_BADAUDIO, $file); // Bad audio stream + } + + $has_audio = true; + } + else if ($type === 'video') { + if ($video_dims) { + error(S_BADSTREAM, $file); // Too many video streams + } + + // VP8 or VP9 for webm video + if ($format === 'webm') { + if ($stream['codec_name'] !== 'vp8' && $stream['codec_name'] !== 'vp9') { + error(S_BADVIDEO, $file); // Bad video stream + } + } + // H264 for mp4 video + else if ($format === 'mp4') { + if ($stream['codec_name'] !== 'h264') { + error(S_BADVIDEO, $file); // Bad video stream + } + + // Reject 10 bit streams + if ($stream['bits_per_raw_sample'] > 8) { + error(S_NOREC, $file); + } + + // Only accept yuv420p streams + if ($stream['pix_fmt'] !== 'yuv420p') { + error(S_NOREC, $file); + } + } + else { + error(S_BADVIDEO, $file); // Bad video stream + } + + $width = (int)$stream['width']; + $height = (int)$stream['height']; + + if (!$width || !$height || $width > MAX_WEBM_DIMENSION || $height > MAX_WEBM_DIMENSION) { + error(S_TOOLARGERES, $file); // Dimensions too big + } + + $sar = null; + + if (isset($stream['sample_aspect_ratio'])) { + $tmp_sar = explode(':', $stream['sample_aspect_ratio']); + + $tmp_sar[0] = (int)$tmp_sar[0]; + $tmp_sar[1] = (int)$tmp_sar[1]; + + if ($tmp_sar[1] && $tmp_sar[0] !== $tmp_sar[1]) { + $tmp_sar = $tmp_sar[0] / $tmp_sar[1]; + + if ($tmp_sar < 2 && $tmp_sar > 0.5) { + $sar = $tmp_sar; + } + } + } + + $video_dims = array($width, $height, $sar); + } + else { + error(S_BADSTREAM, $file); // Bad stream + } + } + + if (!$video_dims) { + error(S_NOVIDEOSTREAM, $file); // No video streams + } + + return $video_dims; +} + +/** + * Generates thumbnails for webm files + * Returns the thumbnail filename on success. + * Returns false if the thumbnail couldn't be generated. + */ +function thumb_webm($file, $ext) { + $binary = '/usr/local/bin/ffmpeg-mp4'; + + $out_file = $file . '.tmp.jpg'; + + if ($ext === '.webm') { + $format = 'webm'; + } + else if ($ext === '.mp4') { + $format = 'mp4'; + } + else { + return false; + } + + // $file and $format must be safe for shell_exec + $res = shell_exec("$binary -f $format -i \"$file\" -vframes 1 -an -y \"$out_file\" 2>&1"); + + if (file_exists($out_file)) { + return $out_file; + } + + quick_log_to( "/www/perhost/bad-upload.log", "webm failure on $file:\n$res"); + + return false; +} + +/** + * Contest banners 468x60 + */ +function get_contest_banner() { + $query = "SELECT file_id, file_ext, board FROM contest_banners WHERE is_live = 1 ORDER BY RAND() LIMIT 1"; + + $res = mysql_global_call($query); + + if (!$res) { + return ''; + } + + $banner = mysql_fetch_assoc($res); + + if (!$banner) { + return ''; + } + + $img_url = STATIC_SERVER . "image/contest_banners/{$banner['file_id']}.{$banner['file_ext']}"; + $link_url = '//boards.' . L::d($banner['board']) . '/' . $banner['board'] . '/'; + + return '
    '; +} + +/** + * Get latest post number from /j/ + * Returns a json { "no": 123 } + */ + +function get_last_post_no() { + $no = 0; + $query = "SELECT no FROM `j` ORDER BY no DESC LIMIT 1"; + $res = mysql_board_call($query); + if ($res) { + if ($row = mysql_fetch_row($res)) { + $no = (int)$row[0]; + } + } + echo "{\"no\":$no}"; +} + +/** + * Deletes partial jsons for all live threads + */ +function purge_json_tails() { + $query = 'SELECT no FROM `' . SQLLOG . '` WHERE resto = 0 AND archived = 0'; + + $res = mysql_board_call($query); + + if (!$res) { + return false; + } + + while ($row = mysql_fetch_row($res)) { + $thread_id = (int)$row[0]; + update_json_tail_deletion($thread_id, true); + } + + return true; +} + +/** + * Deletes, if necessary, the partial json tail file after post deletion. + */ +function update_json_tail_deletion($thread_id, $force = false) { + if (!JSON_TAIL_SIZE && !$force) { + return false; + } + + $tail_size = $force ? 0 : get_json_tail_size($thread_id); + + if (!$tail_size) { + $fname = RES_DIR . $thread_id . '-tail.json'; + + if (USE_GZIP) { + $fname = "$fname.gz"; + } + + if (file_exists($fname)) { + return unlink($fname); + } + } + + return false; +} + +/** + * Returns the number of posts in the partial -tail json. + * 0 if no tail json is available + */ +function get_json_tail_size($thread_id) { + global $log; + + $tail_size = (int)JSON_TAIL_SIZE; + + if (!$tail_size || !isset($log[$thread_id])) { + return 0; + } + + $th = $log[$thread_id]; + + if ($th['sticky'] && $th['undead']) { + $tail_size = $tail_size * 2; + } + + $post_count = count($th['children']); + + if ($post_count >= $tail_size * 2) { + return $tail_size; + } + else { + return 0; + } +} + +/** + * Test function for mobile image resizing + */ +function resize_mobile_image($path, $w, $h, $fsize, $tim, $ext) { + if ($ext !== '.jpg' && $ext !== '.png') { + return false; + } + + $MAX_W = 1024; + $MAX_H = 1024; + $MAX_PXL = 524288; + $MAX_PNG_BYTES = 524288; + + if ($ext === '.png' && $fsize <= $MAX_PNG_BYTES) { + return; + } + + if (($w > $MAX_W || $h > $MAX_H) && $w * $h > $MAX_PXL) { + $jpeg_quality = 80; + + $memory_limit_increased = false; + + if ($w * $h > 3000000) { + $memory_limit_increased = true; + ini_set('memory_limit', memory_get_usage() + $w * $h * 15); + } + + if ($ext === '.jpg') { + $img_in = ImageCreateFromJPEG($path); + } + else { + $img_in = ImageCreateFromPNG($path); + } + + if (!$img_in) { + error(S_FAILEDUPLOAD . ' (rmi)', $path); + } + + $ratio = $w / $h; + + if ($ratio > 1) { + $out_w = $MAX_W; + $out_h = round($MAX_W / $ratio); + } + else { + $out_w = round($MAX_H * $ratio); + $out_h = $MAX_H; + } + + $img_out = ImageCreateTrueColor($out_w, $out_h); + + ImageCopyResampled($img_out, $img_in, 0, 0, 0, 0, $out_w, $out_h, $w, $h); + ImageDestroy($img_in); + + $out_path = IMG_DIR . $tim . 'm.jpg'; + ImageJPEG($img_out, $out_path, $jpeg_quality); + ImageDestroy($img_out); + + if ($memory_limit_increased) { + ini_restore('memory_limit'); + } + + return $out_path; + } +} + +/** + * Returns the number of unique IPs for a given thread id + * $thread_id needs to be cached in $log. + */ +function get_unique_ip_count($thread_id) { + global $log; + + if (!isset($log[$thread_id]) || $log[$thread_id]['archived']) { + return false; + } + + $posts = $log[$thread_id]['children']; + + if (empty($posts)) { + return 1; + } + + $ip_map = array(); + $ip_count = 1; + $ip_map[$log[$thread_id]['host']] = true; + + foreach ($posts as $pid => $val) { + if (!isset($log[$pid])) { + continue; + } + $post = $log[$pid]; + if (!isset($ip_map[$post['host']])) { + ++$ip_count; + $ip_map[$post['host']] = true; + } + } + + return $ip_count; +} + +function generate_del_pwd() { + return '_' . substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 32); +} + +function get_hashed_mod_name($name) { + if (!$name) { + die('Internal Server Error (ghmn0)'); + } + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (ghmn1)'); + } + + $hashed_bits = hash_hmac('sha256', $name, $admin_salt, true); + + $hashed_name = base64_encode($hashed_bits); + + if (!$hashed_name) { + die('Internal Server Error (ghmn2)'); + } + + return $hashed_name; +} + +function create_memcached_instance() { + $m = new Memcached(); + //$m->setOption(Memcached::OPT_TCP_NODELAY, true); + $m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); + $m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms + $m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms + $m->addServer(MEMCACHED_HOST, MEMCACHED_PORT); + return $m; +} + +function forcearchive() { + if (!has_level()) { + error("Can't let you do that."); + } + + if (!ENABLE_ARCHIVE) { + error('Archives are disabled on this board.'); + } + + if (!isset($_POST['id'])) { + error('Bad Request.'); + } + + $tid = (int)$_POST['id']; + + $query = 'SELECT resto, sticky, archived, no, name, sub, com, filename, ext FROM `%s` WHERE no = %d'; + $res = mysql_board_call($query, BOARD_DIR, $tid); + + if (!$res) { + error('Database error.'); + } + + $thread = mysql_fetch_assoc($res); + + if (!$thread || $thread['resto']) { + error('Thread not found.'); + } + + if ($thread['archived']) { + error('This thread is already archived.'); + } + + if ($thread['sticky']) { + error(S_MAYNOTDELSTICKY); + } + + archive_thread($tid); + + // Log the action + $action_log_post = array( + 'no' => $thread['no'], + 'name' => $thread['name'], + 'sub' => $thread['sub'], + 'com' => $thread['com'], + 'filename' => $thread['filename'], + 'ext' => $thread['ext'] + ); + + log_mod_action(3, $action_log_post); + + if (ENABLE_JSON_THREADS) { + generate_board_archived_json(); + } + + updating_index(); +} + +// Called remotely by other tools +function rebuild_threads_by_id() { + header('Content-Type: text/plain'); + + if (!isset($_POST['ids']) || !is_array($_POST['ids']) || empty($_POST['ids'])) { + echo '0'; + return; + } + + $live_ids = array(); + + // Rebuild archived threads first + foreach ($_POST['ids'] as $id) { + $id = (int)$id; + + if (!$id) { + continue; + } + + $query = "SELECT archived FROM `" . SQLLOG . "` WHERE no = $id LIMIT 1"; + $res = mysql_board_call($query); + + if (!$res) { + echo '0'; + return; + } + + if (mysql_fetch_row($res)[0] === '1') { + rebuild_archived_thread($id); + } + else { + $live_ids[] = $id; + } + } + + // Rebuild live threads + if (!empty($live_ids)) { + + foreach ($live_ids as $id) { + updatelog($id, 1); + } + + if (STATIC_REBUILD) { + return; + } + + updatelog(0, 0); // rebuild indexes + } + + echo '1'; +} + +function rebuild_archive_list($print = false) { + $board = BOARD_DIR; + + $html = ''; + + $maxlen = 100; + + $max_age_in_days = 3; + + $hour_clause = $max_age_in_days * 24; + + $thread_limit = 3000; + + $query = <<= DATE_SUB(NOW(), INTERVAL $hour_clause HOUR) +ORDER BY root DESC +LIMIT $thread_limit +SQL; + + $res = mysql_board_call($query); + + $thread_count = mysql_num_rows($res); + + head($html, 0, 0, 0, 0, true); + + $html .= ''; + + $html .= '
    +
    '; + + $html .= '

    Displaying ' . number_format($thread_count) . ' expired thread' + . (!$thread_count || $thread_count > 1 ? 's' : '') . ' from the past ' . $max_age_in_days . ' day' + . ($max_age_in_days > 1 ? 's' : '') . '

    + + + + + '; + + while ($row = mysql_fetch_assoc($res)) { + if (strpos($row['sub'], 'SPOILER<>') === 0) { + $row['sub'] = substr($row['sub'], 9); + } + + if (!empty($row['sub'])) { + if ($row['com'] !== '') { + $teaser = '' . $row['sub'] . ': ' . $row['com']; + } + else { + $teaser = $row['sub']; + } + } + else { + $teaser = $row['com']; + } + + $teaser = preg_replace('/(?:
    )+/', ' ', str_replace('"', "'", $teaser)); + + $href_context = generate_href_context($row['sub'], $row['com']); + + if ($href_context !== '') { + $href_context = "/$href_context"; + } + + $html .= ' + + + +'; + } + + $html .= '
    No.Excerpt
    ' . $row['no'] . '' + . truncate_comment($teaser, $maxlen) . +'[View] +

    '; + + $html .= '
    '; + + $html .= '
    '; + + if (AD_BOTTOM_ENABLE == 1) { + $bottomad = ''; + + if (defined('AD_BOTTOM_TEXT') && AD_BOTTOM_TEXT) { + $bottomad .= '
    ' + . ad_text_for(AD_BOTTOM_TEXT) . '
    ' + . (defined('AD_BOTTOM_PLEA') ? AD_BOTTOM_PLEA : ''); + } + + if ($bottomad) { + $html .= "$bottomad
    "; + } + } + + $html .= '
    '; + + if (!defined('CSS_FORCE')) { + $html .= 'Style: + + '; + } + + $html .= '
    '; + + foot($html, false, true); + + if ($print) { + echo $html; + } + else { + print_page(INDEX_DIR . 'archive'. PHP_EXT, $html); + } +} + +function rebuild_syncframe_page($print = false) { + $tpl_file = YOTSUBA_DIR . 'views/syncframe.html'; + + if (!file_exists($tpl_file)) { + die('Template file not found'); + } + + $html = file_get_contents($tpl_file); + + if ($print) { + die($html); + } + else { + print_page(BOARDS_ROOT . 'syncframe' . PHP_EXT, $html); + } +} + +function rebuild_search_page($print = false) { + $html = ''; + + // board select box + + $board_select_html = ''; + + // header --- + + $cssVersion = $print ? CSS_VERSION_TEST : CSS_VERSION; + $defaultcss = 'yotsubanew'; + $mobilecss = 'yotsubamobile.' . $cssVersion . '.css'; + + $styles = array( + 'Yotsuba New' => "yotsubanew.$cssVersion.css", + 'Yotsuba B New' => "yotsubluenew.$cssVersion.css", + 'Futaba New' => "futabanew.$cssVersion.css", + 'Burichan New' => "burichannew.$cssVersion.css", + 'Photon' => "photon.$cssVersion.css", + 'Tomorrow' => "tomorrow.$cssVersion.css" + ); + + $dcssl = $defaultcss . '.' . $cssVersion . '.css'; + + $css = ''; + + foreach ($styles as $style => $stylecss) { + $css .= ''; + } + + $css .= ''; + + $scriptjs = ''; + + $testjs = $print ? 'test/core-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'core.min.' . JS_VERSION_CORE . '.js'; + $testextra = $print ? 'test/extension-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'extension.min.' . JS_VERSION_EXT . '.js'; + + $scriptjs .= ''; + $scriptjs .= ''; + + if (defined('FAVICON')) { + $favicon = ''; + } + else { + $favicon = ''; + } + + $includenav = file_get_contents_cached(NAV_TXT); + + $html .= ' + + + + + + + +' . $favicon . ' +' . $css . ' +Search 4chan' . $scriptjs . +' +' . $stylejs . $includenav . +'
    +
    4chan Search
    +
    '; + + // --- + + $html .= '
    ' . $board_select_html . '
    '; + + $html .= '
    +
    +
    '; + + $html .= '

    '; + + $html .= '
    '; + + if (!defined('CSS_FORCE')) { + $html .= 'Style: + + '; + } + + $html .= '
    '; + + foot($html); + + if ($print) { + echo $html; + } + else { + print_page(BOARDS_ROOT . 'globalsearch' . PHP_EXT, $html); + } +} + +/** + * Enforce the maximum number of allowed threads per user, per board. + * error() if limit has been reached + */ +function validate_user_thread_limit($ip, $password = null, $pass_id = null) { + $clauses = array(); + + $clauses[] = "host = '" . mysql_real_escape_string($ip) . "'"; + + if ($password) { + $clauses[] = "pwd = '" . mysql_real_escape_string($password) . "'"; + } + + if ($pass_id) { + $clauses[] = "4pass_id = '" . mysql_real_escape_string($pass_id) . "'"; + } + + $ts = $_SERVER['REQUEST_TIME'] - ((int)MAX_USER_THREADS_PERIOD * 3600); + + $clauses = implode(' OR ', $clauses); + + $query = 'SELECT COUNT(*) FROM `' . SQLLOG + . "` WHERE resto = 0 AND archived = 0 AND time > $ts AND ($clauses)"; + + $res = mysql_board_call($query); + + if (!$res) { + return true; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count >= (int)MAX_USER_THREADS) { + $plural = MAX_USER_THREADS > 1 ? 's' : ''; + error(sprintf(S_TOOMANYTHREADS, MAX_USER_THREADS, $plural)); + } + + return true; +} + +function is_poster_op($host, $hashed_pwd, $resto) { + $query = 'SELECT host, pwd FROM `%s` WHERE no = %d'; + $res = mysql_board_call($query, SQLLOG, $resto); + + if (!$res) { + return false; + } + + $post = mysql_fetch_assoc($res); + + if (!$post) { + return false; + } + + return $post['host'] === $host || $post['pwd'] === $hashed_pwd; +} + +function spam_filter_check_qa_bot($board, $resto, $ip, $country, $com, $captcha_resp) { + if (preg_match('/Edge|Safari|WebKit|Firefox|Mozilla/', $_SERVER['HTTP_USER_AGENT']) && $captcha_resp['hostname'] === 'boards.4chan.org') { + return true; + } + + // Check if IP is known + $long_ip = ip2long($ip); + + if (spam_filter_is_user_known($long_ip)) { + return false; + } + + if (!preg_match('/Edge|Mobile/', $_SERVER['HTTP_USER_AGENT']) && preg_match('/WebKit/', $_SERVER['HTTP_USER_AGENT']) !== preg_match('/WebKit/', $_SERVER['HTTP_CONTENT_TYPE'])) { + return true; + } + + $bot_countries = array( + 'AD','AE','AF','AG','AI','AL','AM','AN','AO','AR','AS','AW','AZ', + 'BB','BD','BF','BG','BH','BI','BJ','BM','BN','BO','BR','BS','BT','BV','BW','BY','BZ', + 'CC','CF','CG','CH','CI','CK','CL','CM','CN','CO','CR','CU','CV','CX','CY','CZ', + 'DJ','DM','DO','DZ','EC','EE','EG','EH','ER','ET','FJ','FM','FO', + 'GA','GD','GE','GF','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GY', + 'HK','HM','HN','HT','HU','HR','ID','IL','IN','IO','IQ','IR','IS','JM','JO', + 'KE','KG','KH','KI','KM','KN','KR','KW','KY','KZ', + 'LA','LB','LC','LI','LK','LR','LS','LU','LY', + 'MA','MD','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MY','MZ','NA', + 'NE','NF','NG','NI','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PM','PN','PR','PS','PT','PW', + 'QA','RE','RS','RO','RU','RW','SA','SB','SC','SD','SH','SI','SJ','SK','SL','SM','SN','SO','SR','ST','SV','SY','SZ', + 'TC','TD','TF','TG','TJ','TM','TN','TO','TP','TR','TT','TV','TW','TZ','UG','UM','UY','UZ', + 'VA','VE','VC','VG','VI','VN','VU','WF','WS','YE','YT','ZA','ZM','ZR','ZW' + ); + + // Check country + if (!in_array($country, $bot_countries)) { + return false; + } + + if ($resto) { + $query = 'SELECT time FROM %s WHERE resto = %d ORDER BY no DESC LIMIT 1'; + + $res = mysql_board_call($query, $board, $resto); + + if (!$res) { + return false; + } + + $last_time = mysql_fetch_row($res); + + if (!$last_time) { + return false; + } + + $last_time = (int)$last_time[0]; + + if ($_SERVER['REQUEST_TIME'] - $last_time < 14400) { + return false; + } + } + + return true; +} + +function log_qa_spam_filter($is_hit, $thread_id, $ip, $country, $captcha_resp) { + $_bot_headers = ''; + + foreach ($_SERVER as $_h_name => $_h_val) { + if (substr($_h_name, 0, 5) == 'HTTP_') { + $_bot_headers .= "$_h_name: $_h_val\n"; + } + } + + $_bot_headers .= "_Captcha: " . $captcha_resp['hostname'] . "\n"; + + $_bot_headers .= "_Country: $country\n"; + + log_spam_filter_trigger($is_hit ? 'blocked_qa' : 'ok_qa', BOARD_DIR, $thread_id, $ip, 1, $_bot_headers); +} + +function log_spam_filter_trigger($action, $board, $thread_id, $ip, $arg_num, $meta = '') { + $query = << 'error', 'message' => 'Nothing to do.'); + echo json_encode($data); + return; + } + + $html = purify_html($html); + + $data = array('status' => 'success', 'data' => $html); + + echo json_encode($data); +} + +function purify_html($html) { + static $purifier = null; + + if ($purifier === null) { + require_once 'lib/htmlpurifier/HTMLPurifier.standalone.php'; + + $config = HTMLPurifier_Config::createDefault(); + $config->set('Cache.DefinitionImpl', null); + + $config->set('HTML.Doctype', 'HTML 4.01 Transitional'); + + $config->set('URI.AllowedSchemes', array('http' => true, 'https' => true)); + $config->set('HTML.SafeIframe', true); + $config->set('URI.SafeIframeRegexp', HTML_IFRAME_WHITELIST); + $config->set('HTML.Allowed', HTML_WHITELIST); + $config->set('CSS.AllowTricky', true); + $config->set('CSS.Trusted', true); + $config->set('Attr.AllowedFrameTargets', array('_blank')); + + $def = $config->getHTMLDefinition(true); + $def->addAttribute('iframe', 'allowfullscreen', 'Bool'); + + $def->addElement('video', 'Block', 'Flow', 'Common', array( + 'controls' => 'Bool', + 'height' => 'Length', + 'width' => 'Length', + 'poster' => 'URI', + 'autoplay' => 'Bool', + 'loop' => 'Bool', + 'muted' => 'Bool', + 'src' => 'URI' + )); + + $css_def = $config->getDefinition('CSS'); + + $css_def->info['color'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('transparent')), + new HTMLPurifier_AttrDef_CSS_Color() + ) + ); + + $purifier = new HTMLPurifier($config); + + if (!$purifier) { + error('Internal Server Error'); + } + } + + return $purifier->purify($html); +} + +function count_thread_replies($board, $thread_id) { + $thread_id = (int)$thread_id; + + if ($thread_id <= 0) { + return 0; + } + + $sql = "SELECT COUNT(*) as cnt FROM `%s` WHERE resto = $thread_id"; + + $res = mysql_board_call($sql, $board); + + if (!$res) { + return 0; + } + + return (int)mysql_fetch_row($res)[0]; +} + +// TODO: remove later +function check_safe_ua_sig($ua, $sig) { + if (!$ua || !$sig) { + return true; + } + + $thres = 3; + + $ua_sig = "$ua.$sig"; + + $sql = "SELECT 1 FROM event_log WHERE type = 'log_safe_ua' AND ua_sig = '%s' LIMIT $thres"; + + $res = mysql_global_call($sql, $ua_sig); + + if (!$res) { + return true; + } + + if (mysql_num_rows($res) < $thres) { + return false; + } + + return true; +} + +// April 2024 +function april_2024_parse_email($email) { + if ($email[0] != '$') { + return 0; + } + + $tag = substr(trim($email), 1); + + $stocks = april_2024_get_stock_list(); + + $idx = array_search($tag, $stocks); + + if ($idx === false) { + return 0; + } + + $count = april_2024_get_stock_count($tag); + + if ($count < 10) { + return 0; + } + + return 10000 + $idx; +} + +function april_2024_get_stock_list() { + static $stocks = [ + 'PEPE', 'WOJK', 'ANIME', 'CHAD', 'CLOWN', 'LOL', 'SICP', 'AUTSM', 'BANE', + 'CIA', 'BOOB', 'RDDT', 'DESU', 'JANNY', 'GME', 'CHUCK', 'YTSB', 'GACHI' + ]; + + return $stocks; +} + +function april_2024_get_stock_from_s4p($since4pass) { + if ($since4pass < 10000) { + return false; + } + + $val = $since4pass - 10000; + + if ($val < 0) { + return false; + } + + $badges = april_2024_get_stock_list(); + + if ($val >= 0 && $val < count($badges)) { + return $badges[$val]; + } + else { + return false; + } +} + +function april_2024_get_name() { + $net_worth = april_2024_get_net_worth(); + + if ($net_worth < 500) { + return 'Destitute Investor'; + } + else if ($net_worth < 1500) { + return 'Helpless Investor'; + } + else if ($net_worth < 5000) { + return 'Poor Investor'; + } + else if ($net_worth < 50000) { + return 'Fledgling Investor'; + } + else if ($net_worth < 500000) { + return 'Aspiring Investor'; + } + else if ($net_worth < 2000000) { + return 'Rich Investor'; + } + else if ($net_worth < 5000000) { + return 'Anonymous Magnate'; + } + else { + return 'Anonymous Mogul'; + } +} + +function april_2024_get_post_cls($since4pass) { + $stock = april_2024_get_stock_from_s4p($since4pass); + + if ($stock) { + return " p-xa24-$stock"; + } + else { + return ''; + } +} + +function april_2024_get_name_badge($since4pass) { + $stock = april_2024_get_stock_from_s4p($since4pass); + + if ($stock) { + return " "; + } + else { + return ''; + } +} + +function april_2024_get_stock_count($stock) { + $userpwd = UserPwd::getSession(); + + if (!$userpwd || $userpwd->isNew()) { + return 0; + } + + $user_id = $userpwd->getPwd(); + + $sql =<<isNew()) { + return 0; + } + + $user_id = $userpwd->getPwd(); + + $sql =<< 0 +SQL; + + $res = mysql_global_call($sql, $user_id); + + if (!$res) { + return 0; + } + + $stocks = []; + + while ($row = mysql_fetch_row($res)) { + $stocks[$row[0]] = (int)$row[1]; + } + + $sql =<< $count) { + if (isset($prices[$stock])) { + $net_worth += ($prices[$stock] * $count); + } + } + + return $net_worth; +} + +// --- + +function clear_no_captcha_token() { + setcookie('_ct', null, -3600, '/', '.' . L::d(BOARD_DIR)); +} + +function generate_no_captcha_token() { + if (BOARD_DIR === 'pol' || BOARD_DIR === 'b' || BOARD_DIR === 'r9k' || BOARD_DIR === 'bant') { + return false; + } + + $long_ip = ip2long($_SERVER['REMOTE_ADDR']); + + if (!$long_ip) { + return false; + } + + if (!spam_filter_is_user_known($long_ip, BOARD_DIR, null, 15)) { + return false; + } + + $salt = file_get_contents_cached(SALTFILE); + + if (!$salt) { + return false; + } + + $time = $_SERVER['REQUEST_TIME']; + + $msg = $_SERVER['REMOTE_ADDR'] . '.' . $time; + + $msg = hash_hmac('sha1', $msg, $salt); + + if (!$msg) { + return false; + } + + $msg = substr($msg, 0, 20) . '.' . $time; + + setcookie('_ct', $msg, $time + 300, '/', '.' . L::d(BOARD_DIR)); // 5 minutes +} + +function verify_no_captcha_token($token) { + list($hash, $ts) = explode('.', $token); + + $ts = (int)$ts; + + if (!$hash || !$ts) { + return false; + } + + if ($ts < $_SERVER['REQUEST_TIME'] - 300) { // 5 minutes + return false; + } + + $salt = file_get_contents_cached(SALTFILE); + + if (!$salt) { + return false; + } + + $msg = $_SERVER['REMOTE_ADDR'] . '.' . $ts; + + if (substr(hash_hmac('sha1', $msg, $salt), 0, 20) === $hash) { + return true; + } + + return false; +} + +function get_random_real_name() { + $first_name_nid = mt_rand(1, 1000); + + if (mt_rand(0, 999) < 10) { + $type = 2; + } + else { + $type = 1; + } + + $query = "SELECT data FROM april_names WHERE nid = $first_name_nid AND type = $type"; + $res = mysql_global_call($query); + $first_name = mysql_fetch_row($res)[0]; + + if (!$first_name) { + $first_name = 'Alberto'; + } + + $last_name_nid = mt_rand(1, 1000); + + $query = "SELECT data FROM april_names WHERE nid = $last_name_nid AND type = 3"; + $res = mysql_global_call($query); + $last_name = mysql_fetch_row($res)[0]; + + if (!$last_name) { + $last_name = 'Barbosa'; + } + + return "$first_name $last_name"; +} + + +function log_mod_action($action_type, $post, $vip_capcode = false) { + $mask_shift = 128; + $action_id = $mask_shift + $action_type; + + $query =<<verifyCode($dec_secret, $otp, 1)) { + error("Incorrect or expired OTP."); + } +} + +function validate_csrf() { + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + error('Bad Request.'); + } + + if (!isset($_COOKIE['_tkn']) || !isset($_POST['_tkn']) + || $_COOKIE['_tkn'] == '' || $_POST['_tkn'] == '' + || $_COOKIE['_tkn'] !== $_POST['_tkn']) { + + if (!is_local()) { + error('Bad Request.'); + } + } +} + +function validate_referer($strict = false) { + if (!$strict && (!isset($_SERVER['HTTP_REFERER']) || $_SERVER['HTTP_REFERER'] == '')) { + return; + } + + if (!preg_match('/^https?:\/\/([_a-z0-9]+)\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) { + error('Bad Request.'); + } +} + +function dev_make_remote_thumbnail() { + if (!is_local()) { + die('403'); + } + + $infile = $_FILES['file']['tmp_name']; + $file_ext = $_POST['file_ext']; + $src_width = $_POST['src_width']; + $src_height = $_POST['src_height']; + $th_width = $_POST['th_width']; + $th_height = $_POST['th_height']; + + if (!$infile || !$file_ext || !$src_width || !$src_height || !$th_width || !$th_height) { + return; + } + + $jpeg_quality = 65; + + switch ($file_ext) { + case 'gif': + $img_in = ImageCreateFromGIF($infile); + break; + case 'jpg': + $img_in = ImageCreateFromJPEG($infile); + break; + case 'png': + $img_in = ImageCreateFromPNG($infile); + break; + default : + return; + } + + if (!$img_in) { + return; + } + + $img_out = ImageCreateTrueColor($th_width, $th_height); + + if (!$img_out) { + return; + } + + ImageCopyResampled($img_out, $img_in, 0, 0, 0, 0, $th_width, $th_height, $src_width, $src_height); + + ImageDestroy($img_in); + + ImageJPEG($img_out, NULL, $jpeg_quality); + + ImageDestroy($img_out); +} + +/*-----------Main-------------*/ +switch( $mode ) { + case 'make_remote_thumbnail': + dev_make_remote_thumbnail(); + die(); + case 'purgejsontails': + if (has_flag('developer')) { + echo purge_json_tails() ? 'OK' : 'ERROR'; + } + die(); + case 'listarchive': + if (has_flag('developer')) { + rebuild_archive_list(true); + } + die(); + case 'search': + if (has_flag('developer')) { + rebuild_search_page(true); + } + die(); + case 'rebuildsearchpage': + if (has_flag('developer') || has_level('manager')) { + rebuild_search_page(); + echo 'done'; + } + die(); + case 'rebuildsyncframepage': + if (has_flag('developer') || has_level('manager')) { + rebuild_syncframe_page(isset($_GET['print'])); + echo 'done'; + } + die(); + case 'rebuildarchivedthread': + if (has_flag('developer') || has_level('manager')) { + rebuild_archived_thread((int)$_GET['id']); + echo 'done'; + } + die(); + + case 'regist': + case 'post': + require_request_method( "POST" ); + validate_referer(); + new_post( $name, $email, $sub, $com, '', $pwd, $upfile, $upfile_name, $resto, $age, $filetag ); + break; + case 'report': + report(); + break; + + case 'preview_html': + preview_html(); + break; + + case 'rebuild': + require_request_method( "GET" ); + rebuild(); + break; + case 'rebuildall': + rebuild( 1 ); + break; + + case 'rebuildadmin': + rebuild_deletions( array($no => '1') ); + echo 'Rebuilt OK!'; // shut rpc up + break; + + case 'rebuildcatalog': + if (ENABLE_CATALOG) { + rebuild_catalog(); + } + break; + + case 'rebuildboardsjson': + if (has_flag('developer')) { + rebuild_boards_json(); + } + break; + + case 'rebuildthumb': + rebuildallthumb(isset($_GET['archiveonly']) || isset($_ENV['archiveonly'])); + break; + + case 'cataloginfo': + get_catalog_info(); + break; + + case 'admindel': case 'admindelete': + user_delete( $no, $pwd ); + echo ""; + break; + case 'updatelog': + updatelog_remote( $no, $noidx ); + break; + case 'nothing': + break; + case 'arcdel': + require_request_method( "POST" ); + validate_referer(); + arcdel($no, true, $res); + break; + case 'usrdel': case 'delete': + require_request_method( "POST" ); + validate_referer(); + user_delete( $no, $pwd, true, $res ); + break; + case 'rebuild_threads_by_id': + require_request_method( "POST" ); + if (is_local()) { + rebuild_threads_by_id(); + } + else { + updating_index(); + } + break; + case 'forcearchive': + require_request_method( "POST" ); + validate_referer(); //validate_csrf(); + forcearchive(); + break; + case 'copythreads': + require_request_method( "GET" ); + do_copy_threads(); + break; + case 'movethread': + validate_csrf(); + do_move_thread(); + break; + case 'latest': + if (has_level('janitor')) { + get_last_post_no(); + } + die(); + case 'rake_post': + //april_rake_commit(); + die('0\nNo'); + break; + default: + require_request_method( "GET" ); + if( JANITOR_BOARD == 1 && !has_level( 'janitor' ) ) { + die( '' ); + } + if( $res ) { + resredir( $res ); + } else { + updating_index(); + } +} diff --git a/imgboard.php b/imgboard.php new file mode 100644 index 0000000..9b58b9c --- /dev/null +++ b/imgboard.php @@ -0,0 +1,10402 @@ +' . LOCKDOWN_MSG . '
    '); + } +} + +if (TEST_BOARD && (!has_level() || !has_flag('developer'))) { + die(''); +} + +// test +if( TEST_BOARD || has_flag('developer') ) { + ini_set( 'display_errors', 1 ); + //error_reporting(E_ALL & ~E_NOTICE); +} + +extract( $_POST, EXTR_SKIP ); +extract( $_GET, EXTR_SKIP ); +extract( $_COOKIE, EXTR_SKIP ); + +if (isset( $_COOKIE['4chan_pass']) && $_COOKIE['4chan_pass']) { + $pwdc = $_COOKIE['4chan_pass']; +} +else { + $pwdc = null; +} + +// FIXME whitelist +unset( $dest ); +unset( $log ); +unset( $update_avg_secs ); + +if( $argv[1] ) $mode = $argv[1]; +$id = intval( $id ); + +if( $_SERVER["REQUEST_METHOD"] == "POST" ) { + // bust cache + header( 'Cache-Control: private, no-cache, must-revalidate' ); + header( 'Expires: -1' ); + header( 'Vary: *' ); +} + +if( array_key_exists( 'upfile', $_FILES ) ) { + $upfile_name = $_FILES["upfile"]["name"]; + $upfile = $_FILES["upfile"]["tmp_name"]; +} else { + $upfile_name = $upfile = ''; +} + +$fwritetimer = 0.0; + +ignore_user_abort( true ); + +$word_filters_enabled = false; +if (WORD_FILT) { + $word_filt_root = '/www/global/yotsuba/wordfilters/'; + + if (file_exists($word_filt_root . BOARD_DIR . '.php')) { + include_once($word_filt_root . BOARD_DIR . '.php'); + $word_filters_enabled = true; + } + else if (file_exists($word_filt_root . 'global.php')) { + include_once($word_filt_root . 'global.php'); + $word_filters_enabled = true; + } +} + +if( JANITOR_BOARD == 1 && !has_level( 'janitor' ) ) { + die( '' ); +} + +if( JANITOR_BOARD == 1 ) + include_once 'plugins/broomcloset.php'; + +// QENHANCE +if( META_BOARD ) { + include_once 'plugins/enhance_q.php'; +} + +$mysql_connect_opts = 0; +mysql_board_connect(BOARD_DIR); + +$board_flags_array = null; + +if (ENABLE_BOARD_FLAGS) { + $_flags_type = (defined('BOARD_FLAGS_TYPE') && BOARD_FLAGS_TYPE) ? BOARD_FLAGS_TYPE : BOARD_DIR; + $_board_flags_path = '/www/global/yotsuba/lib/board_flags_' . $_flags_type . '.php'; + if (file_exists($_board_flags_path)) { + include_once($_board_flags_path); + $board_flags_array = get_board_flags_array(); + } +} + +$thread_unique_ips = 0; + +$index_rbl = PAGE_MAX; +$index_last_thread = 0; +$index_last_post = 0; + +if (JANITOR_BOARD && PAGE_MAX == 0) { + $index_rbl = ceil(LOG_MAX / DEF_PAGES); +} + +$valid_boards = "3|aco|adv|an|biz|diy|fa|fit|gd|gif|int|lit|hc|hr|a|b|ck|co|cm|c|d|e|f|g|h|i|k|lgbt|m|n|o|out|p|r|s|t|u|vp|vg|vr|v|w|x|y|wg|ic|cgl|hm|mlp|mu|pol|po|r9k|s4s|sci|soc|tg|tv|toy|trv|jp|sp|wsg|qa|qst|his|trash|news|wsr|vip|bant|vrpg|vmg|vst|vt|vm|pw|xs"; + +$boards_matching_arr = array(); + +$captcha_bypass = null; +$rangeban_bypass = false; +$passid = ''; + +// FIXME, this should be put somewhere else. +function is_local() { + $longip = ip2long( $_SERVER[ 'REMOTE_ADDR' ] ); + + return !$longip || cidrtest( $longip, "10.0.0.0/24" ) || cidrtest( $longip, "204.152.204.0/24" ) || cidrtest( $longip, "127.0.0.0/24" ); +} + +/** + * Abbreviates posts on index pages. + * Truncate $str to $max_lines lines and return $str and $abbr + * where $abbr = whether or not $str was actually truncated. + * Expects well-formed HTML. + */ +function abbreviate($str, $max_lines = 20) { + $lines = explode('
    ', $str); + + if (count($lines) > $max_lines) { + $abbr = 1; + $lines = array_slice($lines, 0, $max_lines); + $str = implode('
    ', $lines ); + + $unpaired_tags = array( + 'img' => true, + 'br' => true, + 'input' => true, + 'hr' => true, + 'param' => true + ); + + preg_match_all('/<\/([^>]+)>/', $str, $closed_tags, PREG_SET_ORDER); + $closed_count = count($closed_tags); + + $closed_map = array(); + + foreach ($closed_tags as $m) { + if (!isset($closed_map[$m[1]])) { + $closed_map[$m[1]] = 1; + } + else { + $closed_map[$m[1]] += 1; + } + } + + preg_match_all('/<([a-z0-9]+)(?: |>)/', $str, $open_tags, PREG_SET_ORDER); + $open_count = count($open_tags); + + for ($i = 0; $i < $open_count; ++$i) { + $tag = $open_tags[$i][1]; + + if (isset($unpaired_tags[$tag])) { + continue; + } + + if (!isset($closed_map[$tag])) { + $str .= ""; + } + else if ($closed_map[$tag] > 0) { + $closed_map[$tag] -= 1; + } + else if ($closed_map[$tag] <= 0) { + $str .= ""; + } + } + } + else { + $abbr = 0; + } + + return array($str, $abbr); +} + +/** + * Currently only used on /archive + * strips html tags and replaces sjis art with [SJIS] placeholders + */ +function truncate_comment($str, $length, $keep_spoilers = false) { + // remove sjis + if (SJIS_TAGS && strpos($str, '/', '[SJIS]', $str); + } + + $len = mb_strlen($str); + + if ($len <= $length) { + return $str; + } + + if (!$keep_spoilers) { + $str = strip_tags($str); + } + else { + $str = strip_tags($str, ''); + } + + if ($len <= $length) { + return $str; + } + + $str = mb_substr($str, 0, $length); + + // remove truncated html entities + $str = preg_replace('/&[^;]*$/', '', $str); + + if ($keep_spoilers) { + $str = preg_replace('/<[^>]*$/', '', $str); + + $oc = substr_count($str, ''); + + if ($oc) { + $cc = substr_count($str, ''); + $dc = $oc - $cc; + if ($dc > 0) { + $str .= str_repeat('', $dc); + } + } + } + + $str .= '…'; + + return $str; +} + +function paranoid_rename( $src, $dest ) +{ + $across_devices = false; //keep around for future use + $u = false; + + if( $across_devices ) { + // rename to dest dir, then over dest + $dsrc = dirname( $dest ) . "/" . basename( $src ); + if( !@rename( $src, $dsrc ) ) $u = $src; + else if( !@rename( $dsrc, $dest ) ) $u = $dsrc; + } else { + if( !@rename( $src, $dest ) ) $u = $src; + } + + if( $u ) + unlink( $u ); +} + +function rename_across_device( $src, $dest ) +{ + // FIXME: copy() does a chmod but we don't need that + copy($src, $dest); + unlink($src); +} + +function getmypid_cached() +{ + static $pid = -1; + + if ($pid === -1) $pid = getmypid(); + + return $pid; +} + +// print $contents to $filename by using a temporary file and renaming it +// may destroy $contents in the process +// (makes *.html and *.gz if USE_GZIP is on) +function print_page( $filename, &$contents, $force_nogzip = 0, $trim_whitespace = 1 ) +{ + global $fwritetimer; + + $timestarted = microtime( true ); + + if( NEW_HTML == 1 && $trim_whitespace ) { + $contents = str_replace( array("\r\n", "\n", "\t"), array('', '', ''), $contents ); + } + + $gzip = ( USE_GZIP == 1 && !$force_nogzip ); + + if( $gzip ) { + $tempname = dirname( $filename )."/gztmp".getmypid_cached(); + + // FIXME: number of syscalls done by gzwrite is not optimal (it does a small one then 4KB writes after) + // for small files (how small?) do gzencode() and file_put_contents() instead. + + $fp = gzopen($tempname, "wb9"); + if( $fp === false ) return; + gzwrite($fp, $contents); + gzclose($fp); + // chmod( $tempname, 0664 ); //it was created 0600 + + paranoid_rename( $tempname, $filename . ".gz" ); + } else { + $tempname = dirname( $filename )."/tmp".getmypid_cached(); + if( file_put_contents( $tempname, $contents ) === false ) return; + // chmod( $tempname, 0664 ); //it was created 0600 + paranoid_rename( $tempname, $filename ); + } + + $fwritetimer += ( microtime( true ) - $timestarted ); +} + +function file_get_contents_cached( $filename ) +{ + static $cache = array(); + if( isset( $cache[$filename] ) ) + return $cache[$filename]; + $cache[$filename] = @file_get_contents( $filename ); + + return $cache[$filename]; +} + +function file_array_cached( $filename ) +{ + static $cache = array(); + if( isset( $cache[$filename] ) ) + return $cache[$filename]; + $cache[$filename] = @file( $filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); + + return $cache[$filename]; +} + +function get_blotter() { + if (!SHOW_BLOTTER) { + return ''; + } + + $msg_limit = 3; + + $blotter = <<
    +HTML; + + $query = << 0) { + while ($row = mysql_fetch_assoc($res)) { + if ($mtime === 0) { + $mtime = $row['date']; + } + + $blotter .= '' . + date('m/d/y', $row['date']) . '' . + $row['content'] . ''; + } + } + else { + return ''; + } + + $blotter .= '[Hide] [Show All]'; + + return $blotter; +} + +function blotter_contents() +{ + static $cache; + if( isset( $cache ) ) return $cache; + $ret = ""; + $topN = 4; //how many lines to print + $bl_lines = file( BLOTTER_PATH ); + $bl_top = array_slice( $bl_lines, 0, $topN ); + $date = ""; + foreach( $bl_top as $line ) { + if( !$date ) { + $lineparts = explode( ' - ', $line ); + if( strpos( $lineparts[0], '', $lineparts[0] ); + $date = $dateparts[1]; + $date = "
  • Blotter updated: $date"; + } else { + $date = $lineparts[0]; + $date = "
  • Blotter updated: $date"; + } + } + $line = trim( $line ); + $line = str_replace( "\\", "\\\\", $line ); + $line = str_replace( "'", "\'", $line ); + $ret .= "'
  • $line'+\n"; + } + $ret .= "''"; + $cache = array($date, $ret); + + return array($date, $ret); +} + +function find_match_and_prefix( $regex, $str, $off, &$match ) +{ + if( !preg_match( $regex, $str, $m, PREG_OFFSET_CAPTURE, $off ) ) return false; + + $moff = $m[0][1]; + $match = array(substr( $str, $off, $moff - $off ), $m[0][0]); + + return true; +} + +// skip_on_spoilers will stop parsing and return the unmodified comment +// if spoiler tags are found inside the string to wrap. +// This is to avoid sjis.spoiler tags mixing mostly. +function parse_bbcode_one( $com, $tn, $st, $et, $nest_limit = 2, $skip_on_spoilers = false ) +{ + if( !find_match_and_prefix( "/\[$tn\]/", $com, 0, $m ) ) return $com; + + $bracket_tn = "[$tn]"; + $bl = strlen( $bracket_tn ); + $el = $bl + 1; + $ret = $m[0] . $st; + $lev = 1; + $off = strlen( $m[0] ) + $bl; + + while( 1 ) { + if (!find_match_and_prefix( "@\[/?$tn\]@", $com, $off, $m)) break; + list( $txt, $tag ) = $m; + + if (!$skip_on_spoilers || $tag === $bracket_tn) { + $ret .= $txt; + } + else if (preg_match('/\[\/?spoiler\]/', $txt)) { + return $com; + } + + $off += strlen( $txt ) + strlen( $tag ); + + if( $tag == $bracket_tn ) { + if( $lev < $nest_limit ) + $ret .= $st; + $lev++; + } else if( $lev ) { + if( $lev <= $nest_limit ) + $ret .= $et; + $lev--; + } + } + + $tail = substr($com, $off, strlen($com) - $off); + $ret .= $tail; + + $lev = min( $lev, $nest_limit ); + + if ($lev > 0) { + if ($skip_on_spoilers && preg_match('/\[\/?spoiler\]/', $tail)) { + return $com; + } + + $ret .= str_repeat( $et, $lev ); + } + + return $ret; +} + +function spoiler_parse( $com ) +{ + return parse_bbcode_one( $com, 'spoiler', '', '' ); +} + +function jsmath_parse( $com ) +{ + $com = parse_bbcode_one( $com, "math", '', '' ); + $com = parse_bbcode_one( $com, "eqn", '
    ', '
    ' ); + + return $com; +} + +/* BBCode for bold, italic, and r/g/b color tags */ +function parse_op_markup($com) { + $com = parse_bbcode_one($com, 'b', '', '', 1); + $com = parse_bbcode_one($com, 'i', '', '', 1); + + $com = parse_bbcode_one($com, 'red', '', '', 1); + $com = parse_bbcode_one($com, 'green', '', '', 1); + $com = parse_bbcode_one($com, 'blue', '', '', 1); + + return $com; +} + +function code_parse( $com ) +{ + return parse_bbcode_one( $com, 'code', '
    ', '
    ' ); +} + +function sjis_parse($com) { + $skip_on_spoilers = strpos($com, '[spoiler]') !== false; + + return parse_bbcode_one($com, 'sjis', '', '', 1, $skip_on_spoilers); +} + +// convenience function for wordfilters. +// text must be html escaped +function random_color( $str, $background = 1, $foreground = 1 ) +{ + $style = ""; + + if( $background ) { + $r = rand( 0, 255 ); + $g = rand( 0, 255 ); + $b = rand( 0, 255 ); + $style = $style . "background: #" . sprintf( "%02x%02x%02x", $r, $g, $b ) . "; "; + } + + if( $foreground ) { + $r = rand( 0, 255 ); + $g = rand( 0, 255 ); + $b = rand( 0, 255 ); + $style = $style . "color: #" . sprintf( "%02x%02x%02x", $r, $g, $b ) . "; "; + } + + if( $style ) { + return "$str"; + } + + return $str; +} + +function append_ban( $board, $ip ) +{ + $cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $board $ip >/dev/null 2>&1 &"; + exec( $cmd ); +} + +// check whether the current user can perform $action (on $no, for some actions) +// board-level access is cached in $valid_cache. +// FIXME move to lib/admin.php +function valid( $action = 'moderator', $no = 0 ) +{ + static $valid_cache, $can_post_html; // the access level of the user + $access_level = array('none' => 0, 'janitor' => 1, 'janitor_this_board' => 2, 'moderator' => 5, 'manager' => 10, 'admin' => 20); + if( !isset( $valid_cache ) ) { + $valid_cache = $access_level['none']; + if( isset( $_COOKIE['4chan_auser'] ) && isset( $_COOKIE['apass'] ) ) { + $user = mysql_real_escape_string( $_COOKIE['4chan_auser'] ); + $pass = $_COOKIE['apass']; + } + if( $user && $pass ) { + $result = mysql_global_call( "SELECT allow,deny,password_expired,username,password FROM " . SQLLOGMOD . " WHERE username='$user' LIMIT 1" ); + list( $allow, $deny, $expired, $username, $password ) = mysql_fetch_row( $result ); + mysql_free_result( $result ); + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $hashed_admin_password = hash('sha256', $username . $password . $admin_salt); + + if ($hashed_admin_password !== $pass) { + return false; + } + + if( $expired ) { + error( 'Your password has expired; check IRC for instructions on changing it.' ); + } + + if( $allow ) { + $allows = explode( ',', $allow ); + $seen_janitor_token = false; + // each token can increase the access level, + // except that we only know that they're a moderator or a janitor for another board + // AFTER we read all the tokens + $cphtml = false; + + foreach( $allows as $token ) { + if( $token == 'janitor' ) { + $seen_janitor_token = true; + } else if( $token == 'manager' && $valid_cache < $access_level['manager'] ) { + $valid_cache = $access_level['manager']; + } else if( $token == 'admin' && $valid_cache < $access_level['admin'] ) { + $valid_cache = $access_level['admin']; + } else if( ( $token == BOARD_DIR || $token == 'all' ) && $valid_cache < $access_level['janitor_this_board'] ) { + $valid_cache = $access_level['janitor_this_board']; // or could be moderator, will be increased in next step + } elseif( $token == 'html' ) { + $cphtml = true; + } + } + + $can_post_html = $cphtml; + // now we can set moderator or janitor status + if( !$seen_janitor_token ) { + if( $valid_cache < $access_level['moderator'] ) + $valid_cache = $access_level['moderator']; + } else { + if( $valid_cache < $access_level['janitor'] ) + $valid_cache = $access_level['janitor']; + } + if( $deny ) { + $denies = explode( ',', $deny ); + if( in_array( BOARD_DIR, $denies ) ) { + $valid_cache = $access_level['none']; + } + } + } + } + } + { + // local rpc can do anything + $longip = ip2long( $_SERVER['REMOTE_ADDR'] ); + if( !$longip || cidrtest( $longip, "10.0.0.0/24" ) || + cidrtest( $longip, "204.152.204.0/24" ) || cidrtest( $longip, "127.0.0.0/24" ) + ) + return YES; + } + switch( $action ) { + case 'moderator': + return $valid_cache >= $access_level['moderator']; + case 'textonly': + return $valid_cache >= $access_level['moderator']; + case 'htmlnopw': + return $valid_cache >= $access_level['manager']; + case 'htmlpost': + return $can_post_html; + case 'janitor_board': + return $valid_cache >= $access_level['janitor']; + case 'delete': + if( $valid_cache >= $access_level['janitor_this_board'] ) { + return true; + } // if they're a janitor on another board, check for illegal post unlock + else if( $valid_cache >= $access_level['janitor'] ) { + $query = mysql_global_do( "SELECT COUNT(*) from reports WHERE board='" . BOARD_DIR . "' AND no=$no AND cat=2" ); + $illegal_count = mysql_result( $query, 0, 0 ); + mysql_free_result( $query ); + + return $illegal_count >= 3; + } + case 'reportflood': + return $valid_cache >= $access_level['janitor']; + case 'floodbypass': + return $valid_cache >= $access_level['janitor']; + case 'rebuild': + return $valid_cache >= $access_level['janitor']; + case 'admin': + return $valid_cache >= $access_level['admin']; + default: // unsupported action + return false; + } +} + +function iplog_add( $board, $no, $ip, $time, $is_thread, $tim, $had_image ) +{ + mysql_global_call( "INSERT INTO user_actions (board,postno,ip,time,uploaded,action,had_image) VALUES ('%s',%d,%d,from_unixtime(%d),%d,'%s',%d)",$board, $no, ip2long($ip), $time, $tim, $is_thread ? "new_thread" : "new_reply", $had_image); +} + +function clean_log_bool( &$row ) +{ + static $bool_cols = array('sticky', 'permasage', 'closed', 'filedeleted', 'permaage', 'undead', 'archived'); + + foreach ($bool_cols as $col) { + if (!isset($row[$col])) continue; // FIXME split this function up to avoid this test + $c = &$row[$col]; + settype($c, "bool"); + // NOTES: $c = $c ? TRUE : FALSE allocates new bools each time + // $c = $c ? &$itrue : &$ifalse causes them to be converted to strings(!) + // Put this back to TRUE : FALSE instead of a settype call sometime so we can look at the php bytecodes + } +} + +function clean_log_int( &$row ) +{ + static $int_cols = array('no', 'w', 'h', 'tn_w', 'tn_h', 'last_modified', 'time', 'fsize', 'resto'); + + // turn columns into int (does this help?) + foreach( $int_cols as $col ) { + if (!isset($row[$col])) continue; + + $c = &$row[$col]; + settype($c, "int"); + } +} + +function clean_log_delreply( &$row ) +{ + if (!$row['resto']) + return; + + static $del_cols = array('sticky', 'permasage', 'closed', 'last_modified', 'root', 'undead', 'permaage'); + + // delete fields not used for replies + foreach( $del_cols as $col ) { + unset($row[$col]); + } +} + +function clean_log_intern( &$row ) +{ + static $intern_cols = array('name', 'ext', 'capcode', 'country'); + + // Intern repeated strings that are usually the same value. + // In PHP 6 this... doesn't seem to do anything? Let's try again in 7. + static $log_intern; + if( !isset( $log_intern ) ) { + $log_intern = array(); + foreach( $intern_cols as $col ) + $log_intern[$col] = array(); + } + + foreach( $intern_cols as $col ) { + $intern_array = &$log_intern[$col]; + $c = &$row[$col]; + + $v = $c; + if( !isset($intern_array[$v]) ) + $intern_array[$v] = $v; + $c = &$intern_array[$c]; + } +} + +function clean_log_row( &$row ) +{ + //static $rn = 0; + clean_log_delreply( $row ); + clean_log_bool( $row ); + clean_log_int( $row ); + //clean_log_intern( $row ); + //if (++$rn == 100) { + // debug_zval_dump($row); + //} +} + +function log_bad_cache_entry($no) +{ + global $log_cache_level; + global $log; + + internal_error_log("logcache", "missing children for OP no $no, cache level $log_cache_level, cache contents ".count($log)); +} + +function invalidate_log($thread) +{ + global $log_cache_level; + global $log; + + if (isset($log[$thread])) { + if ($log[$thread]['resto']) { + die(S_ASSERT); + } + unset($log[$thread]); + } + + if ($log_cache_level==2) + $log_cache_level = 1; +} + +// build a structure out of all the posts in the database. +// this lets us replace a LOT of queries with a simple array access. +// it only builds the first time it was called. +// rather than calling log_cache(1) to rebuild everything, +// you should just manipulate the structure directly. +// $thread may be any postno in a thread +// without a thread, $archive_mode fetches all live threads if 0 and all archived threads if 1 +function log_cache($invalidate = 0, $thread = 0, $archive_mode = 0) { + global $log_cache_level; + global $log, $ipcount, $mysql_unbuffered_reads; + + if (!isset($log) || $invalidate) { + $log = array(); + $log_cache_level = 0; + } + + // Optimisation for index rebuilding when REPLIES_SHOWN is 0. + // No need to fetch the entire board in this case. + $optimised_indexes = !$thread && !$archive_mode && !REPLIES_SHOWN && IS_REBUILDD; + + // Handle cache + // 1 = Live OPs are cached, 2 = Some threads are cached, 3 = Whole live board is cached + if ($optimised_indexes) { + $nlog_cache_level = 1; + } + else { + $nlog_cache_level = $thread ? 2 : 3; + } + + // Whole board is cached, nothing to do. + if ($log_cache_level == 3 && $archive_mode === 0) { + return; + } + + // Thread is cached, nothing to do. + if ($log_cache_level == 2 && isset($log[$thread])) { + return; + } + + // Live OPs are cached, nothing to do. + if ($log_cache_level == 1 && $optimised_indexes) { + return; + } + + if ($nlog_cache_level > $log_cache_level) { + $log_cache_level = $nlog_cache_level; + } + + $ips = array(); + + mysql_board_call( "SET read_buffer_size=1048576" ); + $mysql_unbuffered_reads = 1; + + $query_archived = false; + + if ($thread) { + if ($archive_mode === 0) { + $where = " WHERE archived = 0 AND (resto = $thread OR no = $thread)"; + } + else if ($archive_mode === 1) { + $where = " WHERE archived = 1 AND (resto = $thread OR no = $thread)"; + } + else { + $query_archived = true; + $where = " WHERE (archived = 0 AND resto = $thread) OR (archived = 1 AND resto = $thread) OR no = $thread"; + } + } + else { + if ($archive_mode === 1) { + $query_archived = true; + $where = ' WHERE archived = 1'; + } + else if ($optimised_indexes) { + $where = ' WHERE archived = 0 AND resto = 0'; + + $_thread_ids = array(); + } + else { + $where = ' WHERE archived = 0'; + } + } + + $fields = "no,sticky,permasage,closed,now,name,sub,com,host,pwd,filename,ext,w,h,tn_w,tn_h,tim,time,md5,fsize,last_modified,root,resto,filedeleted,id,capcode,country,undead,permaage,since4pass"; + + if ($query_archived) { + $fields .= ",archived"; + } + + if (MOBILE_IMG_RESIZE) { + $fields .= ",m_img"; + } + + if (ENABLE_BOARD_FLAGS) { + $fields .= ",board_flag"; + } + + $sql_cache = "sql_no_cache"; + + $query = mysql_board_call( "SELECT $sql_cache $fields FROM `" . SQLLOG . "`" . $where ); + $offset = 0; + + while( $row = mysql_fetch_assoc( $query ) ) { + if (!$query_archived) $row['archived'] = $archive_mode; + clean_log_row( $row ); + + $row_no = $row['no']; + $row_resto = $row['resto']; + + // IF mysql returns rows in order by default then replies come after OP + // so if OP doesn't exist, this post is orphaned and should be skipped + // TODO: let's not skip for now, it seems more likely to cause bugs than anything + //if ($fetching_whole_threads && $row_resto && !isset($log[$row_resto])) continue; + + if ($optimised_indexes) { + $_thread_ids[] = (int)$row_no; + } + + $ips[$row['host']] = TRUE; + + if (!$row_resto) { + $row['children'] = array(); + $row['imgreplycount'] = 0; + $row['replycount'] = 0; + } + else { + if (isset($log[$row_resto])) { + $log[$row_resto]['children'][$row_no] = TRUE; + + if ($row['fsize'] && !$row['filedeleted']) { + $log[$row_resto]['imgreplycount']++; + } + + $log[$row_resto]['replycount']++; + } + } + + $log[$row_no] = $row; + } + + $query = null; + + $nrows = count($log); + //if (BOARD_DIR=="b" && !$thread) quick_log_to("/www/perhost/logcaches.log", "inefficient all-board log_cache run t=$thread r=$nrows inv=$invalidate lev=$log_cache_level", true); + // if (!STATIC_REBUILD && $thread) quick_log_to("/www/perhost/logcaches.log", "inefficient? one-thread log_cache run t=$thread r=$nrows inv=$invalidate lev=$log_cache_level", true); + + $is_single_thread = $thread && isset($log[$thread]['children']); + $ipcount = count( $ips ); + unset($ips); + + $mysql_unbuffered_reads = 0; + mysql_board_call( "SET read_buffer_size=131072" ); + + if (!$thread) { + if ($optimised_indexes) { + if (empty($_thread_ids)) { + return; + } + + $_thread_ids = implode(',', $_thread_ids); + + $query = mysql_board_call("SELECT resto, COUNT(*) AS r_count, SUM(IF(fsize > 0 AND filedeleted = 0, 1, 0)) AS i_count FROM `" . SQLLOG . "` WHERE resto IN($_thread_ids) GROUP BY resto"); + + while ($row = mysql_fetch_assoc($query)) { + $log[$row['resto']]['imgreplycount'] = (int)$row['i_count']; + $log[$row['resto']]['replycount'] = (int)$row['r_count']; + } + + $query = mysql_board_call("SELECT no FROM `" . SQLLOG . "` WHERE no IN($_thread_ids) ORDER BY root DESC"); + } + else { + if ($archive_mode === 1) { + $archived = "archived = 1"; + } else { + $archived = "archived = 0"; + } + $query = mysql_board_call("SELECT no FROM `" . SQLLOG . "` WHERE $archived AND resto = 0 AND root > 0 ORDER BY root DESC"); + } + + $threads = array(); // IDs + + while ($row = mysql_fetch_row($query)) { + $no = (int)$row[0]; + + if (isset($log[$no])) { + $threads[] = $no; + } + } + + $log['THREADS'] = $threads; + + foreach( $threads as $thread ) { + $this_thread = $log[$thread]; + + if (!$this_thread['permaage'] && !$this_thread['sticky'] && $this_thread['replycount'] >= MAX_RES) { + $log[$thread]['bumplimit'] = TRUE; + } + else { + $log[$thread]['bumplimit'] = FALSE; + } + + if ($this_thread['archived']) { + $log[$thread]['archived_on'] = strtotime($this_thread['root']); + } + + if (!$this_thread['permaage'] && !$this_thread['sticky'] && !$log[$thread]['undead'] && $this_thread['imgreplycount'] >= MAX_IMGRES) { + $log[$thread]['imagelimit'] = TRUE; + } + else { + $log[$thread]['imagelimit'] = FALSE; + } + + $log[$thread]['semantic_url'] = generate_href_context($this_thread['sub'], $this_thread['com']); + } + } + else if ($is_single_thread) { + if (!$log[$thread]['permaage'] && !$log[$thread]['sticky'] && $log[$thread]['replycount'] >= MAX_RES) { + $log[$thread]['bumplimit'] = TRUE; + } + else { + $log[$thread]['bumplimit'] = FALSE; + } + + if ($log[$thread]['archived']) { + $log[$thread]['archived_on'] = strtotime($log[$thread]['root']); + } + + if (!$log[$thread]['permaage'] && !$log[$thread]['sticky'] && !$log[$thread]['undead'] && $log[$thread]['imgreplycount'] >= MAX_IMGRES) { + $log[$thread]['imagelimit'] = TRUE; + } + else { + $log[$thread]['imagelimit'] = FALSE; + } + + $log[$thread]['semantic_url'] = generate_href_context($log[$thread]['sub'], $log[$thread]['com']); + } + + // calculate old-status for PAGE_MAX mode + //$threadcount = count( $threads ); + + /*if(EXPIRE_NEGLECTED != 1) { + rsort($threads, SORT_NUMERIC); + + if(PAGE_MAX > 0) // the lowest 5% of maximum threads get marked old + for($i = floor(0.95*PAGE_MAX*DEF_PAGES); $i < $threadcount; $i++) { + if(!$log[$threads[$i]]['sticky']) + $log[$threads[$i]]['old'] = 1; + } + else { // threads w/numbers below 5% of LOG_MAX get marked old + foreach($threads as $thread) { + if($lastno-LOG_MAX*0.95>$thread) + if(!$log[$thread]['sticky']) + $log[$thread]['old'] = 1; + } + } + } else { + $rthreads = array(); + foreach ($threads as $t) { + $root = $log[$t]['root']; + $rthreads[$t] = $root; + } + + arsort($rthreads); + $rthreads = array_keys($rthreads); + + if (PAGE_MAX > 0) { + $floor = (int)floor(0.95*PAGE_MAX*DEF_PAGES); + for($i = $floor; $i < $threadcount; $i++) { + if(!$log[$rthreads[$i]]['sticky']) + $log[$rthreads[$i]]['old'] = 1; + } + } + }*/ +} + +function rebuildallthumb($archiveonly = false) +{ + global $log; + if( !has_level() ) return; + $starttime = microtime( true ); + set_time_limit( 0 ); + if (!$archiveonly) { + log_cache(); + $nposts = count($log); + echo "Rebuilding $nposts live posts
    \n"; + + foreach( $log as $post ) { + if( !$post["ext"] ) continue; + + $ext = $post["ext"]; + $tim = $post["tim"]; + $resto = $post["resto"]; + $fname = IMG_DIR . $tim . $ext; + make_thumb( $fname, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5 ); + } + $totaltime = microtime( true ) - $starttime; + echo "Took $totaltime seconds for live posts
    \n"; + } + + // Run again for archived thumbs + $starttime = microtime( true ); + mysql_check_connections(); + log_cache(1, 0, 1); // fetch archives instead + $nposts = count($log); + echo "Rebuilding $nposts archived posts
    \n"; + + foreach( $log as $post ) { + if( !$post["ext"] ) continue; + + $ext = $post["ext"]; + $tim = $post["tim"]; + $resto = $post["resto"]; + $fname = IMG_DIR . $tim . $ext; + make_thumb( $fname, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5 ); + } + $totaltime = microtime( true ) - $starttime; + echo "Took $totaltime seconds for archived posts
    \n"; +} + +/** + * Displays some text on a lightweight page styled using the default stylesheet. + * $message should be html-escaped + */ +function headless_message($message, $autoclose = false) { + if (DEFAULT_BURICHAN) { + $css = 'yotsubluenew'; + $ws = 'ws'; + } + else { + $css = 'yotsubanew'; + $ws = ''; + } + + $css_ver = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION; + + if ($error) { + $err_css = 'color: red;'; + } + else { + $err_css = ''; + } + + if ($autoclose) { + $js = <<setTimeout(function() { self.close(); }, 3000); +JS; + } + else { + $js = ''; + } + + $html = << + + + + + + + + + + +
    + $message +
    $js + + +HTML; + + return $html; +} + +function do_move_thread() { + if (!isset($_POST['id']) || !isset($_POST['board'])) { + updating_index(); + } + + if (!has_level()) { + updating_index(); + } + + $del = isset($_POST['move_del']) && $_POST['move_del']; + + $ret = move_thread($_POST['id'], $_POST['board'], $del); + + if (is_array($ret)) { + $message = 'Thread moved to /' . $ret[0] . '/' . $ret[1] . ''; + + echo headless_message($message, true); + } + else { + echo headless_message("$ret"); + } +} + +function do_copy_threads() { + if (!has_level()) { + updating_index(); + return; + } + global $argv; + + $to_board = $_REQUEST["board"]; + if (!$to_board) $to_board = $argv[2]; + + if (!$to_board) { + updating_index(); + return; + } + + set_time_limit( 0 ); + header( "Pragma: no-cache" ); + _print(fancystyle()); + + if (UPLOAD_BOARD || JANITOR_BOARD) { + $ret = "The current board doesn't support this feature."; + } else if ($to_board === 'f' || $to_board === 'j') { + $ret = "The destination board doesn't support this feature."; + } else { + $threads = mysql_column_array(mysql_board_call("SELECT no FROM `%s` WHERE resto = 0 AND archived = 0", BOARD_DIR)); + foreach ($threads as $thread) {$ret = copy_thread($thread, $to_board); _print("$thread
    \n");} + } + + if (is_array($ret)) { + $message = 'Thread copied to /' . $ret[0] . '/' . $ret[1] . ''; + + echo headless_message($message, true); + } + else { + echo headless_message("$ret"); + } +} + +function copy_thread($thread_id, $to_board, $delete = false) { + $thread_id = (int)$thread_id; + + if (!$thread_id) { + return "Invalid thread ID."; + } + + // Validate destination board + if (!preg_match('/^[a-z0-9]+$/', $to_board)) { + return 'Invalid destination board.'; + } + + $query = "SELECT COUNT(*) FROM boardlist WHERE dir = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $to_board); + + if (!$res) { + return "Database Error (1)"; + } + + if (mysql_num_rows($res) < 1) { + return "Destination board doesn't exist."; + } + + // --- + + // Fetch the whole thread + $posts = array(); + + $res = mysql_board_call("SELECT * FROM `%s` WHERE no = %d AND resto = 0", BOARD_DIR, $thread_id); + if (!$res) { + return "Database Error (3)"; + } + + $row = mysql_fetch_assoc($res); + if (!$row) { + return "Thread not found."; + } + + $posts[] = $row; + $res = mysql_board_call("SELECT * FROM `%s` WHERE resto = %d", BOARD_DIR, $thread_id); + if (!$res) { + return "Database Error (4)"; + } + + while ($row = mysql_fetch_assoc($res)) { + if (!$row) { + continue; + } + $posts[] = $row; + } + + // Copy posts to the other board + mysql_board_call('START TRANSACTION'); + + $new_resto = 0; + + $from_pids = array(); + $to_pids = array(); + + foreach ($posts as &$post) { + $comment = str_replace($from_pids, $to_pids, $post['com']); + + if ($new_resto === 0) { + $root_time = $post['root']; + } + else { + $root_time = 0; + } + + $query = "INSERT INTO `$to_board`(now,name,sub,com,host,pwd,filename,ext,w, +h,tn_w,tn_h, tim,time,last_modified,md5,fsize,root,resto,capcode, +4pass_id,since4pass,filedeleted,tmd5,id,sticky,closed,country) +VALUE (" . + "'" . $post['now'] . "'," . + "'" . mysql_real_escape_string($post['name']) . "'," . + "'" . mysql_real_escape_string($post['sub']) . "'," . + "'" . mysql_real_escape_string($comment) . "'," . + "'" . mysql_real_escape_string($post['host']) . "'," . + "'" . mysql_real_escape_string($post['pwd']) . "'," . + "'" . mysql_real_escape_string($post['filename']) . "'," . + "'" . $post['ext'] . "'," . + (int)$post['w'] . "," . + (int)$post['h'] . "," . + (int)$post['tn_w'] . "," . + (int)$post['tn_h'] . "," . + "'" . $post['tim'] . "'," . + (int)$post['time'] . "," . + (int)$post['time'] . "," . + "'" . $post['md5'] . "'," . + (int)$post['fsize'] . "," . + "'" . $root_time . "'," . + $new_resto . "," . + "'" . $post['capcode'] . "'," . + "'" . $post['4pass_id'] . "'," . + (int)$post['since4pass'] . "," . + (int)$post['filedeleted'] . "," . + "'" . $post['tmd5'] . "'," . + "'" . mysql_real_escape_string($post['id']) . "'," . + (int)$post['sticky'] . "," . + (int)$post['closed'] . "," . + "'XX')"; + + $res = mysql_board_call($query); + if (!$res) { + if ($new_resto === 0) { + mysql_board_call('ROLLBACK'); + return 'Database Error (5)'; + } + + $post['ext'] = null; + + continue; + } + + $new_pid = mysql_board_insert_id(); + + if ($new_resto === 0) { + $new_resto = $new_pid; + } + + $from_pids[] = ">>{$post['no']}"; + $to_pids[] = ">>$new_pid"; + + $post['new_id'] = $new_pid; + } + + unset($post); + + mysql_board_call('COMMIT'); + + // Copy files + // If the file already exists, update the database and set it as "deleted" + $to_img_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', IMG_ROOT) . $to_board . '/'; + $to_thumb_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', THUMB_ROOT) . $to_board . '/'; + + $dup_pids = array(); + + foreach ($posts as $post) { + if (!$post['ext'] || $post['filedeleted']) { + continue; + } + + $src_thumb = THUMB_DIR . $post['tim'] . 's.jpg'; + $dest_thumb = $to_thumb_dir . $post['tim'] . 's.jpg'; + + if (file_exists($dest_thumb)) { + //$dup_pids[] = $post['new_id']; + continue; + } + + $src_img = IMG_DIR . $post['tim'] . $post['ext']; + $dest_img = $to_img_dir . $post['tim'] . $post['ext']; + + @copy($src_thumb, $dest_thumb); + @copy($src_img, $dest_img); + } + + if (!empty($dup_pids)) { + $dup_clause = implode(',', $dup_pids); + $query = "UPDATE `$to_board` SET filedeleted = 1, ext = '' WHERE no IN($dup_clause)"; + $res = mysql_board_call($query); + } + + // Ask the destination board to build the new thread + remote_rebuild_live_thread($to_board, $new_resto); + + // Rebuild indexes + if (!STATIC_REBUILD) { + updatelog(0, 0); + } + + return array($to_board, $new_resto); +} + +function move_thread($thread_id, $to_board, $delete = false) { + if (UPLOAD_BOARD || BOARD_DIR === 'b' || JANITOR_BOARD) { + return "The current board doesn't support this feature."; + } + + if ($to_board === 'f' || $to_board === 'j') { + return "The destination board doesn't support this feature."; + } + + if ($to_board === BOARD_DIR) { + return "Invalid destination board."; + } + + $thread_id = (int)$thread_id; + + if (!$thread_id) { + return "Invalid thread ID."; + } + + // Validate destination board + if (!preg_match('/^[a-z0-9]+$/', $to_board)) { + return 'Invalid destination board.'; + } + + $query = "SELECT COUNT(*) FROM boardlist WHERE dir = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $to_board); + + if (!$res) { + return "Database Error (1)"; + } + + if (mysql_num_rows($res) < 1) { + return "Destination board doesn't exist."; + } + + // --- + + $board = mysql_real_escape_string(BOARD_DIR); + + // Lock the thread immediately + $query = "UPDATE `$board` SET closed = 1 WHERE no = $thread_id"; + + $res = mysql_board_call($query); + + if (!$res) { + return "Database Error (2)"; + } + + if (mysql_affected_rows() !== 1) { + return "This thread is locked."; + } + + // Fetch the whole thread + $posts = array(); + + $query = "SELECT * FROM `$board` WHERE no = $thread_id AND resto = 0"; + + $res = mysql_board_call($query); + + if (!$res) { + return "Database Error (3)"; + } + + $row = mysql_fetch_assoc($res); + + if (!$row) { + return "Thread not found."; + } + + if ($row['archived'] !== '0') { + return "You cannot move archived threads."; + } + + $posts[] = $row; + + $query = "SELECT * FROM `$board` WHERE resto = $thread_id"; + + $res = mysql_board_call($query); + + if (!$res) { + return "Database Error (4)"; + } + + while ($row = mysql_fetch_assoc($res)) { + if (!$row) { + continue; + } + $posts[] = $row; + } + + // Copy posts to the other board + mysql_board_call('START TRANSACTION'); + + $new_resto = 0; + + $from_pids = array(); + $to_pids = array(); + + foreach ($posts as &$post) { + $comment = str_replace($from_pids, $to_pids, $post['com']); + + if ($new_resto === 0) { + $root_time = 'NOW()'; + } + else { + $root_time = 0; + } + + if (SHOW_COUNTRY_FLAGS && $post['board_flag'] == '') { + $flag_val = $post['country']; + } + else { + $flag_val = 'XX'; + } + + $query = "INSERT INTO `$to_board`(now,name,sub,com,host,pwd,email,filename,ext,w, +h,tn_w,tn_h, tim,time,last_modified,md5,fsize,root,resto,capcode, +4pass_id,since4pass,filedeleted,tmd5,id,country) +VALUE (" . + "'" . $post['now'] . "'," . + "'" . mysql_real_escape_string($post['name']) . "'," . + "'" . mysql_real_escape_string($post['sub']) . "'," . + "'" . mysql_real_escape_string($comment) . "'," . + "'" . mysql_real_escape_string($post['host']) . "'," . + "'" . mysql_real_escape_string($post['pwd']) . "'," . + "'" . mysql_real_escape_string($post['email']) . "'," . + "'" . mysql_real_escape_string($post['filename']) . "'," . + "'" . $post['ext'] . "'," . + (int)$post['w'] . "," . + (int)$post['h'] . "," . + (int)$post['tn_w'] . "," . + (int)$post['tn_h'] . "," . + "'" . $post['tim'] . "'," . + (int)$post['time'] . "," . + (int)$post['time'] . "," . + "'" . $post['md5'] . "'," . + (int)$post['fsize'] . "," . + $root_time . "," . + $new_resto . "," . + "'" . $post['capcode'] . "'," . + "'" . $post['4pass_id'] . "'," . + (int)$post['since4pass'] . "," . + (int)$post['filedeleted'] . "," . + "'" . $post['tmd5'] . "'," . + "''," . + "'" . $flag_val . "')"; + + $res = mysql_board_call($query); + + if (!$res) { + if ($new_resto === 0) { + mysql_board_call('ROLLBACK'); + return 'Database Error (5)'; + } + + $post['ext'] = null; + + continue; + } + + $new_pid = mysql_board_insert_id(); + + if ($new_resto === 0) { + $new_resto = $new_pid; + } + + $from_pids[] = ">>{$post['no']}"; + $to_pids[] = ">>$new_pid"; + + $post['new_id'] = $new_pid; + } + + unset($post); + + mysql_board_call('COMMIT'); + + // Log the action + $thread = $posts[0]; + + $log_com = "From /$board/$thread_id to /$to_board/$new_resto"; + + if ($thread['com'] !== '') { + $log_com = $thread['com'] . '

    ' . $log_com; + } + + $action_log_post = array( + 'no' => $thread['no'], + 'name' => $thread['name'], + 'sub' => $thread['sub'], + 'com' => $log_com, + 'filename' => $thread['filename'], + 'ext' => $thread['ext'] + ); + + log_mod_action(6, $action_log_post); + + // Copy files + // If the file already exists, update the database and set it as "deleted" + $to_img_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', IMG_ROOT) . $to_board . '/'; + $to_thumb_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', THUMB_ROOT) . $to_board . '/'; + + $dup_pids = array(); + + foreach ($posts as $post) { + if (!$post['ext'] || $post['filedeleted']) { + continue; + } + + $src_thumb = THUMB_DIR . $post['tim'] . 's.jpg'; + $dest_thumb = $to_thumb_dir . $post['tim'] . 's.jpg'; + + if (file_exists($dest_thumb)) { + $dup_pids[] = $post['new_id']; + continue; + } + + $src_img = IMG_DIR . $post['tim'] . $post['ext']; + $dest_img = $to_img_dir . $post['tim'] . $post['ext']; + + copy($src_thumb, $dest_thumb); + copy($src_img, $dest_img); + } + + if (!empty($dup_pids)) { + $dup_clause = implode(',', $dup_pids); + $query = "UPDATE `$to_board` SET filedeleted = 1, ext = '' WHERE no IN($dup_clause)"; + $res = mysql_board_call($query); + } + + // Insert notification post if deletion is not requested + if (!$delete) { + $msg = sprintf(S_THREAD_MOVED, ">>>/$to_board/$new_resto"); + + $post_time = $_SERVER['REQUEST_TIME']; + $tim = generate_tim(); + + $query = "INSERT INTO `$board`(now,name,sub,com,host,pwd,filename,ext,w, +h,tn_w,tn_h, tim,time,last_modified,md5,fsize,resto,capcode, +4pass_id,tmd5,id) +VALUE (" . + "'" . date('m/d/y(D)H:i:s', $post_time) . "'," . + "'" . S_ANONAME . "'," . + "''," . + "'" . mysql_real_escape_string($msg) . "'," . + "'', '', '', '', 0, 0, 0, 0, '" . $tim . "'," . $post_time . "," . + $post_time . ", '',0," . $thread_id . ", 'mod', '', '', '')"; + + $res = mysql_board_call($query); + } + + // Ask the destination board to build the new thread + remote_rebuild_live_thread($to_board, $new_resto); + + // Delete source thread if deletion is requested + if ($delete) { + // id, pwd, imgonly, auto, die + delete_post($thread_id, 'trim', 0, 2, 1, 0); + } + // Archive source thread if archiving is enabled + else if (ENABLE_ARCHIVE) { + archive_thread($thread_id); + + if (!STATIC_REBUILD && ENABLE_JSON_THREADS) { + generate_board_archived_json(); + } + } + // Rebuild source thread and indexes if archiving is disabled + else { + updatelog($thread_id, 1); + } + + // Rebuild indexes + if (!STATIC_REBUILD) { + updatelog(0, 0); + } + + return array($to_board, $new_resto); +} + +function remote_rebuild_live_thread($board, $pid) { + $post = array( + 'mode' => 'rebuildadmin', + 'no' => $pid + ); + + rpc_start_request("https://sys.int/$board/imgboard.php", $post, $_COOKIE, true); + + return true; +} + +function archive_thread($thread_id) { + global $log; + + $thread_id = (int)$thread_id; + + $board = mysql_real_escape_string(BOARD_DIR); + + if (!$thread_id) { + return; + } + + // Regenerate the user ID before clearing the IP + $uid = ''; + + if (DISP_ID && DISP_ID_PER_THREAD && !DISP_ID_RANDOM) { + $th = null; + + if (!IS_REBUILDD && isset($log[$thread_id])) { + $th = $log[$thread_id]; + } + else { + $query = 'SELECT id, host FROM `' . BOARD_DIR . "` WHERE no = $thread_id"; + + $res = mysql_board_call($query); + + if ($res) { + $th = mysql_fetch_assoc($res); + } + } + + if ($th && $th['id'] !== '' && $th['host']) { + $uid = generate_uid($thread_id, $_SERVER['REQUEST_TIME'], $th['host']); + $uid = ", id = '" . mysql_real_escape_string($uid) . "'"; + } + } + + // Update the OP. "root" is used for archive pruning. + $query = <<(ID: $id)"; +} + +function emailencode( $str ) +{ + return str_replace( "%40", "@", rawurlencode( $str ) ); +} + +function renderPostHtml($no, $in_thread, $sorted_replies = null, $reply_count = null, $shown_replies = null, $is_archived = false) { + global $log, $board_flags_array; + + extract($log[$no]); + + $namestyle = ''; + + if( JANITOR_BOARD == 1 ) { + $namestyle = broomcloset_style( $name ); + $name = broomcloset_name( $name ); + } + + $mname = $name; + $mname_truncated = ''; + if( $capcode == 'none' && mb_strlen( $name ) > 30 ) { + $mname = explode( '', $name, 2 ); + if( mb_strlen( $mname[0] ) > 30 ) { + $mname[0] = htmlspecialchars_decode($mname[0], ENT_QUOTES); + $mname[0] = mb_substr( $mname[0], 0, 30 ) . '(...)'; + $mname[0] = htmlspecialchars($mname[0], ENT_QUOTES); + $mname_truncated = ' data-tip data-tip-cb="mShowFull"'; + } + + $mname = implode( '', $mname ); + } + + $hasCapcode = $capcode === 'none' ? '' : ' capcode'; + + // NEW CAPCODE STUFF + switch( $capcode ) { + case 'admin': + $capcodeStart = ' ## Admin'; + $capcode_class = ' capcodeAdmin'; + + $capcode = ' Admin Icon'; + $highlight = ''; + break; + + case 'founder': + $capcodeStart = ' ## Founder'; + $capcode_class = ' capcodeFounder'; + + $capcode = ' Founder Icon'; + $highlight = ''; + break; + + case 'admin_highlight': + $capcodeStart = ' ## Admin'; + $capcode_class = ' capcodeAdmin'; + + $capcode = ' Admin Icon'; + $highlight = ' highlightPost'; + break; + + case 'mod': + $capcodeStart = ' ## Mod'; + $capcode_class = ' capcodeMod'; + + $capcode = ' Mod Icon'; + $highlight = ''; + break; + + case 'developer': + $capcodeStart = ' ## Developer'; + $capcode_class = ' capcodeDeveloper'; + + $capcode = ' Developer Icon'; + $highlight = ''; + break; + + case 'manager': + $capcodeStart = ' ## Manager'; + $capcode_class = ' capcodeManager'; + + $capcode = ' Manager Icon'; + $highlight = ''; + break; + + case 'verified': + $capcodeStart = ' ## Verified'; + $capcode_class = ' capcodeVerified'; + + $capcode = ''; + $highlight = ''; + break; + + default: + $capcode = $capcodeStart = $highlight = $capcode_class = ''; + break; + } + + $spoiler = 0; + + if( strpos( $sub, 'SPOILER<>' ) === 0 ) { + $sub = substr( $sub, strlen( 'SPOILER<>' ) ); + $spoiler = 1; + } + + // Only OPs have subjects + if ($sorted_replies !== null) { + $subshortm = $sub; + if( mb_strlen( $sub ) > 30 ) { + $sub = str_replace( array(','), ',', $sub ); + $cutsub = htmlspecialchars_decode( $sub, ENT_QUOTES ); + $cutsub = mb_substr( $cutsub, 0, 30 ); + $cutsub = htmlspecialchars( $cutsub, ENT_QUOTES ); + + $subshortm = '' . $cutsub . '(...)'; + } + + $subshortm = '' . $subshortm . ' '; + $sub = '' . $sub . ' '; + } + else { + $sub = $subshortm = ''; + } + + $com = auto_link( $com, $in_thread ); + + if( !$in_thread ) { + list( $com, $abbreviated ) = abbreviate( $com, MAX_LINES_SHOWN ); + } + + if( isset( $abbreviated ) && $abbreviated ) { + $com .= '

    Comment too long. Click here to view the full text.'; + } + + // Image tag creation + $file = ''; + if( $ext ) { + $img = IMG_DIR . $tim . $ext; + $displaysrc = IMG_DIR2 . $tim . $ext; + + //if ($ext !== ".swf" && BOARD_DIR !== 'j') { + //if ($no % 100 >= 74) { + //$displaysrc = "//is2.4chan.org/" . BOARD_DIR . "/" . $tim . $ext; + //} + //} + + $linksrc = ( ( USE_SRC_CGI == 1 ) ? ( str_replace( '.cgi', '', IMG_DIR2 ) . $tim . $ext ) : $displaysrc ); + + if( defined( 'INTERSTITIAL_LINK' ) ) { + $linksrc = str_replace( INTERSTITIAL_LINK, '', $linksrc ); + } + + // Original filename truncation + $unescaped_filename = htmlspecialchars_decode($filename, ENT_QUOTES); + if( mb_strlen( $unescaped_filename, 'UTF-8' ) > 30 ) { + $shortname = mb_substr($unescaped_filename, 0, 25, 'UTF-8'); + $shortname = htmlspecialchars($shortname, ENT_QUOTES). '(...)' . $ext; + $longname = $filename . $ext; + $need_file_tooltip = true; + } + else { + $shortname = $longname = $filename . $ext; + $need_file_tooltip = false; + } + + if( THREAD_AD == 1 ) { + if( defined( 'THREAD_AD_TXT' ) && THREAD_AD_TXT ) { + $ad = text_link_ad( THREAD_AD_TXT ); + if( $ad ) + $dat .= "" . S_ADNAME . " : $ad
    "; + } + } + + $s_src = IMG_DIR . $tim . $ext; + + // 32>24 byte ascii>base64 conversion + $shortmd5 = base64_encode( pack( 'H*', $md5 ) ); + if( $fsize >= 1048576 ) { + $size = round( ( $fsize / 1048576 ), 2 ) . ' M'; + } + elseif( $fsize >= 1024 ) { + $size = round( $fsize / 1024 ) . ' K'; + } + else { + $size = $fsize . ' '; + } + + $ftype = strtoupper( substr( $ext, 1 ) ); + $mFileInfo = '
    ' . $size . 'B ' . $ftype . '
    '; + + if( !$tn_w && !$tn_h && $ext == '.gif' ) { + $tn_w = $w; + $tn_h = $h; + } + + $class = ''; + if( $spoiler ) { + $class = ' imgspoiler'; + // Replace 3 image tags with one, makes it easier to change in the future + $imgthumb_src = SPOILER_THUMB; + $tn_w = '100'; + $tn_h = '100'; + } + else { + //$imgthumb_src = thumb_url() . $tim . 's.jpg'; + $imgthumb_src = '//' . THUMB_DIR2_PART . $tim . 's.jpg'; + } + + if (MOBILE_IMG_RESIZE && $m_img) { + $m_img_attr = ' data-m'; + } + else { + $m_img_attr = ''; + } + + $imgsrc = '' . $size . 'B' . $mFileInfo . ''; + + if( $filedeleted ) { + $fileinfo = 'File deleted.'; + $imgsrc = ''; + } + else { + $dimensions = ( $ext == '.pdf' ) ? 'PDF' : $w . 'x' . $h; + if( !$spoiler ) { + $fileinfo = '
    ' . S_PICNAME . ': ' . $shortname . ' (' . $size . 'B, ' . $dimensions . ')
    '; + } + else { + $fileinfo = '
    ' . S_PICNAME . ': Spoiler Image (' . $size . 'B, ' . $dimensions . ')
    '; + } + } + + $file = << + $fileinfo + $imgsrc + +HTML; + } + + /** + * OP specific html + */ + if ($sorted_replies !== null) { + if ($in_thread) { + $href = ''; + $postinfo_extra = ''; + } + else { + $href = RES_DIR2 . $no . PHP_EXT2; + + if ($semantic_url !== '') { + $semantic_url = "/$semantic_url"; + } + + $postinfo_extra = '   [' . S_REPLY . ']'; + } + + $oldtext = ''; + $extra = ''; + + // Marked for deletion (old) + if (isset($log[$no]['old'])) { + $oldtext .= '' . S_OLD . '
    '; + } + + $postInfo = ''; + + // Count omitted replies and images + if (!$in_thread) { + $s = $reply_count - $shown_replies; + + if ($shown_replies) { + $t = 0; + $total_t = 0; + + $cur = 1; + + while ($s >= $cur) { + list($row) = each($sorted_replies); + + if ($log[$row]['fsize'] && !$log[$row]['filedeleted']) { + $t++; + } + + $cur++; + } + + $total_t = $t; + + while ($reply_count >= $cur) { + list($row) = each($sorted_replies); + + if ($log[$row]['fsize'] && !$log[$row]['filedeleted']) { + $total_t++; + } + + $cur++; + } + + if ($reply_count != 0) { + reset($sorted_replies); + } + } + else { + $total_t = $t = $imgreplycount; + } + + // desktop + $posts = ( $s < 2 ) ? ' reply' : ' replies'; + $replies = ( $t < 2 ) ? ' image' : ' images'; + + if (( $s > 0 ) && ( $t == 0 )) { + $extra .= '' . $s . $posts . ' omitted. Click here to view.'; + } + elseif (( $s > 0 ) && ( $t > 0 )) { + $extra .= '' . $s . $posts . ' and ' . $t . $replies . ' omitted. Click here to view.'; + } + + // mobile + $posts = ( $reply_count < 2 ) ? ' Reply' : ' Replies'; + $replies = ( $total_t < 2 ) ? ' Image' : ' Images'; + + if (( $reply_count > 0 ) && ( $total_t == 0 )) { + // Text replies only + $info = '' . $reply_count . $posts . ''; + } + elseif (( $reply_count > 0 ) && ( $total_t > 0 )) { + // Image replies + $info = '' . $reply_count . $posts . ' / ' . $total_t . $replies . ''; + } + else { + // nothing + $info = ''; + } + + $postInfo = << + + $info + + + View Thread + +HTML; + } + else { + $s = 0; + } + + // Sticky - Closed + $threadmodes = ''; + + if ($sticky == 1) { + $threadmodes .= ' Sticky'; + } + + if ($closed == 1) { + if ($archived) { + $threadmodes .= ' Archived'; + } + else { + $threadmodes .= ' Closed'; + } + } + + // Staff replies indicator + if (META_BOARD) { + $posts = meta_is_thread_flagged($sorted_replies); + + // admin = 0 + // dev = 1 + // mod = 2 + // manager = 3 + + // array (css_class, text_name) + $larr = array( + 0 => array('Admin', 'Administrator'), + 1 => array('Developer', 'Developer'), + 2 => array('Mod', 'Moderator'), + 3 => array('Manager', 'Manager') + ); + + if ($posts[0] || $posts[1] || $posts[2] || $posts[3]) { + $com .= '

    '; + + foreach ($posts as $key => $postlist) { + if (!$postlist) { + continue; + } + + $postlist = explode(',', $postlist); + + $replies = count($postlist) > 1 ? 'Replies' : 'Reply'; + $com .= '' . $larr[$key][1] . ' ' . $replies . ': '; + + foreach ($postlist as $postnum) { + $com .= '>>' . $postnum . ' '; + } + + $com .= '
    '; + } + + $com .= '
    '; + + } + } + + if (DISP_ID && DISP_ID_PER_THREAD && !$is_archived && !DISP_ID_RANDOM && $id !== '') { + $id = generate_uid($no, $time, $host); + } + + $reply_file = ''; + $op_file = $file; + $post_class = 'op'; + $sidearrows = ''; + } + /** + * Reply + */ + else { + $href = $in_thread ? '' : RES_DIR2 . $resto . PHP_EXT2; + $threadmodes = $postinfo_extra = $oldtext = $postInfo = $extra = $op_file = ''; + $reply_file = $file; + $post_class = 'reply'; + $sidearrows = '
    >>
    '; + + $sub = ''; + } + + $dispuid = ''; + $dispuid = display_uid( $id, $capcode ); + if( $dispuid == '' || $capcode != '' ) { + $dispuid = $capcode; + } + else { + $capcodeStart = ''; + if (FORCED_ANON) { + $capcode_class = ''; + } + } + + $countryFlag = ''; + if ($capcode == '') { + if (ENABLE_BOARD_FLAGS && $board_flag != '' && isset($board_flags_array[$board_flag])) { + $cname = board_flag_code_to_name($board_flag); + $countryFlag = ' '; + } + else if (SHOW_COUNTRY_FLAGS) { + $cname = country_code_to_name($country); + $countryFlag = ' '; + } + } + + $quote = $in_thread ? 'javascript:quote(\'' . $no . '\');' : $href . '#q' . $no; + + $postM = ''; + + // Forced anon on meta boards + if (META_BOARD && $capcode_class != ' capcodeAdmin') { + $name = $mname = S_ANONAME; + } + + if (FORCE_COM && !$capcode) { + $com = FORCE_COM_TEXT; + } + + $display_no = display_no($no); + + if ($since4pass && $capcode == '' && $since4pass < 10000) { + $since4passTag = " "; + } + else { + $since4passTag = ''; + } + + // April 2024 + if ($since4pass && $capcode == '' && $since4pass >= 10000) { + $since4passTag = april_2024_get_name_badge($since4pass); + $_xa24_post_cls = april_2024_get_post_cls($since4pass); + } + else { + $_xa24_post_cls = ''; + } + + return <<$sidearrows +
    + + + $op_file + + + $reply_file +
    $com
    +
    + $postInfo + $oldtext + + $extra +HTML; +} + +// deletes a post from the database +// imgonly: whether to just delete the file or to delete from the database as well +// automatic: always delete regardless of password/admin (for self-pruning) +// children: whether to delete just the parent post of a thread or also delete the children +// die: whether to die on error +// careful, setting children to 0 could leave orphaned posts. +function delete_post($resno, $pwd, $imgonly = 0, $automatic = 0, $children = 1, $die = 1, $lazy_rebuild = false, $archived_deletion = false, $tool = null, $user_is_known = true) +{ + global $log; + + $resno = intval( $resno ); + log_cache( 0, $resno, $archived_deletion ? 1 : 0 ); + + $post_exists = true; + + if (!isset( $log[$resno])) { + if ($die) { + //error( "Can't find the post $resno." ); + updating_index(); + die(); + } + else { + if ($automatic) { + return 0; + } + + $post_exists = false; + } + } + + $row = $log[$resno]; + + if (!$automatic && !has_level('janitor')) { + $cant_del = $cant_del_old = false; + + if ($row['resto']) { + $cant_del = NO_DELETE_REPLY; + } + else { + $cant_del = NO_DELETE_OP; + } + + if ($_SERVER['REQUEST_TIME'] - $log[$resno]['time'] >= RENZOKU_DEL_CANT_AFTER) { + $cant_del = true; + $cant_del_old = true; + } + + if ($cant_del) { + error($cant_del_old ? S_RENZOKU_DEL_CANT_AFTER : S_MAYNOTDEL); + } + } + + // if (!$row['pwd'] && BOARD_DIR=='c') { + // echo ""; + // } + + if ($archived_deletion) { + // Only authed users can delete archived posts + $pass_ok = false; + $host_ok = false; + } + else { + $pass_ok = $pwd && $pwd === $row['pwd']; + $host_ok = $row['host'] == $_SERVER['REMOTE_ADDR']; + } + + $admin_ok = has_level() || can_delete( $resno ); + $can_flood_delete = has_level( 'janitor' ); + + $can_delete = $automatic || $pass_ok || $host_ok || $admin_ok; + + // quick_log_to( "/www/perhost/del.log", date( "r" ) . " deletion of #$resno by " . $_SERVER['REMOTE_ADDR'] . " pwd \"$pwd\" auto " . (int)$automatic . " adminok " . (int)$admin_ok . " hostok " . (int)$host_ok . " passok " . (int)$pass_ok . " orig ip " . $row['host'] . " pwd " . $row['pwd'] . " com " . substr( $row["com"], 0, 50 ) . " " . ( $can_delete ? "succeeded" : "failed" )."\n" ); + + if( !$can_delete ) error( S_BADDELPASS ); + + if( $row['sticky'] ) { + if( has_level() && !has_level( 'admin' ) && !$automatic && !$archived_deletion) error( S_MAYNOTDELSTICKY ); + if( !has_level() ) error( S_MAYNOTDEL ); + } + + if( BOARD_DIR == 'vg' && !$row['resto'] && !$admin_ok && !$archived_deletion ) error( S_MAYNOTDEL ); + + if( !$row['resto'] && !$automatic && !$admin_ok ) { + foreach( $row['children'] as $child => $unused ) { + if( $log[$child]['capcode'] != 'none' ) error( S_MAYNOTDEL ); + } + } + + if( !$automatic && !$admin_ok && !$can_flood_delete ) { + if ($user_is_known) { + $_renzoku_del = RENZOKU_DEL; + } + else { + $_renzoku_del = 600; // FIXME: 10 minutes + } + if( ( time() - (int)$row['time'] ) < $_renzoku_del ) { + error(S_RENZOKU_DEL); + } + } + + // User is authed staff + if ($admin_ok && !IS_REBUILDD) { + $auser = $_COOKIE['4chan_auser']; + + // Use POSTed IP instead of the local one if the deletion was triggered via RPC + if (isset($_POST['remote_addr']) && is_local()) { + $remote_addr = $_POST['remote_addr']; + } + else { + $remote_addr = $_SERVER['REMOTE_ADDR']; + } + + // Authed user is deleting a post that isn't his + if (!$pass_ok && !$host_ok) { + $adfsize = ( $row['fsize'] > 0 ) ? 1 : 0; + $adname = str_replace( ' !', '#', $row['name'] ); + if( $imgonly ) { + $imgonly = 1; + } else { + $imgonly = 0; + } + + if (isset($_POST['template_id'])) { + $template_id = (int)$_POST['template_id']; + } + else { + $template_id = 0; + } + + if ($post_exists && (!$automatic || $automatic === 2)) { + validate_admin_cookies(); + + if (!$tool || !in_array($tool, array('search', 'ban', 'ban-req', 'autopurge', 'threadban'))) { + $tool = ''; + } + + mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,admin_ip,template_id,tool) values('%s',%d, %d,'%s','%s','%s','%s','%s','%s','%s', '%s', %d, '%s')", $imgonly, $resno, $row['resto'], SQLLOG, $adname, $row["sub"], $row["com"], $adfsize, $row["filename"].$row["ext"], $auser, $remote_addr, $template_id, $tool ); + } + + // Clear the report queue if only the file is deleted + if ($imgonly) { + $query = "DELETE FROM reports WHERE board = '" . SQLLOG . "' AND no = " . (int)$resno; + + $res = mysql_global_call($query); + + $query = "DELETE FROM reports_for_posts WHERE board = '" . SQLLOG . "' AND postid = " . (int)$resno; + + $res = mysql_global_call($query); + } + } + // Staff member is deleting a post that is his, or other type of deletions + else if (!$automatic || $automatic === 2) { + if ($auser) { + log_staff_event('staff_self_del', $auser, $remote_addr, $pwd, BOARD_DIR, $row); + } + else { + log_staff_event('staff_auto_del', 'Auto-ban', $remote_addr, $pwd, BOARD_DIR, $row); + } + } + } + + $delete_children = $row['resto'] == 0 && $children && !$imgonly; + + $restoq = $delete_children ? "OR (archived = 0 and resto=$resno) OR (archived = 1 and resto=$resno)" : ''; + + if (UPLOAD_BOARD) { + $up_col = ',filename'; + } + else { + $up_col = ''; + } + + if (MOBILE_IMG_RESIZE) { + $up_col .= ',m_img'; + } + + $result = mysql_board_call( "select no,resto,tim,ext$up_col from `" . SQLLOG . "` where no=$resno $restoq" ); + + // Array of threads to update after one or more replies were deleted. + $updated_threads = array(); + // Array of post number for report and xff clearing + $deleted_threads = array(); + $deleted_replies = array(); + + $purge_files = array(); + + $img_webroot = 'http://i.4cdn.org/' . BOARD_DIR . '/'; + + while( $delrow = mysql_fetch_array( $result ) ) { + // delete + if( $delrow['ext'] ) { + if (UPLOAD_BOARD) { + $delfile = IMG_DIR . $delrow['filename'] . $delrow['ext']; //path to delete + @unlink($delfile); // delete image + if( CLOUDFLARE_PURGE_ON_DEL ) { + cloudflare_purge_url($img_webroot . rawurlencode($delrow['filename']) . $delrow['ext'], true); + } + } + else { + $delfile = IMG_DIR . $delrow['tim'] . $delrow['ext']; //path to delete + $delthumb = THUMB_DIR . $delrow['tim'] . 's.jpg'; + @unlink( $delfile ); // delete image + @unlink( $delthumb ); // delete thumb + + if (ENABLE_OEKAKI_REPLAYS && file_exists(IMG_DIR . $delrow['tim'] . '.tgkr')) { + unlink(IMG_DIR . $delrow['tim'] . '.tgkr'); + + if (CLOUDFLARE_PURGE_ON_DEL) { + $purge_files[] = $img_webroot . $delrow['tim'] . '.tgkr'; + } + } + + if (MOBILE_IMG_RESIZE && $delrow['m_img']) { + @unlink(IMG_DIR . $delrow['tim'] . 'm.jpg'); // delete mobile + } + + if (CLOUDFLARE_PURGE_ON_DEL) { + $purge_files[] = $img_webroot . $delrow['tim'] . $delrow['ext']; + $purge_files[] = $img_webroot . $delrow['tim'] . 's.jpg'; + + if (MOBILE_IMG_RESIZE && $delrow['m_img']) { + $purge_files[] = $img_webroot . $delrow['tim'] . 'm.jpg'; + } + + } + } + } + if( $imgonly ) { + mysql_board_call( "UPDATE `" . SQLLOG . "` SET filedeleted=1,root=root,last_modified=%d WHERE no=%d", $_SERVER['REQUEST_TIME'], $delrow['no'] ); + $log[$delrow['no']]['filedeleted'] = TRUE; + + if ($delrow['resto']) { + mysql_board_call( "UPDATE `" . SQLLOG . "` SET root=root,last_modified=%d WHERE no=%d", $_SERVER['REQUEST_TIME'], $delrow['resto'] ); + if (isset($log[$delrow['resto']])) + $log[$delrow['resto']]['last_modified'] = (int)$_SERVER['REQUEST_TIME']; + } + + //cloudflare_purge_by_basename(BOARD_DIR, $delrow['tim'] . $delrow['ext']); + } + else { + // Thread + if (!$delrow['resto']) { + $thread_key = @array_search( $delrow['no'], $log['THREADS'] ); + if( $thread_key !== false ) { + unset( $log['THREADS'][$thread_key] ); + } + + if( USE_GZIP == 1 ) { + @unlink( RES_DIR . $delrow['no'] . PHP_EXT . '.gz' ); + @unlink( RES_DIR . $delrow['no'] . '.json.gz' ); + } + else { + @unlink( RES_DIR . $delrow['no'] . PHP_EXT ); + @unlink( RES_DIR . $delrow['no'] . '.json' ); + } + + update_json_tail_deletion($delrow['no'], true); + + $deleted_threads[] = (int)$delrow['no']; + } + // Reply. Thread's last_modified field will need to be updated + else if (!isset($updated_threads[$delrow['resto']])) { + $updated_threads[$delrow['resto']] = true; + + $deleted_replies[] = (int)$delrow['no']; + } + + unset( $log[$delrow['no']] ); + } + } + + if (!empty($purge_files)) { + cloudflare_purge_url($purge_files, true); + } + + // Updating last_modified field (threads) + foreach ($updated_threads as $thread_id => $true) { + mysql_board_call("UPDATE `".SQLLOG."` set root=root,last_modified=%d where no=%d", $_SERVER['REQUEST_TIME'], $thread_id); + + if (isset($log[$thread_id])) + $log[$thread_id]['last_modified'] = (int)$_SERVER['REQUEST_TIME']; + + unset( $log[$thread_id]['children'][$delrow['no']] ); + } + + // Clearing reports and xff + if ($deleted_replies) { + $in_clause = 'IN(' . implode(',', $deleted_replies) . ')'; + mysql_global_do("DELETE FROM reports WHERE board='" . BOARD_DIR . "' AND no " . $in_clause); + mysql_global_do("DELETE FROM reports_for_posts WHERE board='" . BOARD_DIR . "' AND postid " . $in_clause); + + if (SAVE_XFF) { + mysql_global_do("UPDATE xff SET is_live = 0 WHERE board='" . BOARD_DIR . "' AND postno " . $in_clause); + } + } + + if ($deleted_threads) { + $in_clause = 'IN(' . implode(',', $deleted_threads) . ')'; + mysql_global_do("DELETE FROM reports WHERE board='" . BOARD_DIR . "' AND (no $in_clause OR resto $in_clause)"); + mysql_global_do("DELETE FROM reports_for_posts WHERE board='" . BOARD_DIR . "' AND (postid $in_clause OR threadid $in_clause)"); + + if (SAVE_XFF) { + mysql_global_do("UPDATE xff SET is_live = 0 WHERE board='" . BOARD_DIR . "' AND postno " . $in_clause); + } + } + + // Halloween 2017 + /* + if ($tool && defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky2017') { + if ($tool === 'ban') { + decrease_halloween_score($resno); + } + else if ($tool === 'ban-req') { + decrease_halloween_score($resno, 0.90); + } + } + */ + + //delete from DB + if( $delete_children ) // delete thread and children + $result = mysql_board_call( "delete from `" . SQLLOG . "` where no=$resno or resto=$resno" ); + elseif( !$imgonly ) // just delete the post + $result = mysql_board_call( "delete from `" . SQLLOG . "` where no=$resno" ); + + rpc_task(); + if( $imgonly && $row['resto'] == 0 ) { + return $resno; // return thread number to stop deletion silliness + } + + return $row['resto']; // so the caller can know what pages need to be rebuilt +} + +function rebuild_deletions($rebuild, $lazy_rebuild = false) +{ + global $log; + + foreach( $rebuild as $key => $val ) { + log_cache( 0, $key ); + if (!isset($log[$key]['children'])) { + internal_error_log("rebuild_deletions", "missing children for OP /" . BOARD_DIR . "/$key"); + continue; + } + updatelog( $key, 1 ); // leaving the second parameter as 0 rebuilds the index each time! + update_json_tail_deletion($key); + } + + if( STATIC_REBUILD ) return; + + updatelog(0, 0, $lazy_rebuild); // update the index page last +} + +/** + * Removes old archived posts + */ +function trim_archive() { + if (STATIC_REBUILD && !IS_REBUILDD) { + return; + } + + if (!ARCHIVE_MAX_AGE) { + return; + } + + $interval = (int)ARCHIVE_MAX_AGE; + + $query = << 0 ) ? ( PAGE_MAX * DEF_PAGES ) : 0; + + $threads = array(); + + $rebuild_archive_list = false; + + $rebuild_archive_json = false; + + if (ENABLE_ARCHIVE) { + if (IS_REBUILDD) { + clearstatcache(true, INDEX_DIR . 'archive' . PHP_EXT . '.gz'); + } + + if (filemtime(INDEX_DIR . 'archive' . PHP_EXT . '.gz') < time() - (int)ARCHIVE_REBUILD_DELAY) { + $rebuild_archive_list = true; + } + } + + // New max-page method + if( $maxthreads ) { + $exp_order = 'no'; + if( EXPIRE_NEGLECTED == 1 ) $exp_order = 'root'; + //logtime( 'trim_db before select threads' ); + $result = mysql_board_call( "SELECT no FROM `" . SQLLOG . "` WHERE archived=0 AND sticky=0 AND undead=0 AND resto=0 ORDER BY $exp_order ASC" ); + //logtime( 'trim_db after select threads' ); + $threadcount = mysql_num_rows( $result ); + + if (!$threadcount && $rebuild_archive_list) { + $rebuild_archive_list = false; + } + + while( $row = mysql_fetch_array( $result ) and $threadcount > $maxthreads ) { + if (ENABLE_ARCHIVE) { + $rebuild_archive_json = true; + archive_thread($row['no']); + } + else { + delete_post( $row['no'], 'trim', 0, 1 ); // imgonly=0, automatic=1, children=1 + } + $threads[$row['no']] = 1; + $threadcount--; + } + + mysql_free_result( $result ); + + if (ENABLE_ARCHIVE) { + if ($rebuild_archive_list) { + rebuild_archive_list(); + } + + if ($rebuild_archive_json && ENABLE_JSON_THREADS) { + generate_board_archived_json(); + } + } + + // Original max-posts method (note: cleans orphaned posts later than parent posts) + } else { + // make list of stickies + $stickies = array(); // keys are stickied thread numbers + $undead = array(); + // COMBINE FOR MAXIMUM EFFICIENCY! + $result = mysql_board_call( "SELECT no from `" . SQLLOG . "` where (sticky=1 OR undead=1) and resto=0" ); + while( $row = mysql_fetch_array( $result ) ) { + if( $row['sticky'] ) $stickies[$row['no']] = 1; + if( $row['undead'] ) $undead[$row['no']] = 1; + } + + // FIXME these if ... continue checks need to be SQL conditions! + $result = mysql_board_call( "SELECT no,resto,sticky FROM `" . SQLLOG . "` ORDER BY no ASC" ); + $postcount = mysql_num_rows( $result ); + while( $row = mysql_fetch_array( $result ) and $postcount >= $maxposts ) { + // don't delete if this is a sticky thread or is undeletable + if( $row['sticky'] == 1 || $row['undead'] == 1 ) continue; + // don't delete if this is a REPLY to a sticky or is in an undeletable thread + if( $row['resto'] != 0 && ( $stickies[$row['resto']] == 1 || $undead[$row['resto']] == 1 ) ) continue; + delete_post( $row['no'], 'trim', 0, 1, 0 ); // imgonly=0, automatic=1, children=0 + $threads[$row['no']] = 1; + $postcount--; + } + mysql_free_result( $result ); + } +} + +// FIXME archives +// debug function, deletes all archived threads +function purge_archive() { + $query = "SELECT no FROM `test` WHERE archived = 1 AND resto = 0"; + + $res = mysql_board_call($query); + + if (!$res) { + return; + } + + while ($thread = mysql_fetch_assoc($res)) { + echo "Deleting {$thread['no']}
    "; + delete_post((int)$thread['no'], '', 0, 0, 1, true, false, true); + } +} + +function rebuild_archived_thread($thread_id) { + global $log; + + log_cache(0, $thread_id, 1); + + if (!isset($log[$thread_id])) { + return false; + } + + // Build the JSON + if (ENABLE_JSON) { + $tailSize = get_json_tail_size($thread_id); + + if ($tailSize) { + generate_thread_json($thread_id, false, false, false, $tailSize); + } + else { + update_json_tail_deletion($thread_id); + } + + generate_thread_json($thread_id); + } + + // Build the HTML + $dat = ''; + + head($dat, $thread_id); + form($dat, $thread_id); + + $dat .= '
    +
    +
    +'; + + $reply_count = $log[$thread_id]['replycount']; + + // Open thread tag and render OP + $sorted_replies = $log[$thread_id]['children']; + ksort($sorted_replies); + + $dat .= '
    ' + . renderPostHtml($thread_id, $thread_id, $sorted_replies, $reply_count, null, true); + + // Render replies + $repCount = 0; + + while (list($resrow) = each($sorted_replies)) { + if (!$log[$resrow]['no']) { + break; + } + + $dat .= renderPostHtml($resrow, $thread_id, null, null, null, true); + + $repCount++; + } + + // Close thread tag + $dat .= ' +
    +
    +'; + + $dat .= '
    '; + + // Close board tag + $lang = S_FORM_REPLY; + + $dat .= ' + +
    '; + + $dat .= '
    '; + + /** + * ADS + */ + + if (defined('AD_ADGLARE_BOTTOM') && AD_ADGLARE_BOTTOM) { + $dat .= '

    '; + } + + if (defined('AD_ADGLARE_BOTTOM_MOBILE') && AD_ADGLARE_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + if (defined('AD_RC_BOTTOM') && AD_RC_BOTTOM) { + $dat .= '

    '; + } + + if (defined('AD_BSA_BOTTOM') && AD_BSA_BOTTOM) { + $dat .= AD_BSA_BOTTOM; + } + + if (defined('AD_RC_BOTTOM_MOBILE') && AD_RC_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + if (defined('AD_ADNIUM_BOTTOM_MOBILE') && AD_ADNIUM_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) { + $dat .= '

    '; + } + /* + if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM) { + $dat .= '
    '; + } + else if (defined('AD_TERRA_BOTTOM_DESKTOP') && AD_TERRA_BOTTOM_DESKTOP) { + $_ad_info = explode(',', AD_TERRA_BOTTOM_DESKTOP); + $dat .= '
    '; + } + */ + if (defined('ADS_DANBO') && ADS_DANBO) { + $dat .= '

    '; + } + if (defined('AD_CUSTOM_BOTTOM') && AD_CUSTOM_BOTTOM) { + $dat .= '
    ' . AD_CUSTOM_BOTTOM . '
    '; + } + + $resredir = ''; + + // deletion mode is "arcdel" instead of "usrdel" + $dat .= '
    ' + . S_REPDEL . $resredir . ' [' + . S_DELPICONLY . '] '; + + if (!defined('CSS_FORCE')) { + $dat .= 'Style: + + '; + } + + $dat .= '
    '; + + foot($dat); + + // Write the page + print_page(RES_DIR . $thread_id . PHP_EXT, $dat); +} + +function calculate_indexes_to_rebuild( $updated_thread ) +{ + global $index_rbl; + $query = mysql_board_call( "SELECT COUNT(no) FROM `%s` WHERE archived = 0 AND root > (SELECT root FROM `%s` WHERE no=%d)", SQLLOG, SQLLOG, $updated_thread ); + $index_rbl = floor( mysql_result( $query, 0, 0 ) / DEF_PAGES ); +} + +function rebuild_indexes_daemon() +{ + global $index_rbl, $index_last_thread, $index_last_post, $log; + static $index_arr = array(); + + $index_rbl = PAGE_MAX; + + // Get latest thread + $query = mysql_board_call( "SELECT max(no) last_post, max(resto) last_thread FROM `%s` WHERE archived = 0", SQLLOG ); + $q = mysql_fetch_assoc( $query ); + + $latest_thread = $q['last_thread']; + $latest_post = $q['last_post']; + + if( $index_last_thread != $latest_thread ) { + // cry :( + $index_last_thread = $latest_thread; + $index_last_post = $latest_post; + + updatelog(); + + if (ENABLE_JSON_THREADS && ENABLE_ARCHIVE) { + generate_board_archived_json(); + } + + return; + } + + if( $index_last_post != $latest_post ) { + $index_last_thread = $latest_thread; + $index_last_post = $latest_post; + + $post_arr = $log['THREADS']; + + // Now we know we're not going to have identical arrays. + $count = count( $post_arr ); + $last_seen_thread = 0; + + for( $i = 0; $i < $count; $i++ ) { + if( $post_arr[$i] != $index_arr[$i] ) $last_seen_thread = $i; + } + + $index_rbl = floor( $last_seen_thread / DEF_PAGES ); + $index_arr = $post_arr; + + updatelog(); + + return; + } + + // YAY NOTHING TO UPDATE! + return; +} + +function style_group() +{ + return ( DEFAULT_BURICHAN == 1 ) ? "ws_style" : "nws_style"; +} + +function rebuildd_stats() +{ + if (!IS_REBUILDD) return ""; + + global $update_avg_secs; + global $rpc_chs; + + $avgtime = $update_avg_secs; + + $memuse = (int)(memory_get_usage(true) / 1024); + $peakmemuse = (int)(memory_get_peak_usage(true) / 1024); + $rpccount = count($rpc_chs); + + return ""; +} + +// Changes relative board urls to absolute //board.4chan.org urls +// mostly for /j/ and error pages on sys.4chan +function fix_board_nav($nav, $fix_protocol = false) { + if ($fix_protocol) { + $protocol = (stripos($_SERVER["HTTP_REFERER"], "https") === 0) ? 'https:' : 'http:'; + } + else { + $protocol = ''; + } + + return preg_replace('/href="\/([a-z0-9]+)\/"/', "href=\"$protocol//boards." . L::d(BOARD_DIR) . "/$1/\"", $nav); +} + +// Same but for /archive lmao +function fix_board_nav_archive($nav) { + $nav = preg_replace('/href="\/([a-z0-9]+)\/"/', 'href="/$1/archive"', $nav); + $nav = preg_replace('/href="\/f\/archive"/', 'href="/f/"', $nav); + $nav = preg_replace('/href="\/b\/archive"/', 'href="/b/"', $nav); + + return $nav; +} + +function head( &$dat, $res, $error = 0, $page = 0, $npages = 0, $is_arclist = false ) +{ + //( $dat, 0, 0, 0, 0, true ) + global $log, $thread_unique_ips; + + $titlepart = $rta = $favicon = $css = $rss = $subtitle = $extra = ''; + + $includenav = file_get_contents_cached(NAV_TXT); + + if( JANITOR_BOARD == 1 ) { + $dat .= broomcloset_head( $dat ); + $includenav = fix_board_nav($includenav); + } + else if ($error) { + $includenav = fix_board_nav($includenav, true); + } + else if ($is_arclist) { + $includenav = fix_board_nav_archive($includenav); + } + + if( TITLE_IMAGE_TYPE == 1 ) { + $titleimg = rand_from_flatfile( YOTSUBA_DIR, 'title_banners.txt' ); + //$titleimg = STATIC_SERVER . 'image/title/' . $titleimg; + + $titlepart .= '
    '; + } elseif( TITLE_IMAGE_TYPE == 2 ) { + $titlepart .= ''; + } + + if( defined( 'SUBTITLE' ) ) { + $subtitle = '
    ' . SUBTITLE . '
    '; + } + + // CSS Workings + $cssVersion = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION; + $defaultcss = ( DEFAULT_BURICHAN == 1 ) ? 'yotsubluenew' : 'yotsubanew'; + $mobilecss = ( ( DEFAULT_BURICHAN == 1 ) ? 'yotsublue' : 'yotsuba' ) . 'mobile.' . $cssVersion . '.css'; + + $styles = array( + 'Yotsuba New' => "yotsubanew.$cssVersion.css", + //'Yotsuba' => "yotsuba.$cssVersion.css", + 'Yotsuba B New' => "yotsubluenew.$cssVersion.css", + 'Futaba New' => "futabanew.$cssVersion.css", + 'Burichan New' => "burichannew.$cssVersion.css", + 'Photon' => "photon.$cssVersion.css", + 'Tomorrow' => "tomorrow.$cssVersion.css" + ); + + // /j/ versioning fix + if( BOARD_DIR == 'j' ) { + $css = ''; + $extra = << + document.addEventListener('mousedown', function(e) { + var t = e.target; + if (t === document) { + return; + } + if (/^>>>\//.test(t.textContent) && /sys\.4chan/.test(t.href)) { + t.href = t.href.replace('sys.4chan', 'boards.4chan'); + } + }, false); + +JJS; + } else { + if( defined( 'CSS_FORCE' ) ) { + foreach( $styles as $style => $stylecss ) { + $rel = ( $style == 'Yotsuba New' ) ? 'stylesheet' : 'alternate stylesheet'; + $css .= ''; + } + } + else { + $dcssl = $defaultcss . '.' . $cssVersion . '.css'; + $css .= ''; + foreach( $styles as $style => $stylecss ) { + $css .= ''; + } + + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) { + $css .= ''; + } + } + + // Christmas 2021 + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'tomorrow') { + $extra = << + + +JJS; + } + + // Halloween spooky.css + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky') { + $extra = << + function fc_skelrot(e) { + var el, idx, thres, max; + if (e && e.detail && e.detail.count) { + thres = 0.33; + } + else { + thres = 0.0; + } + max = 23; + if (Math.random() < thres) { + return; + } + if (el = document.getElementById('skellington')) { + el.parentNode.removeChild(el); + } + idx = 1 + Math.floor(Math.random() * max); + el = document.createElement('img'); + el.id = 'skellington'; + el.className = 'desktop' + (Math.random() < 0.25 ? ' topskel' : ''); + el.alt = ''; + if (Math.random() < 0.01) { + el.src = '//s.4cdn.org/image/temp/dinosaur.gif'; + } + else { + el.src = '//s.4cdn.org/image/skeletons/' + idx + '.gif'; + } + document.body.insertBefore(el, document.body.firstElementChild); + } + function fc_spooky_init() { + if (window.matchMedia && window.matchMedia('(min-width: 481px)').matches) { + document.addEventListener('4chanThreadUpdated', fc_skelrot, false); + window.dark_captcha = true; + fc_skelrot(); + } + } + function fc_spooky_cleanup() { + var el = document.getElementById('skellington'); + window.dark_captcha = false; + document.removeEventListener('4chanThreadUpdated', fc_skelrot, false); + el && el.parentNode.removeChild(el); + } + +JJS; + } + } + + $css .= ''; + + // April 2024 + $css .= ''; + + if (SHOW_COUNTRY_FLAGS) { + $css .= ''; + } + + if (ENABLE_BOARD_FLAGS) { + $_flags_type = (defined('BOARD_FLAGS_TYPE') && BOARD_FLAGS_TYPE) ? BOARD_FLAGS_TYPE : BOARD_DIR; + $css .= ''; + } + + if( CODE_TAGS ) { + $css .= ''; + } + + // Various optional tags + if( USE_RSS == 1 ) { + $rss = ''; + } + + if( RTA == 1 ) { + $rta = ''; + } + + if( defined( 'FAVICON' ) ) { + $favicon = ''; + } + + $thread_unique_ips = 0; + $jsUniqueIps = ''; + + if (SHOW_THREAD_UNIQUES) { + if ($res) { + $thread_unique_ips = get_unique_ip_count($res); + } + + if ($thread_unique_ips) { + $jsUniqueIps = 'var unique_ips = ' . $thread_unique_ips . ';'; + } + } + + // js tags + $jsVersion = TEST_BOARD ? JS_VERSION_TEST : JS_VERSION; + $comLen = MAX_COM_CHARS; + $styleGroup = style_group(); + $maxFilesize = MAX_KB * 1024; + $maxLines = MAX_LINES; + $jsCooldowns = json_encode(array( + 'thread' => RENZOKU3, + 'reply' => RENZOKU, + 'image' => RENZOKU2 + )); + + $tailSizeJs = ''; + + if ($res) { + $tailSize = get_json_tail_size($res); + + if ($tailSize) { + $tailSizeJs = ",tailSize = $tailSize"; + } + } + + $title = TITLE; + + $scriptjs = << +var style_group = "$styleGroup", +cssVersion = $cssVersion, +jsVersion = $jsVersion, +comlen = $comLen, +maxFilesize = $maxFilesize, +maxLines = $maxLines, +clickable_ids = 1, +cooldowns = $jsCooldowns +$tailSizeJs; +$jsUniqueIps +JS; + + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) { + $scriptjs .= 'var css_event = "' . CSS_EVENT_NAME . '";'; + + if (defined('CSS_EVENT_VERSION')) { + $_css_event_version = (int)CSS_EVENT_VERSION; + } + else { + $_css_event_version = 1; + } + + $scriptjs .= 'var css_event_v = ' . $_css_event_version . ';'; + } + + if ((int)MAX_WEBM_FILESIZE < (int)MAX_KB) { + $scriptjs .= 'var maxWebmFilesize = ' . (MAX_WEBM_FILESIZE * 1024) . ';'; + } + + $_is_archived = false; + + if (ENABLE_ARCHIVE) { + $scriptjs .= 'var board_archived = true;'; + + if ($res && $log[$res]['archived']) { + $scriptjs .= 'var thread_archived = true;'; + $_is_archived = true; + } + } + + if (DISP_ID) { + $scriptjs .= 'var user_ids = true;'; + } + + if (JSMATH) { + $scriptjs .= 'var math_tags = true;'; + } + + if (SJIS_TAGS) { + $scriptjs .= 'var sjis_tags = true;'; + } + + if (SPOILERS) { + $scriptjs .= 'var spoilers = true;'; + } + + if (CAPTCHA_TWISTER) { + $scriptjs .= 'var t_captcha = true;'; + } + + if( $res && $log[$res]['bumplimit'] ) { + $scriptjs .= 'var bumplimit = 1;'; + } + + if( $res && $log[$res]['imagelimit'] ) { + $scriptjs .= 'var imagelimit = 1;'; + } + + if( AD_PLEA ) $scriptjs .= 'var check_for_block = ' . (int)AD_PLEA . ';'; + + if( $error ) $scriptjs .= 'is_error = "true";'; + + // Danbo ads + if (defined('ADS_DANBO') && ADS_DANBO) { + if (DEFAULT_BURICHAN) { + $scriptjs .= "var danbo_rating = '__SFW__';"; + } + else { + $scriptjs .= "var danbo_rating = '__NSFW__';"; + } + + // Set up fallbacks + $_danbo_fallbacks = []; + + if (defined('AD_BIDGEAR_TOP') && AD_BIDGEAR_TOP) { + $_danbo_fallbacks['t_bg'] = AD_BIDGEAR_TOP; + } + else { + if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + $_danbo_fallbacks['t_abc_d'] = AD_ABC_TOP_DESKTOP; + } + + if (defined('AD_ABC_TOP_MOBILE') && AD_ABC_TOP_MOBILE) { + $_danbo_fallbacks['t_abc_m'] = AD_ABC_TOP_MOBILE; + } + } + + if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM) { + $_danbo_fallbacks['b_bg'] = AD_BIDGEAR_BOTTOM; + } + else if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) { + $_danbo_fallbacks['b_abc_m'] = AD_ABC_BOTTOM_MOBILE; + } + + if (!$_danbo_fallbacks) { + $_danbo_fallbacks = 'null'; + } + else { + $_danbo_fallbacks = json_encode($_danbo_fallbacks); + } + + $scriptjs .= 'var danbo_fb = ' . $_danbo_fallbacks . ';'; + + // Tag closed further below + $scriptjs .= ''; + + // PubFuture + if (DEFAULT_BURICHAN) { + $scriptjs .= ''; + } + + $testjs = ( TEST_BOARD ) ? 'test/core-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'core.min.' . JS_VERSION_CORE . '.js'; + $testextra = ( TEST_BOARD ) ? 'test/extension-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'extension.min.' . JS_VERSION_EXT . '.js'; + + $scriptjs .= ''; + + if( !$error ) $scriptjs .= ''; + + // April 2022 + //$scriptjs .= ''; + + if (TEST_BOARD) { + $stylejs = ''; + } + else { + $stylejs = ''; + } + + if (ENABLE_PAINTERJS && $_GET['mode'] != 'oe_finish') { + if (TEST_BOARD) { + $scriptjs .= ''; + $css .= ''; + } + else { + $scriptjs .= ''; + $css .= ''; + } + } + /* + if (!$is_arclist && defined('CSS_MATERIAL') && CSS_MATERIAL) { + $css .= ''; + } + */ + if( !$res ) { + $prev = ( $page - DEF_PAGES ) / DEF_PAGES; + $next = ( $page + DEF_PAGES ) / DEF_PAGES; + + if( $prev == 0 ) { + $prev_link = SELF_PATH2; + } else if( $prev > 0 ) { + $prev_link = $prev . PHP_EXT2; + } + + // maybe >= ? + if( ( $npages - $page ) > DEF_PAGES ) { + $next_link = $next . PHP_EXT2; + } + } + + if ($is_arclist) { + $canonical = ''; + } + else if (!$res) { + if ($page > 0) { + $canonical = ''; + } + else { + $canonical = ''; + } + } + elseif ($res) { + $href_context = $log[$res]['semantic_url']; + + if ($href_context !== '') { + $href_context = "/$href_context"; + } + + $canonical = ''; + } + else { + $canonical = ''; + } + + $clean_title = strip_tags(TITLE); + + if ($res) { + $page_metatags = generate_page_metatags($log[$res]['sub'], $log[$res]['com']); + + if ($page_metatags) { + $page_description = $page_metatags[0] . ' - ' . META_DESCRIPTION; + $page_keywords = META_KEYWORDS . $page_metatags[1]; + } + else { + $page_description = META_DESCRIPTION; + $page_keywords = META_KEYWORDS; + } + + $page_title = generate_page_title($res, $log[$res]['sub'], $log[$res]['com']) + . ' - ' . preg_replace('/^[\[\/][a-z0-9]+[\]\/] - /i', '', $clean_title); + } + else { + $page_description = META_DESCRIPTION; + $page_keywords = META_KEYWORDS; + + $page_title = $clean_title; + + if ($is_arclist) { + $page_title .= ' - Archive'; + } + else if ($page > 0) { + $page_title .= ' - Page ' . (($page / DEF_PAGES) + 1); + } + } + + $page_title .= ' - 4chan'; + + if (!$_is_archived) { + $_delegate_ch = ''; + } + else { + $_delegate_ch = ''; + } + + $dat .= ' + + + + + + + +' . $rta . ' +' . $favicon . ' +' . $css . ' +' . $canonical . ' +' . $rss . $_delegate_ch . ' +' . $page_title . '' . $scriptjs . $extra; + + $embedearly = EMBEDEARLY; + + $adembedearly = AD_EMBEDEARLY; + + if (AD_ADBLOCK_TEXT && (DEFAULT_BURICHAN || BOARD_DIR === 'pol' || BOARD_DIR === 'bant')) { + $adembedearly .= file_get_contents_cached(AD_ADBLOCK_TEXT); + } + + $board_class = 'board_' . BOARD_DIR; + + if (!$res) { + if ($is_arclist) { + $board_class = 'is_arclist ' . $board_class; + } + else { + $board_class = 'is_index ' . $board_class; + } + } + else { + $board_class = 'is_thread ' . $board_class; + } + + if (TEXT_ONLY) { + $board_class = 'text_only ' . $board_class; + } + + if (!$is_arclist) { + $abovePostForm = '
    '; + } + else { + $abovePostForm = ''; + } + + $dat .= << + + + +$stylejs +$includenav + +
    + $titlepart +
    $title
    + $subtitle +
    +$abovePostForm +HTML; + + if (!$error && !$is_arclist) { + /* + if (defined('ADS_BIDGLASS_TOP_MOBILE') && ADS_BIDGLASS_TOP_MOBILE) { + $dat .= ''; + } + else if (defined('AD_ABC_TOP_MOBILE') && AD_ABC_TOP_MOBILE) { + $dat .= '

    '; + }*/ + } +} + +function delete_uploaded_files() +{ + global $upfile_name, $upfile, $dest, $pchfile; + if( $dest || $upfile ) { + @unlink( $dest ); + @unlink( $upfile ); + } +} + +/* Footer */ +function foot( &$dat, $error = false, $is_arclist = false ) +{ + global $update_avg_secs; + + $includenav = file_get_contents_cached(NAV2_TXT); + + $dat .= $includenav; + $dat .= rebuildd_stats(); + + if( CODE_TAGS ) { + $dat .= ''; + } + + $dat .= EMBEDLATE . ''; +} + +function error($mes, $unused = '') { + global $mode; + + if (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json') { + error_json($mes); + } + + if( $mode == "report" ) fancydie( $mes ); + + delete_uploaded_files(); + + head( $dat, 0, 1 ); + + $protocol = (stripos($_SERVER["HTTP_REFERER"], "https") === 0) ? 'https:' : 'http:'; + + $dat .= '
    ' . $mes . '

    [" . S_RELOAD . "]



    "; + + foot( $dat, true ); + + if (TEST_BOARD==1) { + internal_error_log("post error", $mes); + } + + die( $dat ); +} + +function error_json($msg) { + delete_uploaded_files(); + + header('Content-Type: application/json'); + + echo json_encode(['error' => $msg]); + + die(); +} + +function error_redirect($mes, $redirect, $timeout = 3000) { + delete_uploaded_files(); + head( $dat, 0, 1 ); + $dat .= << + setTimeout(function() { window.location = "$redirect"; }, $timeout); + +HTML; + $dat .= '
    ' . $mes . '

    [" + . S_RELOAD . "]



    "; + foot( $dat ); + + if (TEST_BOARD==1) { + internal_error_log("post error", $mes); + } + + die( $dat ); +} + +/* Auto Linker */ +function normalize_link_cb( $m ) +{ + + $subdomain = $m[1]; + $original = $m[0]; + $board = strtolower( $m[2] ); + $m[0] = $m[1] = $m[2] = ''; + + $count = count( $m ) - 1; + for( $i = $count; $i > 2; $i-- ) { + if( $m[$i] ) { + $no = $m[$i]; + break; + } + } + + if( $subdomain != 'boards') { + return $original; + } + + if( stripos( $no, 'catalog' ) === 0 ) { + + if( ( $pos = stripos( $no, '#s=' ) ) !== false ) { + $term = substr( $no, $pos + 3 ); + } else { + $term = 'catalog'; + } + + return ">>>/$board/$term"; + } + + if( $board == BOARD_DIR && $no && $no != 'catalog' ) { + return ">>$no"; + } else { + return ">>>/$board/$no"; + } +} + +function normalize_links( $proto ) +{ + // change http://xxx.4chan.org/board/res/no links into plaintext >># or >>>/board/# + $proto = preg_replace_callback( '@https?://([a-z]*)[.](?:4chan|4channel)[.]org/(\w+)/(?:(res|thread)/(\d+)(?:\/[-a-z0-9]+)?(?:#[qp]?(\d*))?|(catalog(?:#s=[a-z0-9+]+)?)|\w+.php[?]res=(\d+)(?:#[qp]?(\d*))?|)(?=[\s. false); + return false; + } + $r = (int)mysql_fetch_row($q)[0]; + $log[$no] = array('resto' => $r); + return $r; + + return false; +} + +// FIXME +// This might not be used anywhere anymore +function intraboard_link_cb( $m ) +{ + global $intraboard_cb_resno, $log; + $no = $m[1]; + $resno = $intraboard_cb_resno; + $resto = post_resto( $no ); // doesn't like assignment in condition + if( $resto !== false ) { + $resdir = ( $resno ? '' : RES_DIR2 ); + $ext = PHP_EXT2; + $id = NEW_HTML ? "p$no" : "$no"; + if( $resno && $resno == $resto ) // linking to a reply in the same thread + return ">>$no"; + elseif( $resto == 0 ) // linking to a thread + return ">>$no"; else // linking to a reply in another thread + return ">>$no"; + } + + return '' . $m[0] . ''; +} + +// FIXME +// This might not be used anywhere anymore, see parse_intraboard_link() +function intraboard_links( $proto, $resno ) +{ + global $intraboard_cb_resno; + + $intraboard_cb_resno = $resno; + + $proto = preg_replace_callback( '/>>([0-9]+)/', 'intraboard_link_cb', $proto ); + + return $proto; +} + +function other_board_resto( $board, $resno ) +{ + // this function is a little slow + // board requirements - either is the current board (for /test/) or is public (in boardlist) + // returns - + // FALSE if resno does not exist + // 0 if resno is a thread + // an id if resno is a reply to a thread + + static $boardlist = array(); + + if( !$boardlist ) + $boardlist = array_flip( mysql_column_array( mysql_global_call( "select sql_cache dir from boardlist" ) ) ); + + if( $board != BOARD_DIR && !isset( $boardlist[$board] ) ) + return false; + + $q = mysql_board_call( "select resto from `%s` where no=%d", $board, $resno ); + if( !mysql_num_rows( $q ) ) + return false; + $r = mysql_result( $q, 0 ); + + return $r; +} + +function interboard_link_cb( $m ) +{ + // on one hand, we can link to imgboard.php, using any old subdomain, + // and let apache & imgboard.php handle it when they click on the link + // on the other hand, we can use the database to fetch the proper subdomain + // and even the resto to construct a proper link to the html file (and whether it exists or not) + + // for now, we'll assume there's more interboard links posted than interboard links visited. + $board = '/'; + $otherboard = mb_strtolower( $m[1] ); + if( $m[2] ) { + $resto = other_board_resto( $otherboard, $m[2] ); + $id = "#p"; + + if( $resto === false ) + $url = ""; + else if( $resto ) + $url = $board . $otherboard . '/thread/' . $resto . $id . $m[2]; + else + $url = $board . $otherboard . '/thread/' . $m[2] . $id . $m[2]; + } else $url = $board . $otherboard . '/'; + + $b = $m[1]; + $mlp_hack = BOARD_DIR == 'mlp' && ( $b == 'b' || $b == 'co' ); + $original = mb_strtolower( $m[0] ); + if( !$url || $mlp_hack ) + return '' . $original . ''; + else + return "{$original}"; +} + +function interboard_catalog_link_cb( $m ) +{ + $board = $m[1]; + if( $board == 'f' ) return $m[0]; + + $lsearchquery = strtolower( urlencode( urldecode( $m[2] ) ) ); + $original = mb_strtolower( str_replace( ">", "> ", $m[0] ) ); + + if( $lsearchquery == "catalog" ) { + return "$original"; + } elseif( $lsearchquery == 'rules' ) { + return '' . $original . ''; + } else { + return "$original"; + } +} + +function boards_matching_arr() +{ + static $boards_matching_arr; + global $valid_boards; + + if( empty( $boards_matching_arr ) ) $boards_matching_arr = explode( '|', $valid_boards ); + + return $boards_matching_arr; +} + +// Normalize and linkify internal and non-quote links +// before inserting the post into the database. +function normalize_and_linkify($proto) { + + if (strpos($proto, "4chan") !== false || strpos($proto, "4cdn.org") !== false) { + // normalize long links + $proto = normalize_links($proto); + + // linkify other internal links + if ((strpos($proto, "4chan") !== false && strpos($proto, "/derefer") === false) || strpos($proto, "4cdn.org") !== false) { + $proto = preg_replace_callback( '/(https?:\/\/(?:[A-Za-z]*\.)?)(4chan|4channel|4cdn)(\.org)(\/[\w\-\.,@?^=%&;:\/~\+#\(\)]*[\w\-\@?^=%&;\/~\+#])?/i', 'clean_internal_link', $proto ); + $proto = preg_replace( '/([<][^>]*?)\\2<\/a>([^<]*?[>])/i', '\\1\\3\\4\\5\\6\\7\\8', $proto ); + } + } + + if (strpos($proto, '>>>') !== false) { + $proto = preg_replace_callback('#>>>/([a-z0-9]+)/([a-z0-9+/,l\-]*)#', 'auto_link_static_cb', $proto); + } + + return $proto; +} + +// Removes >> links from internal 4chan.org links +function clean_internal_link($matches) { + $link = preg_replace('/>>>|>>/', '', $matches[0]); + return "$link"; +} + +function auto_link_static_cb($matches) { + $post = $matches[0]; + $inter_board = $matches[1]; + $no = $matches[2]; + + $boards_matching_arr = boards_matching_arr(); + + $full_link = $post; + + $inter_board = strtolower( $inter_board ); + + $is_board_link = ($no == ''); + + $resno = $no; + + // Text boards + // Catalog, rules, and /rs/ links + if (!is_numeric( $resno ) || $is_board_link) { + $url = urlencode( urldecode( $resno ) ); + + $target = ''; + + if( strpos( $resno, 'rules' ) === 0 ) { + $ruleno = ''; + $ruleloc = strpos( $resno, '/' ); + + if( $ruleloc !== false ) { + $ruleno = substr( $resno, $ruleloc + 1 ); + } + + $parsed_link = '//www.' . L::d($inter_board) . "/rules#$inter_board$ruleno"; + $target = ' target="_blank"'; + } + else if (in_array($inter_board, $boards_matching_arr)) { + $parsed_link = '//boards.' . L::d($inter_board) . "/$inter_board/"; + + if( $inter_board == 'f' && $url == 'catalog' ) return $full_link; + + if( !$is_board_link ) { + $parsed_link .= ($url == 'catalog' ? 'catalog' : "catalog#s=$url"); + } + } + else { + return $full_link; + } + + return '' . $full_link . ''; + } + + return $full_link; +} + +function auto_link( $proto, $resno ) +{ + global $current_resno; + static $has_gen = 0; + + if( !$has_gen ) { + boards_matching_arr(); + $has_gen = 1; + } + + // The majority of posts don't contain links, so don't go there and waste time on preg junk + if( strpos( $proto, '>>' ) !== false ) { + $current_resno = $resno; + $proto = preg_replace_callback( '#(>>[0-9]+|>>>/[a-z0-9]+/[a-z0-9+/-]*)#', 'auto_link_cb', $proto ); + } + + return $proto; +} + +function auto_link_cb( $post ) +{ + global $current_resno; + + //var_dump($post); + $is_inter = ( strpos( $post[0], '>>>/' ) === 0 ); + $post = $post[0]; + + if( $is_inter ) { + preg_match( '#>>>/([a-z0-9]+)/(.*)#', $post, $match ); + + return parse_interboard_link( $match[0], $match[1], $match[2] ); + } else { + $no = explode( '>>', $post ); + + return parse_intraboard_link( $post, $no[1], $current_resno ); + } +} + +function parse_intraboard_link( $post, $no, $resno ) +{ + $full_link = $post; + //$no = substr( $post, $i - $in_link_char, $in_link_char ); + $resto = post_resto( $no ); + + if( $resto === false ) { + $parsed_link = '' . $full_link . ''; + + return $parsed_link; + } + + $ext = PHP_EXT2; + $id = NEW_HTML ? "p$no" : "$no"; + + // linking to a reply or the OP in the same thread + if ($resno && ($resno == $resto || $resno == $no)) { + $parsed_link = ">>$no"; + } + // linking to an OP in another thread or from indexes + elseif ($resto == 0) { + $parsed_link = ">>$no"; + } + // linking to a reply in another thread or from indexes + else { + $parsed_link = ">>$no"; + } + + return $parsed_link; +} + +function parse_interboard_link( $post, $inter_board, $no ) +{ + global $valid_boards; + + // make sure to account for > not > + $full_link = $post; + $inter_board = strtolower( $inter_board ); + + // Are we a board link? + $is_board_link = ( $no == '' ); + + // ... to get resno! + $resno = $no; + + $valid_rule = $inter_board == 'global' && strpos( $resno, 'rules' ) !== false; + + // Skip static links (boards, catalog) + if ($is_board_link + || $valid_rule + || !is_numeric($resno) + || !in_array($inter_board, boards_matching_arr()) + ) { + return $full_link; + } + + // Valid board, now check post number + $resto = other_board_resto( $inter_board, $resno ); + + if( $resto === false ) { // dead link + $url = ''; + } elseif( $resto ) { // different thread + $url = '//boards.' . L::d($inter_board) . "/{$inter_board}/thread/$resto#p$resno"; + } else { // same thread + $url = '//boards.' . L::d($inter_board) . "/{$inter_board}/thread/$resno#p$resno"; + } + + $disable = BOARD_DIR == 'mlp' && ( $inter_board == 'b' || $inter_board == 'co' ); + if( !$url || $disable ) { + $parsed_link = '' . $full_link . ''; + } else { + $parsed_link = "$full_link"; + } + + return $parsed_link; +} + +function trans_same_board_links( &$com ) +{ + $match = '>>>/' . BOARD_DIR . '/'; + $len = strlen( $match ); + $i = stripos( $com, $match ); + $boardlen = strlen( BOARD_DIR ) - 1; + $dir = BOARD_DIR; + $com .= '~'; + + while( isset( $com{$i} ) ) { + + if( is_numeric( $com{$i + $len} ) ) { + // Match, replace out + $com = substr_replace( $com, '>>', $i, $len ); + + } + + $i = stripos( $com, $match, $i + $len ); + if( $i === false ) { + $com = substr( $com, 0, strlen( $com ) - 1 ); + + return; + } + } +} + +function auto_link_parser( $post, $resno ) +{ + $post .= '<'; + + $i = 0; + $in_link = false; + $in_link_char = 0; + + $is_inter = false; + $inter_found_board = false; + $inter_board = ''; + $inter_is_catalog = false; + + $is_intra = false; + $gt_count = 0; + $mbcl = 10; // forgot about dis links :( + + $dbg = ""; + + while( isset( $post{$i} ) ) { + $seen_gt_this = false; + $c = $post{$i}; + + if( !$in_link ) { + // Not in a link, find > + + if( $c == '&' ) { + if( $post{$i + 1} == 'g' && $post{$i + 2} == 't' && $post{$i + 3} == ';' ) { + if( $gt_count < 3 ) $gt_count++; + + $i = $i + 4; + continue; + } + } + + if( ( $c == '/' && $gt_count == 3 ) || ( $gt_count > 1 && is_numeric( $c ) ) ) { + $in_link_char = 0; + $in_link = true; + $i--; // shift us back a char to get the right match... + } + + $gt_count = 0; + } else { + // We can be sure we have a valid character for our link + if( $in_link_char == 0 ) { + + if( $c == '/' ) { + $is_inter = true; + $is_intra = false; + } else { + $is_intra = true; + $is_inter = false; + } + } + + if( $is_inter ) { + + if( $in_link_char == 0 ) { + $in_link_char++; + $i++; + continue; + } + + if( $in_link_char > $mbcl && $c != '/' && !$inter_found_board ) { + // yup :( + + $in_link = false; + $is_inter = false; + + $i++; + continue; + } + + if( !$inter_found_board && $c == '/' ) { + $inter_board = substr( $post, $i - ( $in_link_char - 1 ), $in_link_char - 1 ); + + $inter_found_board = true; + $in_link_char++; + $i++; + continue; + } + + if( $inter_found_board ) { + $match = ( + ctype_alnum( $c ) || + $c == '+' || + $c == '/' || + $c == '-' + ); + + if( $match ) { + $in_link_char++; + $i++; + continue; + } + } + + if( !$inter_found_board ) { + $in_link_char++; + $i++; + continue; + } + + + parse_interboard_link( $post, $i, $in_link_char, $inter_board ); + + $is_inter = false; + $in_link = false; + $inter_found_board = false; + } + + if( $is_intra ) { + if( is_numeric( $c ) ) { + $in_link_char++; + } else { + // reached the end + parse_intraboard_link( $post, $i, $in_link_char, $resno ); + + $in_link = false; + $is_intra = false; + } + } + } + + $i++; + } + + return substr( $post, 0, strlen( $post ) - 1 ) . $dbg; +} + +/** + * New version of check_blacklist() + * $post must be an array with the following fields: + * resto, filename, name, tripcode, password, 4pass_id + */ +function check_md5_blacklist($md5, $original_md5, $post, $dest) { + if (!$md5) { + return false; + } + + $board = BOARD_DIR; + + if (DEFAULT_BURICHAN) { + $ws_clause = " OR boardrestrict = '_ws_'"; + } + else { + $ws_clause = ''; + } + + $sql =<< 0) { + $private_reason = "DMCA complaint from {$row['description']} (blacklist ID: {$row['id']})"; + auto_ban_poster($ban_name, 3, 1, $private_reason, S_DMCABANREASON, true, $pwd, $pass_id); + error_redirect(S_BANNED, 'https://www.' . L::d(BOARD_DIR) . '/banned'); + } + else { + $query = "INSERT INTO user_actions (board,postno,ip,time,uploaded,action) VALUES ('%s', %d, %d, NOW(), 0, 'fail_dmca')"; + mysql_global_call($query, $board, 0, $ip); + } + + error(S_DMCAFAIL, $dest); + } + + if ($row['quiet']) { + show_post_successful_fake($resto); + die(); + } + + error(S_FAILEDUPLOAD, $dest); +} + +function check_blacklist($post, $dest, $file_ext = '', $resto = 0, $pwd = null, $pass_id = null) { + //if( has_level() ) return; + + $board = BOARD_DIR; + + if (DEFAULT_BURICHAN) { + $ws_clause = " OR boardrestrict = '_ws_'"; + } + else { + $ws_clause = ''; + } + + $querystr = "SELECT SQL_NO_CACHE * FROM blacklist WHERE active=1 AND (boardrestrict='' or boardrestrict='$board'$ws_clause) AND (0 "; + foreach( $post as $field => $contents ) { + if( $contents ) { + $contents = mysql_real_escape_string( html_entity_decode( $contents ) ); + $querystr .= "OR (field='$field' AND contents='$contents') "; + } + } + $querystr .= ") LIMIT 1"; + + $query = mysql_global_call( $querystr ); + if( mysql_num_rows( $query ) == 0 ) return false; + + $row = mysql_fetch_assoc( $query ); + $prvreason = "Blacklisted ${row['field']} - " . htmlspecialchars( $row['contents'] ); + + if ($row['field'] == 'md5') { + $prvreason .= ' - Filename: ' . htmlspecialchars($post['filename']) . $file_ext; + } + + if (!$row['ban']) { + if (TEST_BOARD) { + error( "Blacklisted: " . $prvreason, $dest ); + } + } + // Auto-ban + else if ($row['ban'] == '1') { + auto_ban_poster($post['trip'] ? $post['nametrip'] : $post['name'], $row['banlength'], 1, $prvreason, $row['banreason'], false, $pwd, $pass_id); + } + // Show error (DMCA requests) + else if ($row['ban'] == '2') { + $ip = ip2long($_SERVER['REMOTE_ADDR']); + + $query = "SELECT ip FROM user_actions WHERE action = 'fail_dmca' AND ip = %d AND time >= DATE_SUB(NOW(), INTERVAL 1 DAY)"; + + $res = mysql_global_call($query, $ip); + + if ($res && mysql_num_rows($res) > 0) { + $prvreason = "DMCA complaint from {$row['description']} (blacklist ID: {$row['id']})"; + + auto_ban_poster($post['trip'] ? $post['nametrip'] : $post['name'], 3, 1, $prvreason, S_DMCABANREASON, true, $pwd, $pass_id); + + error_redirect(S_BANNED, 'https://www.' . L::d(BOARD_DIR) . '/banned'); + } + else { + $query = "INSERT INTO user_actions (board,postno,ip,time,uploaded,action) VALUES ('%s',%d,%d,NOW(),0,'fail_dmca')"; + mysql_global_call($query, $board, 0, $ip); + } + + error(S_DMCAFAIL, $dest); + } + /* + { + $ip = $_SERVER['REMOTE_ADDR']; + quick_log_to("/www/perhost/blacklist.log", "IP $ip board /$board/: $prvreason"); + } + */ + + if ($row['quiet']) { + show_post_successful_fake($resto); + die(); + } + + error(S_FAILEDUPLOAD, $dest); +} + +// we've already failed the floodcheck, check if they're a repeat offender and ban them +function check_fail_floodcheck($info) +{ + $ip = ip2long($_SERVER['REMOTE_ADDR']); + mysql_global_call("insert into user_actions (ip,board,action,time) values (%d,'%s','fail_floodcheck',now())", $ip, ''); + $query = mysql_global_call("select count(*)>%d from user_actions where ip=%d and action='fail_floodcheck' and time >= subdate(now(), interval 1 hour)", LOGIN_FAIL_HOURLY, $ip); + quick_log_to("/www/perhost/floodchecks.log", $info); + if(mysql_result($query,0,0)) { + auto_ban_poster("Anonymous", 1, 1, "got a flood check warning 5 times in an hour", "Sending an excessive number of server requests"); + } +} + +// word-wrap without touching things inside of tags +function wordwrap2( $str, $cols, $cut ) +{ + // if there's no runs of $cols non-space characters, wordwrap is a no-op + if( mb_strlen( $str ) < $cols || !preg_match( '/[^ <>]{' . $cols . '}/', $str ) ) { + return $str; + } + $sections = preg_split( '/[<>]/', $str ); + $str = ''; + for( $i = 0; $i < count( $sections ); $i++ ) { + if( $i % 2 ) { // inside a tag + $str .= '<' . $sections[$i] . '>'; + } else { // outside a tag + $words = explode( ' ', $sections[$i] );/* + $exclude = array( + 'http://', + 'https://', + 'www.' + ); +*/ + foreach( $words as &$word ) {/* + foreach( $exclude as $match ) { + if( stripos( $word, $match ) === 0 && stripos( $word, '4chan.org' ) !== false ) continue 2; + }*/ + + $word = htmlspecialchars_decode( $word, ENT_QUOTES ); + $word = utf8_wordwrap( $word, $cols, $cut, true ); + $word = htmlspecialchars( $word, ENT_QUOTES ); + + } + + $str .= implode( ' ', $words ); + } + } + + return $str; +} + +function logtime( $desc ) +{ + static $run = -1; + if( !PROFILING ) return; + if( $run == -1 ) { + $run = getmypid_cached(); + } + $board = BOARD_DIR; + $time = microtime( true ); + mysql_global_call( "INSERT INTO profiling_times VALUES ('$board',$run,$time,'$desc')" ); +} + +function time_log($r) { + if (TEST_BOARD && $_SERVER['HTTP_ACCEPT'] !== 'application/json') { + echo "\n"; + } +} + +function is_bad_xff( $xff ) +{ + if ($xff === '8.8.8.8' || $xff === '62.210.138.29' || $xff === '212.129.0.228') { + return true; + } + + list( $xffs ) = post_filter_get( "xffwhitelist" ); + $ipnum = ip2long( $xff ); + + if( !$ipnum ) return true; // text in xff field + + return find_ipxff_in( 0, $ipnum, $xffs ); +} + +function has_doubles( $id ) +{ + if( $id % 1000 == 0 ) return false; + + $ones = $id % 10; + $tens = ( $id / 10 ) % 10; + + return $ones == $tens; +} + +function generate_uid($resto, $time, $ip = false) { + if (DISP_ID_RANDOM) { + $str = mt_rand(); + } + else { + $str = !$ip ? $_SERVER["REMOTE_ADDR"] : $ip; + + if (DISP_ID_PER_THREAD) { + $str .= $resto ? $resto : date( 'Ymd', $time ); + } else { + $str .= 'hats'; // we will put a hat on it to confuse people :) + } + } + + $salt = file_get_contents_cached( SALTFILE ); + $hash = base64_encode( pack( "H*", sha1( $str . $salt ) ) ); + + return substr( $hash, 0, 8 ); +} + +function parse_vip_capcode($capcode) { + // Flood check + $longip = ip2long($_SERVER['REMOTE_ADDR']); + + $query = <<= SUBDATE(NOW(), INTERVAL 1 HOUR) +SQL; + + $res = mysql_global_call($query, $longip); + + if (!$res) { + return false; + } + + $count = mysql_fetch_row($res)[0]; + + if ($count >= 3) { + return false; + } + + // Now check the capcode + list($_, $user_id, $user_key) = explode('!', $capcode, 3); + + if (!$user_id || !$user_key) { + return false; + } + + $query = "SELECT name, user_key FROM vip_capcodes WHERE active = 1 AND user_id = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $user_id); + + if (!$res) { + return false; + } + + $user = mysql_fetch_assoc($res); + + if ($user && password_verify($user_key, $user['user_key'])) { + $query = "UPDATE vip_capcodes SET last_used = %d, last_ip = '%s' WHERE user_id = '%s' LIMIT 1"; + mysql_global_call($query, $_SERVER['REQUEST_TIME'], $_SERVER['REMOTE_ADDR'], $user_id); + return $user['name']; + } + + // Log the failure + $query = <<getPwd(); + + if (!$pass) { + error(S_GENERICERROR, $dest); + } + + $resto = (int)$resto; + + // time + $time = $_SERVER['REQUEST_TIME']; + $tim = generate_tim(); + + $captcha_bypass_allow_credits = false; + + $memcached = null; + + if (isset($_POST['recaptcha_challenge_field'])) { + error('Legacy captcha is no longer supported.'); + } + else if (isset($_POST['g-recaptcha-response'])) { + if (CAPTCHA_TWISTER) { + error('reCAPTCHA v2 is no longer supported.'); + } + + // Recaptcha v2 + start_auth_captcha(); + + if (!$captcha_bypass) { + $_c_ret = end_recaptcha_verify(); + } + } + else { + if (valid_captcha_bypass() !== true) { + $memcached = create_memcached_instance(); + + $_unsolved_count = 0; + + // Captcha bypass credits + if ($resto && isset($_POST['t-challenge']) && $_POST['t-challenge'] === 'noop') { + if (use_twister_captcha_credit($memcached, $host, $userpwd) === false) { + error(S_CAPTCHATIMEOUT); + } + } + // Captcha verification failed + else if (is_twister_captcha_valid($memcached, $host, $userpwd, BOARD_DIR, $resto, $_unsolved_count) === false) { + // Silent captcha failure for new suspicious users + //$_bad_actor = spam_filter_is_bad_actor(); + //$_threat_score = spam_filter_get_threat_score($_SERVER['HTTP_X_GEO_COUNTRY'], !$resto, true); + if (isset($_SERVER['HTTP_X_BOT_SCORE'])) { + $_bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + } + else { + $_bot_score = 100; + } + + if (!$userpwd || $userpwd->isUserKnownOrVerified(1440, 1) === false) { + if ($_bot_score > 1 && $_bot_score < 80) { + $_meta = spam_filter_format_http_headers(htmlspecialchars($com), $_SERVER['HTTP_X_GEO_COUNTRY'], $upfile_name, $_threat_score); + if (isset($_POST['t-challenge']) && $_POST['t-response'] && isset($_COOKIE['_tcs'])) { + log_failed_captcha($host, $userpwd, BOARD_DIR, $resto, true, $_meta); + } + show_post_successful_fake($resto, false); + die(); + } + } + error(S_BADCAPTCHA); + } + // Captcha verification succeeded + else if ($_unsolved_count < 2 && CAPTCHA_ALLOW_BYPASS && spam_filter_is_bad_actor() === false) { + $captcha_bypass_allow_credits = true; + } + } + } + /* + if (spam_filter_is_bad_actor()) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('log_bad_actor', BOARD_DIR, $resto, $host, 1, $_bot_headers); + } + */ + if (PASS_POST_ONLY && !$captcha_bypass) { + error(S_PASS_POST_ONLY); + } + + // Validate 2FA if posting html + if ((isset($_POST['html']) && $_POST['html']) && (has_level('manager') || has_flag('html') || has_flag('developer'))) { + validate_otp(); + $log_html_post = $log_mod_action = true; + } + + $locked_time = $time; + // check closed + if( $resto ) { + if( !$cchk = mysql_board_call( "select closed,sticky,undead,archived,sub,com from `" . SQLLOG . "` where no=" . $resto ) ) { + echo S_SQLFAIL; + } + list( $closed, $sticky, $undead, $is_archived, $_thread_sub, $_thread_com ) = mysql_fetch_row( $cchk ); + if ($is_archived) { + error(S_MAYNOTREPLY, $upfile); + } + $is_undead_sticky = $sticky == 1 && $undead == 1; + if( $closed == 1 && !has_level() ) error( S_MAYNOTREPLY, $upfile ); + mysql_free_result( $cchk ); + + $sub = ''; + } + else { + $is_undead_sticky = false; + } + + $has_image = $upfile && file_exists( $upfile ); + + $md5 = null; + $original_md5 = null; // MD5 before exif and other metadata stripping + + if( $has_image ) { + if (UPLOAD_BOARD) { + if( file_exists( IMG_DIR . $upfile_name ) ) error( "Filename already exists.", $upfile ); + + $dest = $upfile; + + $upfile_name = sanitize_text( $upfile_name ); + if( !is_file( $dest ) ) error( S_FAILEDUPLOAD, $dest ); + if ($upfile_name[0] === '.') { + error('Error: Invalid filename (first character cannot be a period).', $dest); + } + $size = getimagesize( $dest ); + if( !is_array( $size ) ) error( S_NOREC, $dest ); + + $W = $size[0]; + $H = $size[1]; + $fsize = filesize( $dest ); + if( $fsize > MAX_KB * 1024 ) error( S_TOOLARGE, $dest ); + if( $size[2] == 6 || $size[2] == 5 ) { + error( S_FAILEDUPLOAD, $dest ); + } + switch( $size[2] ) { + case 4 : + case 13 : + $ext = ".swf"; + break; + default : + $ext = ".xxx"; + error( S_FAILEDUPLOAD, $dest ); + break; + } + + time_log( "sfpi" ); + rpc_task(); + + $len = strlen( $ext ); + } + else { + // NOT upload board + + // check image limit + if( $resto && !$sticky && !$undead && !has_level() ) { + if( !$result = mysql_board_call( "SELECT COUNT(*) FROM `" . SQLLOG . "` WHERE archived = 0 AND resto=$resto AND fsize!=0 AND filedeleted=0" ) ) { + echo S_SQLFAIL; + } + $countimgres = mysql_result( $result, 0, 0 ); + if( $countimgres >= MAX_IMGRES && !has_level() ) error(S_MAXIMAGESREACHED, $upfile ); + mysql_free_result( $result ); + } + + //upload processing + $dest = $upfile; + + // TODO: what does that preg_replace do? those are probably utf8 codes + $upfile_name = sanitize_text( preg_replace('/\xe2\x80(\xae|\xad|\x8f|\x8e)/', '', $upfile_name) ); + + if (!is_file($dest)) { + error(S_FAILEDUPLOAD, $dest); + } + + // Use filesize() later as it's possible to trick jpegtrans to generate a much bigger file + $fsize = $_FILES['upfile']['size']; + + if (!$fsize || $fsize > MAX_KB * 1024) { + error( S_TOOLARGE, $dest ); + } + + $webm_sar = null; + + // PDF processing + if( ENABLE_PDF == 1 && strcasecmp( '.pdf', substr( $upfile_name, -4 ) ) == 0 ) { + $ext = '.pdf'; + $W = $H = 1; + // run through ghostscript to check for validity + if( pclose( popen( "/usr/local/bin/gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=nullpage $dest", 'w' ) ) ) { + error( S_FAILEDUPLOAD, $dest ); + } + } + // Webm / MP4 + else if (ENABLE_WEBM && preg_match('/\.(webm|mp4)$/i', $upfile_name)) { + if ($fsize > MAX_WEBM_FILESIZE * 1024) { + error(S_TOOLARGE, $dest); + } + + $original_md5 = md5_file($dest); + + $ext = '.' . strtolower(pathinfo($upfile_name, PATHINFO_EXTENSION)); + + //if ($ext == '.mp4' && BOARD_DIR != 'test') { + // error(S_NOREC, $dest); + //} + + $size = validate_webm($dest, $ext); + $W = $size[0]; + $H = $size[1]; + $webm_sar = $size[2]; + } + // PNG, GIF, JPEG + else { + $size = getimagesize( $dest ); + if( !is_array( $size ) ) { + quick_log_to( "/www/perhost/bad-upload.log", "unrecognized file $upfile_name"); + + error( S_NOREC, $dest ); + } + + $W = $size[0]; + $H = $size[1]; + switch( $size[2] ) { + case 1 : + $ext = ".gif"; + break; + case 2 : + $ext = ".jpg"; + break; + case 3 : + $ext = ".png"; + break; + case 4 : + $ext = ".swf"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 5 : + $ext = ".psd"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 6 : + $ext = ".bmp"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 7 : + $ext = ".tiff"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 8 : + $ext = ".tiff"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 9 : + $ext = ".jpc"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 10 : + $ext = ".jp2"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 11 : + $ext = ".jpx"; + error( S_FAILEDUPLOAD, $dest ); + break; + case 13 : + $ext = ".swf"; + error( S_FAILEDUPLOAD, $dest ); + break; + default : + $ext = ".xxx"; + error( S_FAILEDUPLOAD, $dest ); + break; + } + if (GIF_ONLY == 1 && $size[2] != 1 && $ext != '.webm') error(S_FAILEDUPLOAD, $dest); + } // end PDF processing -else + + // This doesn't seem to use the $md5 arg + if ($upfile_name === '') { + error('Blank file names are not supported.', $dest); + } + + time_log( "sfpi" ); + rpc_task(); + + // Picture reduction + if( !$resto ) { + $maxw = MAX_W; + $maxh = MAX_H; + } else { + $maxw = MAXR_W; + $maxh = MAXR_H; + } + if( defined( 'MIN_W' ) && MIN_W > $W ) error( S_TOOSMALL, $dest ); + if( defined( 'MIN_H' ) && MIN_H > $H ) error( S_TOOSMALL, $dest ); + if( defined( 'MAX_DIMENSION' ) ) + $maxdimension = MAX_DIMENSION; + else + $maxdimension = 5000; + if( $W > $maxdimension || $H > $maxdimension ) { + error( S_TOOLARGERES, $dest ); + } elseif( $W > $maxw || $H > $maxh ) { + $W2 = $maxw / $W; + $H2 = $maxh / $H; + ( $W2 < $H2 ) ? $key = $W2 : $key = $H2; + $TN_W = ceil( $W * $key ) + 1; + $TN_H = ceil( $H * $key ) + 1; + } + + // Strip metadata, exif, comments and other embeddded extra data + if ($ext === '.jpg') { + // Embed detection is done later below if STRIP_EXIF is disabled + if (STRIP_EXIF) { + $original_md5 = md5_file($dest); + + if (strip_jpeg_exif($dest) === false) { + error(S_FAILEDUPLOAD, $dest); + } + + clearstatcache(true, $dest); + } + } + else if ($ext === '.png') { + $original_md5 = md5_file($dest); + $_ret = strip_png_chunks($dest, MAX_KB * 1024); + if ($_ret < 0) { + if ($_ret === -2) { + error('APNG format not supported.', $dest); + } + else { + error(S_FAILEDUPLOAD, $dest); + } + } + else if ($_ret > 0) { + clearstatcache(true, $dest); + } + } + else if ($ext === '.gif') { + $original_md5 = md5_file($dest); + $_ret = strip_gif_extra_data($dest, $fsize); + if ($_ret < 0) { + error(S_FAILEDUPLOAD, $dest); + } + clearstatcache(true, $dest); + } + + // It should be safe to check the filesize now + // clearstatcache() must be called if the file was modified + $fsize = filesize($dest); + + if (!$fsize) { + error(S_TOOLARGE, $dest); + } + else if ($ext === '.webm' && $fsize > MAX_WEBM_FILESIZE * 1024) { + error(S_TOOLARGE, $dest); + } + else if ($fsize > MAX_KB * 1024) { + error(S_TOOLARGE, $dest); + } + + // Check for JPEG embedded data. jpegtran seems to remove unknown data + // so this only needs to be done if STRIP_EXIF is disabled + if (!STRIP_EXIF && $ext === '.jpg' && $fsize > 204800) { + validate_jpeg_size($dest, $fsize); + } + } + + $insfile = preg_replace('/\.[a-z0-9]+$/i', '', $upfile_name); + + $md5 = md5_file( $dest ); + $mes = $upfile_name . ' ' . S_UPGOOD; + } + + if( $_FILES["upfile"]["error"] > 0 ) { + if( $_FILES["upfile"]["error"] == UPLOAD_ERR_INI_SIZE ) + error( S_TOOLARGE, $dest ); + if( $_FILES["upfile"]["error"] == UPLOAD_ERR_FORM_SIZE ) + error( S_TOOLARGE, $dest ); + if( $_FILES["upfile"]["error"] == UPLOAD_ERR_PARTIAL ) + error( S_FAILEDUPLOAD, $dest ); + if( $_FILES["upfile"]["error"] == UPLOAD_ERR_CANT_WRITE ) + error( S_FAILEDUPLOAD, $dest ); + } + + if( $upfile_name && $_FILES["upfile"]["size"] == 0 ) { + error( S_TOOLARGEORNONE, $dest ); + } + + if( ENABLE_EXIF == 1 ) { + $exif = htmlspecialchars( shell_exec( "/usr/local/bin/exiftags $dest" ) ); + } + + $resto = (int)$resto; + if( $resto ) { + if( !mysql_result( mysql_board_call( "select count(no) from `" . SQLLOG . "` where root>0 and no=$resto" ), 0, 0 ) ) + error( S_NOTHREADERR, $dest ); + } + + $pass_is_bannable = !$userpwd->isNew(); + + // Most common errors checked, now check for post-block from ban requests + if (BLOCK_ON_BR && !has_level()) { + check_for_ban_request($host, $pass_is_bannable ? $pass : null); + } + + // Standardize new character lines + $com = str_replace( "\r\n", "\n", $com ); + $com = str_replace( "\r", "\n", $com ); + + $comlim = has_level() ? MAX_COM_CHARS_AUTHED : MAX_COM_CHARS; + $longlim = has_level() ? 255 : 100; + + if( mb_strlen( $com ) > $comlim ) error( S_TOOLONG, $dest ); + if( strlen( $name ) > $longlim ) error( S_TOOLONG, $dest ); + if( strlen( $email ) > $longlim ) error( S_TOOLONG, $dest ); + if( strlen( $sub ) > $longlim ) error( S_TOOLONG, $dest ); + if( strlen( $resto ) > 10 ) error( S_GENERICERROR, $dest ); + if( strlen( $url ) > 10 ) error( S_GENERICERROR, $dest ); + + $sub = normalize_content( $sub ); + + // start of some attempt to get rid of *all* zero width bollocks + if( BOARD_DIR != 'jp' && BOARD_DIR != 'a' && !SJIS_TAGS) { + $com = normalize_content( $com ); + $com = strip_zerowidth( $com ); + } + + // strip no break spaces and soft hyphens + $com = str_replace(array("\xC2\xAD", "\xC2\xA0"), '', $com); + + // name/subject too! + $sub = strip_zerowidth( $sub ); + $name = strip_zerowidth( $name ); + + // Strip unicode emoticons + $name = strip_emoticons($name, SJIS_TAGS); + + if ($sub !== '') { + $sub = strip_emoticons($sub, SJIS_TAGS); + } + + if ($com !== '') { + $com = strip_emoticons($com, SJIS_TAGS); + } + + // strip out ltr junk from name + $name = preg_replace( '#([\x{2000}-\x{200F}]|[\x{2028}-\x{202F}])#u', '', $name ); + + if ($sub !== '') { + $sub = strip_fake_capcodes($sub); + } + + if( !strlen( $name ) || preg_match( "/^[ | |]*$/", $name ) ) $name = ""; + if( !strlen( $com ) || preg_match( "/^[ | |\t]*$/", $com ) ) $com = ""; + if( !strlen( $sub ) || preg_match( "/^[ | |]*$/", $sub ) ) $sub = ""; + + //$name = str_replace( S_MANAGEMENT, "\"" . S_MANAGEMENT . "\"", $name ); + //$name = str_replace( S_DELETION, "\"" . S_DELETION . "\"", $name ); + + // Remove intra spoilers + if (SPOILERS && stripos($com, '[spoiler]') !== false) { + //$com = preg_replace( '/\[spoiler\]\s+\[\/spoiler\]/', '', $com ); + $com = preg_replace('/(\S)\[spoiler\](.*?)\[\/spoiler\](\S)/', '\\1\\2\\3', $com); + } + + //lol /b/ + $xff = get_request_xff(); + //if( is_bad_xff( $xff ) ) $xff = ""; + + $youbi = array(S_SUN, S_MON, S_TUE, S_WED, S_THU, S_FRI, S_SAT); + $yd = $youbi[date( "w", $time )]; + if( SHOW_SECONDS == 1 ) { + $now = date( "m/d/y", $time ) . "(" . (string)$yd . ")" . date( "H:i:s", $time ); + } else { + $now = date( "m/d/y", $time ) . "(" . (string)$yd . ")" . date( "H:i", $time ); + } + + $c_name = $name; + $c_email = $email; + + if (JANITOR_BOARD == 1) { + $name = get_hashed_mod_name($_COOKIE['4chan_auser']); + $email = ''; + } + + // April 2023 + //$_has_xa23_content = $com && preg_match('/^[^>]{8,}/m', $com) > 0; + + $com = preg_replace( '#>>>/' . BOARD_DIR . '/([0-9]+)#', '>>$1', $com ); + + $sub = sanitize_text( $sub ); + $sub = preg_replace( "/[\r\n]/", "", $sub ); + $sub = strip_private_unicode($sub); + $url = sanitize_text( $url ); + $url = preg_replace( "/[\r\n]/", "", $url ); + $resto = sanitize_text( $resto ); + $resto = preg_replace( "/[\r\n]/", "", $resto ); + $com = sanitize_text( $com, 1, true ); + $com = strip_private_unicode($com); + + if( FORCED_ANON == 1 ) { + if( !has_level('admin') ) $name = S_ANONAME; + $sub = ''; + } + + if (UPLOAD_BOARD) { + if( NO_TEXTONLY == 1 ) { + if( !$resto && !$has_image ) error( S_NOPIC, $dest ); + } else { + if( !$resto && !$textonly && !$has_image ) error( S_NOPIC, $dest ); + } + } else { + if (!TEXT_ONLY) { + if( NO_TEXTONLY == 1 && (!has_level() || $email === '') ) { + if( !$resto && !$has_image ) error( S_NOPIC, $dest ); + } else { + if( !$resto && !$textonly && !$has_image ) error( S_NOPIC, $dest ); + } + } + + if( REQUIRE_SUBJECT && !$resto && !strlen( $sub ) ) error( S_NOSUB, $dest ); + } + // Check for sage, nonoko and nonokosage + $is_sage = false; + + if (stripos($email, 'sage') !== false) { + $is_sage = true; + $email = str_ireplace('sage', '', $email); + } + + $email_lower = strtolower($email); + + if ($email_lower === 'nonoko') { + $is_nonoko = true; + } + + if( SPOILERS == 1 && $spoiler ) { + $sub = "SPOILER<>$sub"; + } + + if( !has_level() ) { + $match = array(); + if( substr_count( $com, "\n" ) > 6 ) preg_match_all( '#([^\n]+\n+)\1{5,}#', $com, $match ); + if( !empty( $match[0] ) ) { + foreach( $match[1] as $key => $var ) { + //auto_ban_poster( $name, 0, 1, 'Matched the same string 5 times or more in 1 post seperated by newlines (Matched: ' . $var . ')', 'Please do not spam.' ); + error( S_REJECTTEXTBAN ); + } + } + } + + // FIXME sanitize_text() replaces repeated \n too, is this duplicate? + // disable on code tag boards (we replace multiple brs instead) + if (!CODE_TAGS && !SJIS_TAGS) { + $com = preg_replace( "/\n(( | )*\n){3,}/", "\n", $com ); + } + + if( !has_level() && substr_count( $com, "\n" ) > MAX_LINES ) error(S_TOOMANYLINES, $dest ); + + if( ENABLE_EXIF == 1 && $exif ) { + //turn exif into a table + $exiflines = explode( "\n", $exif ); + $exif = ""; + foreach( $exiflines as $exifline ) { + list( $exiftag, $exifvalue ) = explode( ': ', $exifline ); + if( $exifvalue != '' ) + $exif .= ""; + else + $exif .= ""; + } + $exif .= '
    $exiftag$exifvalue
    $exiftag
    '; + $exiftext .= '

    ' . sprintf(S_EXIF_TOGGLE, $tim) . '
    '; + $exiftext .= "$exif"; + } + + $name = preg_replace( "/[\r\n]/", "", $name ); + + $names = mb_convert_encoding($name, 'CP932', 'UTF-8'); // convert to Windows Japanese #kami + + //start new tripcode crap + list ( $name ) = explode( "#", $name ); + + // Strip unicode point-of-interest and # characters + if (preg_match('/[\x{2318}\x{ff03}\x{FE5F}]/u', $name)) { + $name = preg_replace('/[\x{2318}\x{ff03}\x{FE5F}]/u', '', $name); + } + + $name = normalize_content( $name ); + $name = sanitize_text( $name ); + $name = strip_private_unicode($name); + + if (preg_match('/^\s+$/', $name)) { + $name = ''; + } + + $name = str_replace('!', '', $name); + + if( preg_match( "/\#+$/", $names ) ) { + $names = preg_replace( "/\#+$/", "", $names ); + } + if( preg_match( "/\#/", $names ) ) { + + $names = str_replace( "&#", "&&", htmlspecialchars( $names, ENT_COMPAT | ENT_HTML401, 'Shift_JIS' ) ); # otherwise HTML numeric entities screw up explode()! + + list ( $nametemp, $trip, $sectrip ) = str_replace( "&&", "&#", explode( "#", $names, 3 ) ); + + if ($sectrip != '') { + $trip = ''; + } + + $names = $nametemp; + if( STRIP_TRIPCODE == 0 ) $name .= "
    "; + + if ($trip != "" && STRIP_TRIPCODE == 0) { + $salt = strtr( preg_replace( "/[^\.-z]/", ".", substr( $trip . "H.", 1, 2 ) ), ":;<=>?@[\\]^_`", "ABCDEFGabcdef" ); + $trip = substr( crypt( $trip, $salt ), -10 ); + $name .= " !" . $trip; + } + + if( $sectrip != "" && STRIP_TRIPCODE == 0 ) { + $salt = file_get_contents_cached( SALTFILE ); + $sha = base64_encode( pack( "H*", sha1( $sectrip . $salt ) ) ); + $sha = substr( $sha, 0, 11 ); + $name .= " !!" . $sha; + } + } //end new tripcode crap + + // Check the length of the name field again to prevent truncation in the middle of tripcode HTML + if (strlen($name) > 255) { + error(S_TOOLONG, $dest); + } + + //Cookies + $cookie_domain = '.' . L::d(BOARD_DIR); + setrawcookie( "4chan_name", rawurlencode( $c_name ), $time + ( $c_name ? ( 7 * 24 * 3600 ) : -3600 ), '/', $cookie_domain ); + + if( !strlen( $name ) ) $name = S_ANONAME; + if( !strlen( $com ) ) $com = S_ANOTEXT; + if( !strlen( $sub ) ) $sub = S_ANOTITLE; + + /* since4pass */ + if ($captcha_bypass && $passid && $email && strpos(" $email ", ' since4pass ') !== false) { + $since4pass = get_since_4chan($passid); + } + else { + $since4pass = 0; + } + + // April 2024 + /* + if ($email) { + $_xa24_since4pass = april_2024_parse_email($email); + $since4pass = $_xa24_since4pass; + } + else { + $_xa24_since4pass = false; + } + */ + if (FORTUNE_TRIP == 1 && $email == 'fortune') { + $fortunes = array("Bad Luck", "Average Luck", "Good Luck", "Excellent Luck", "Reply hazy, try again", "Godly Luck", "Very Bad Luck", "Outlook good", "Better not tell you now", "You will meet a dark handsome stranger", "キタ━━━━━━(゚∀゚)━━━━━━ !!!!", "( ´_ゝ`)フーン ", "Good news will come to you by mail"); + // Christmas 2021 + /* + $fortunes = array("You're on the nice list!", "You're on the naughty list!", "Krampus is coming to your house!", "It's going to be a white Christmas!", "Merry Christmas!", "Happy Hanukkah!", "Happy Kwanzaa!", "Feliz Navidad!", "Happy Festivus!", "You're getting a lump of coal in your stocking!", "Blessed Yule!", "You're standing under the mistletoe!", "The poster above you has been very very naughty!", "You got an extra thick slice of fruitcake.", "You're on the Elf Watchlist.", "Your heart is two sizes too small.", "Your heart grew three sizes!", "Bah! Humbug."); + */ + $fortunenum = rand( 0, sizeof( $fortunes ) - 1 ); + $fortcol = "#" . sprintf( "%02x%02x%02x", + 127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) ), + 127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) + 2 / 3 * M_PI ), + 127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) + 4 / 3 * M_PI ) ); + $com .= "

    Your fortune: " . $fortunes[$fortunenum] . "
    "; + } + + if (DICE_ROLL == 1) { + if( $email ) { + if( preg_match( "/dice[ +](\\d+)[ d+](\\d+)(([ +-]+?)(-?\\d+))?/", $email, $match ) ) { + $dicetxt = S_DICE_PFX . ' '; + $dicenum = min( 25, $match[1] ); + $diceside = $match[2]; + $diceaddexpr = $match[3]; + $dicesign = $match[4]; + $diceadd = intval( $match[5] ); + + for( $i = 0; $i < $dicenum; $i++ ) { + $dicerand = mt_rand( 1, $diceside ); + if( $i ) $dicetxt .= ", "; + $dicetxt .= $dicerand; + $dicesum += $dicerand; + } + + if( $diceaddexpr ) { + if( strpos( $dicesign, "-" ) > 0 ) $diceadd *= -1; + $diceadd_formatted = ( $diceadd >= 0 ? " + " : " - " ) . abs( $diceadd ); + $dicetxt .= $diceadd_formatted; + $dicesum += $diceadd; + } + + if ($dicenum > 1) { + $dicetxt .= " = $dicesum"; + } + + $dicetxt .= " ({$dicenum}d{$diceside}" . ($diceaddexpr ? $diceadd_formatted : "") . ")

    "; + + $com = "$dicetxt" . $com; + } + } + } + + // fixme: this is needed for bypassing the r9k filter. $email gets reset below. + $options_field = $email; + + $emails = $email; + $admin_highlight = false; + $uid = null; + $capcode = 'none'; + $delay_refresh = false; + if (strpos($email, 'capcode_') === 0) { + // are we trying to capcode? + if (!has_level('admin') && !has_flag('capcodename')) { + $name = S_ANONAME; + } + // Only pass and authed users can use VIP capcodes + if ($captcha_bypass) { + $capcode = parse_capcode($email, $name); + } + else { + $capcode = parse_capcode($email); + } + + $ma = ( $capcode == 'admin_highlight') ? 'admin' : $capcode; + $ma = ucfirst( $ma ); + + if( DISP_ID == 1 && $ma != 'None' ) { + $uid = $ma; + } + + if( $ma != 'None' && STRIP_EXIF_ON_CAPCODE && $ext == ".jpg" ) { + system( "/usr/local/bin/jpegtran -copy none -outfile '$dest' '$dest'" ); + $md5 = md5_file( $dest ); + } + + if ($capcode !== 'none') { + $log_mod_action = $log_capcode_post = true; + } + + setcookie('options', $email, $time + (7 * 24 * 3600), '/', $cookie_domain); + } + else if (isset($_COOKIE['options'])) { + setcookie('options', null, $time -3600, '/', $cookie_domain); + } + + $email = ''; + + $nameparts = explode( '
    !', $name ); + + time_log( "trip" ); + + //logtime( "starting autoban checks" ); + + /** + * Ban check step + */ + $ban_fields = array(); + + if ($pass_is_bannable) { + $ban_fields['password'] = $pass; + } + /* + if ($nameparts[1]) { + $ban_fields['tripcode'] = $nameparts[1]; + } + */ + if ($passid) { + $ban_fields['4pass_id'] = $passid; + } + + $user_is_banned = check_for_ban($host, $ban_fields, $resto, $userpwd && $userpwd->verifiedLevel()); + + if ($user_is_banned) { + if (!$captcha_bypass) { + mysql_global_call("INSERT INTO user_actions (board,ip,time,action) VALUES ('%s',%d,from_unixtime(%d),'%s')", BOARD_DIR, ip2long($host), $time, 'is_banned'); + } + + $redirect = 'https://www.' . L::d(BOARD_DIR) . '/banned'; + + if ($user_is_banned == 1) { + // Banned + error_redirect(S_BANNED, $redirect); + } + else if ($user_is_banned == 2) { + // Warned + error_redirect(S_WARNED, $redirect); + } + else { + // Ban evasion + error("Error: $user_is_banned"); + } + } + + /** + * Validate the maximum number of allowed threads per user + */ + if (!$resto) { + validate_user_thread_limit( + $_SERVER['REMOTE_ADDR'], + isset($ban_fields['password']) ? $pass : null, + $passid + ); + } + + /* + * Embedded data detection and banned re-post block + */ + if ($has_image) { + // Check if the file contains embedded data. + if (false && CLEANUP_UPLOADS) { + // Update the size if the file was modified + if (cleanup_uploaded_file($dest, $ext)) { + clearstatcache(true, $dest); + $fsize = filesize($dest); + if (!$fsize) { + error(S_TOOLARGE, $dest); + } + $md5 = md5_file($dest); + } + } + + // Check if the uploaded file should be blocked because of previous bans + if (check_for_banned_upload($md5)) { + //log_spam_filter_trigger('block_banned_reupload', BOARD_DIR, $resto, $host, 1, $md5); + error(S_FAILEDUPLOAD, $dest); + } + } + + // --- + + $autosage = false; + + // See bellow wordwrap2() + if (strpos($com, 'rep?~') !== false) { + $com = str_replace(array('~?rep?~', '~?erep?~'), '', $com); + } + + if (!has_level() || $capcode === 'none' || BOARD_DIR == 'test') { + $autosage = spam_filter_post_content_new(BOARD_DIR, $resto, $com, $sub, $name, $upfile_name, $pass, ($captcha_bypass && $passid) ? $passid : null); + spam_filter_post_ip($userpwd, $resto, $has_image); + } + + if (!$capcode || $capcode == 'none') { + check_md5_blacklist($md5, $original_md5, [ + 'resto' => $resto, + 'name' => $nameparts[0], + 'tripcode' => $nameparts[1], + 'filename' => "$insfile$ext", + 'password' => $pass, + '4pass_id' => ($captcha_bypass && $passid) ? $passid : null + ], $dest); + } + + spam_filter_post_trip( $name, $trip, $dest ); + + time_log( "ab" ); + + // Only process linebreaks for non-html posts + if (!$log_html_post) { + $com = nl2br($com, false); + $com = str_replace("\n", "", $com); //\n is erased + } + + $com .= $exiftext; // must be done after spam filter, since it has a javascript: link + + if (SJIS_TAGS) { + $com = sjis_parse($com); + } + + if( SPOILERS == 1 ) { + $com = spoiler_parse( $com ); + if (stripos( $com, '') !== false) { + $com = preg_replace('/(\s|
    |(?R))*<\/s>/', '', $com); + //$com = preg_replace('/(\S)(.*?)<\/s>(\S)/', '\\1\\2\\3', $com); + } + } + /* + if( JSMATH == 1 ) { + $com = jsmath_parse( $com ); + } + */ + if( CODE_TAGS ) { + $com = preg_replace( '#(\[code\](.{0,6})\[\/code\])#', '\\2', $com ); + + $com = code_parse( $com ); + + $com = str_replace( '

    ', '
    ', $com );
    +		$com = preg_replace( '#(
    ){4,}#', '


    ', $com ); + } + + if (OP_MARKUP && (!$resto || is_poster_op($host, $pass, $resto))) { + $com = parse_op_markup($com); + } + + // pull this down to here to get rid of any shenanigans + if (!$resto) { // new threads require subject or comment + if ($sub === '' && ($com === '' || preg_match('/^(?:
    |\s)+$/', $com))) { + if ($options_field === '' || !$has_image || !has_level()) { + error(S_NOTEXT_OP, $dest); + } + } + else if (TEXT_ONLY && $sub === '') { + error(S_NOSUB, $dest); + } + } + else if (!$has_image && ($com === '' || preg_match('/^(?:
    |\s)+$/', $com))) { // replies without image + error(S_NOTEXT, $dest); + } + + if( WORD_FILT && $word_filters_enabled ) { + $com = word_filter( $com, "com" ); + if( $sub ) + $sub = word_filter( $sub, "sub" ); + $namearr = explode( ' ', $name ); + if( strstr( $name, ' ' ) ) { + $nametrip = ' ' . $namearr[1]; + } else { + $nametrip = ""; + } + if( $namearr[0] != S_ANONAME ) + $name = word_filter( $namearr[0], "name" ) . $nametrip; + } + + /*if( $html != 1 || ( !has_level('manager') ) ) { + $com = wordwrap2( $com, 35, "{{w_br}}" ); + }*/ + + // April 2022 + /* + if (strpos($com, ':') !== false) { + $com = april_process_post_emotes($com, $_COOKIE['xa_sid']); + } + */ + $com = normalize_and_linkify($com); + + //$html = isset( $_POST['html'] ) && $_POST['html'] == 1; + $html = 0; + if( (!has_level('manager') && !has_flag('html')) || $html != 1 ) { + $com = wordwrap2( $com, 35, "{{w_br}}" ); + } + + $com = preg_replace( '#(>>>/[a-z0-9]+/[^ <$]*|>>[0-9]+)#', '~?rep?~\\1~?erep?~', $com ); + $com = preg_replace( "!(^|r>|r> )(>[^<]*)!", "\\1\\2", $com ); + $com = preg_replace( '#~?rep?~(.+?)~?erep?~#', '\\1', $com ); + + $com = str_replace( array( '~?rep?~', '~?erep?~' ), '', $com ); + + $com = str_replace( '{{w_br}}', '', $com ); + + $admin_style = "padding: 5px;margin-left: .5em;border-color: #faa;border: 2px dashed rgba(255,0,0,.1);border-radius: 2px"; //FIXME make it easier to edit css so this can go in it + if( $admin_highlight ) { + $com = "
    $com
    "; + } + + if( DISP_ID == 1 && !$uid ) { + $uid = $is_sage && !META_BOARD && !DISP_ID_NO_HEAVEN ? "Heaven" : generate_uid( $resto, $time ); + } + + // Validate comment for OPs by regex + if (!$resto && COM_REGEX) { + if (preg_match(COM_REGEX, $com) === 0) { + error(S_INVALID_COM); + } + } + + //post text is now completely created, thumbnail not + + if( !$silent_reject ) { + //logtime( "Before flood check" ); + $may_flood = has_level( 'janitor' ); + + if (!$may_flood || (!has_level() && (META_BOARD || $_POST['name'] != ''))) { + if( $com ) { + // Check for duplicate comments + $query = "select sql_no_cache max(time) from `%s` where com='%s' " . + "and host='%s' " . + "and time>%d"; + $result = mysql_board_call( $query, SQLLOG, $com, $host, $time - RENZOKU_DUPE ); + if( $ltime = mysql_result( $result, 0, 0 ) ) { + //check_fail_floodcheck($com); + $str = sprintf(S_RENZOKU_DUP, sec2hms( ( $ltime + RENZOKU_DUPE ) - $time, false, true ) ); + error( $str, $dest ); + } + mysql_free_result( $result ); + } + + /** + * Posting cooldowns + */ + if (!$resto) { + /** + * New threads + */ + $query = "select max(time) from `%s` where time>%d " . + "and host='%s' and root>0"; //root>0 == non-sticky + $result = mysql_board_call( $query, SQLLOG, ( $time - RENZOKU3 ), $host ); + if( $ltime = mysql_result( $result, 0, 0 ) ) { + $str = sprintf(S_RENZOKU3, sec2hms( ( $ltime + RENZOKU3 ) - $time, false, true ) ); + error( $str, $dest ); + } + mysql_free_result( $result ); + // Cross-board cooldown + $query = "SELECT 1 FROM user_actions WHERE ip = %d AND action = 'new_thread' AND board != '%s' AND time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)"; + $result = mysql_global_call($query, ip2long($host), BOARD_DIR); + if (mysql_num_rows($result) > 0) { + error( S_RENZOKU3, $dest ); // You must wait longer before posting another thread + } + } + else { + /** + * Replies + * Pass users have lower cooldowns + */ + + // Check for same image flood first + if ($has_image && $resto) { + $query = "SELECT time FROM `%s` WHERE host = '%s' AND md5 = '%s' AND resto != %d ORDER BY no DESC LIMIT 1"; + + $result = mysql_board_call($query, SQLLOG, $host, $md5, $resto); + + if ($flood_row = mysql_fetch_assoc($result)) { + $last_time = (int)$flood_row['time']; + + $cooldown = RENZOKU_DUPE; + $cooldown_error = S_RENZOKU2_DUP; + + if ($captcha_bypass) { + $cooldown = ceil((int)$cooldown / 2); + } + else { + $cooldown_error .= S_RENZOKU_PASS; + } + + if ($last_time > $time - $cooldown) { + error(sprintf($cooldown_error, sec2hms($last_time + $cooldown - $time, false, true)), $dest); + } + } + } + + // Now the standard cooldown + $query = "SELECT time, resto, fsize FROM `%s` WHERE host = '%s' AND resto > 0 ORDER BY no DESC LIMIT 1"; + + $result = mysql_board_call($query, SQLLOG, $host); + + if ($flood_row = mysql_fetch_assoc($result)) { + $last_time = (int)$flood_row['time']; + + if ($has_image) { + $cooldown = RENZOKU2; + $cooldown_error = S_RENZOKU2; + } + else { + $cooldown = RENZOKU; + $cooldown_error = S_RENZOKU; + } + + if ($captcha_bypass) { + $cooldown = ceil((int)$cooldown / 2); + } + else { + $cooldown_error .= S_RENZOKU_PASS; + } + + if ($last_time > $time - $cooldown) { + error(sprintf($cooldown_error, sec2hms($last_time + $cooldown - $time, false, true)), $dest); + } + } + } + /* + if (SAVE_XFF == 1 && $xff) { + // Check for multiple ips with same xff + $result = mysql_global_call( "select count(distinct ip)>2 from xff where xff='%s' and is_live=1", $xff ); + if( mysql_result( $result, 0, 0 ) ) { + auto_ban_poster( $name, 14, 1, "Detected 3 proxies for same IP", "Proxy/Tor exit node." ); + error( S_GENERICERROR, $dest ); + } + // Check for multiple xffs with same ip? + } + */ + // Check for OP bump limiting + if ($resto && RENZOKU_OP) { + $query = 'SELECT host, time FROM `%s` WHERE no = %d'; + $query = mysql_board_call($query, SQLLOG, $resto); + if ($query) { + $result = mysql_fetch_assoc($query); + // Poster is OP + if ($result && $result['host'] === $host) { + // OP can only bump his thread RENZOKU_OP_TIME seconds after its creation + if ($result['time'] > ($time - RENZOKU_OP_TIME)) { + $is_sage = 1; + } + // OP can only bump his thread every RENZOKU_OP_TIME2 seconds + else { + $query2 = "SELECT time FROM `%s` WHERE host = '%s' AND resto = %d ORDER BY no DESC LIMIT 1"; + $query2 = mysql_board_call($query2, SQLLOG, $host, $resto); + if ($query2) { + $result = mysql_fetch_assoc($query2); + if ($result && $result['time'] > ($time - RENZOKU_OP_TIME2)) { + $is_sage = 1; + } + } + mysql_free_result($query2); + } + } + } + mysql_free_result($query); + } + } + + // Minimal cooldowns for authed users (3s) + if ($may_flood) { + $query = "SELECT time FROM `%s` WHERE host = '%s' ORDER BY no DESC LIMIT 1"; + + $result = mysql_board_call($query, SQLLOG, $host); + + if ($flood_row = mysql_fetch_assoc($result)) { + $last_time = (int)$flood_row['time']; + + $cooldown = 5; + + if ($last_time > $time - $cooldown) { + error(sprintf(S_RENZOKU, sec2hms($last_time + $cooldown - $time, false, true)), $dest); + } + } + } + + time_log( "fc" ); + + $tensorchan_score = 0; + + // thumbnail + $image_path = ""; + $m_img = false; + if ($has_image) { + if( USE_THUMB && !UPLOAD_BOARD ) { + // Detect and block NSFW content + $_need_inference = tensorchan_is_needed($userpwd, $resto, $W, $H, $ext); + + if ($_need_inference) { + $_tensor_png = false; + } + else { + $_tensor_png = null; + } + + $ret = make_thumb( $dest, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5, $webm_sar, $_tensor_png); + + if (!$ret && $ext != ".pdf") { + error(S_IMGFAIL, $dest); + } + + if ($_need_inference && $_tensor_png) { + $tensorchan_score = tensorchan_check_nsfw($_tensor_png); + unset($_tensor_png); + } + } + + $name_part = UPLOAD_BOARD ? $insfile : $tim; + $image_path = IMG_DIR . $name_part . $ext; + if( move_uploaded_file( $dest, $image_path ) === false ) { + error( S_FAILEDUPLOAD, $dest ); + } + chmod( $image_path, 0664 ); + + if (MOBILE_IMG_RESIZE) { + $m_img = resize_mobile_image($image_path, $W, $H, $fsize, $tim, $ext); + } + + // Oekaki + if (ENABLE_PAINTERJS) { + // Replays + if (ENABLE_OEKAKI_REPLAYS) { + $oe_replay_path = null; + + if (isset($_FILES['oe_replay']) && $_FILES['oe_replay']['name'] === 'tegaki.tgkr' && $insfile === 'tegaki') { + if (oekaki_validate_replay($_FILES['oe_replay']) === true) { + $oe_replay_path = IMG_DIR . $tim . '.tgkr'; + + if (move_uploaded_file($_FILES['oe_replay']['tmp_name'], $oe_replay_path) === false) { + error(S_FAILEDUPLOAD); + } + + chmod($oe_replay_path, 0664); + } + } + + // Oekaki meta + if (isset($_POST['oe_time'])) { + if (isset($_POST['oe_src']) && $resto) { + $oe_src_pid = oekaki_get_valid_src_pid($_POST['oe_src'], BOARD_DIR, $resto); + } + else { + $oe_src_pid = null; + } + + $com .= oekaki_format_info( + $_POST['oe_time'], + $oe_replay_path ? $tim : null, + $oe_src_pid + ); + } + } + } + } + + //logtime( "Thumbnail created" ); + time_log( "t" ); + + // Infrequent flood check (dupe image) + if( $has_image && (!$capcode || $capcode === 'none')) { + if ($resto) { + $result = mysql_board_call("SELECT sql_no_cache `no`,`resto` FROM `" . SQLLOG . "` WHERE archived = 0 and (resto = %d OR no = %d) AND `md5`='%s' AND filedeleted=0 limit 1", $resto, $resto, $md5); + } + else { + $result = mysql_board_call("SELECT sql_no_cache `no`,`resto` FROM `" . SQLLOG . "` WHERE archived = 0 AND resto = 0 AND `md5`='%s' AND filedeleted=0 limit 1", $md5); + } + + if( mysql_num_rows( $result ) ) { + list( $dupeno, $duperesto ) = mysql_fetch_row( $result ); + if( !$duperesto ) $duperesto = $dupeno; + error( '' . S_DUPE . ' here.', $dest ); + } + + if ($resto && MAX_IMG_REPOST_COUNT > 0) { + $_query = 'SELECT COUNT(*) FROM `' . SQLLOG . "` WHERE archived = 0 AND resto != 0 AND `md5` = '%s' AND filedeleted = 0"; + + $result = mysql_board_call($_query, $md5); + + if ($result) { + $_count = (int)mysql_fetch_row($result)[0]; + + if ($_count >= MAX_IMG_REPOST_COUNT) { + error(S_DUPE); + } + } + } + + if ( defined('SQLLOGMD5') ) { + // TODO: There's a race here. This should just be INSERT and check for failure! + $result = mysql_board_call("SELECT sql_no_cache * FROM `%s` WHERE md5='%s' AND now > DATE_SUB(NOW(), INTERVAL 1 DAY) limit 1", SQLLOGMD5, $md5); + if ( mysql_num_rows( $result ) ) { + list( $dc_now, $dc_filename, $dc_md5p ) = mysql_fetch_row( $result ); + + if( $dc_now ) { + error('Error: You must wait longer before reposting this file.', $dest ); + } + } + } + } + + $rootpredicate = $resto ? "0" : "now()"; + + // ROBOT9000 + if (defined('ROBOT9000') && ROBOT9000) { + // Logged in uses can bypass r9k by using the "bypass_r9k" command in the Options field. + // Capcoded posts always bypass r9k. + if (($options_field !== 'bypass_r9k' || !has_level('janitor')) && $capcode === 'none') { + require_once 'plugins/robot9000.php'; + $r9k_status = r9k_process($com, $md5, ip2long($host)); + if ($r9k_status !== R9K_OK) { + error($r9k_status, $dest ); + } + } + } + + // FIX ME, comments with html get truncated and break the layout, but only mods and janitors can bypass the regular limits + if (strlen($com) > 65536) { + error(S_TOOLONG, $dest); + } + + //logtime( "Before insertion" ); + + //find sticky & autosage + // auto-sticky + //$sticky = false; + // autosagin is now done in spam_filter_post_content + //$autosage = spam_filter_should_autosage( $com, $sub, $name, $fsize, $resto, $W, $H, $dest, $insertid ); + + //old auto-sticky code -- disabled + // if(defined('AUTOSTICKY') && AUTOSTICKY) { + // $autosticky = preg_split("/,\s*/", AUTOSTICKY); + // if($resto == 0) { + // if($insertid % 1000000 == 0 || in_array($insertid,$autosticky)) + // $sticky = true; + // } + // } + + $flag_cols = ""; + $flag_vals = ""; + + if( $captcha_bypass ) { + $flag_cols .= ',4pass_id'; + $flag_vals .= ",'" . $passid . "'"; + } + + if ($since4pass) { + $flag_cols .= ',since4pass'; + $flag_vals .= ",$since4pass"; + } + /* + if( $sticky ) { + $flag_cols .= ",sticky"; + $flag_vals .= ",1"; + } + */ + //permasage just means "is sage" for replies + if( $resto ? $is_sage : $autosage ) { + $flag_cols .= ",permasage"; + $flag_vals .= ",1"; + } + + if( $capcode ) { + $flag_cols .= ',capcode'; + $flag_vals .= ",'$capcode'"; + } + + if ($m_img) { + $flag_cols .= ',m_img'; + $flag_vals .= ",1"; + } + + //$country = geoip_country_code_by_addr( $_SERVER['REMOTE_ADDR'] ); + //if( !$country ) $country = 'XX'; + $geo_data = GeoIP2::get_country($_SERVER['REMOTE_ADDR']); + + if ($geo_data && isset($geo_data['country_code'])) { + $country = $geo_data['country_code']; + + // FIXME: football cups + /* + if (BOARD_DIR === 'sp' && $country === 'GB' && isset($geo_data['sub_code'])) { + if ($geo_data['sub_code'] === 'WLS') { + $country = 'XW'; + } + else if ($geo_data['sub_code'] === 'SCT') { + $country = 'XS'; + } + else if ($geo_data['sub_code'] !== 'NIR') { + $country = 'XE'; + } + } + */ + } + else { + $country = 'XX'; + } + + // User Agent ID + $browser_id = spam_filter_get_browser_id(); + + // Request Signature + $_req_sig = spam_filter_get_req_sig(); + + /** + * Flood checks + */ + $_threat_score = 0; + + if (!$captcha_bypass) { + $_pwd_known = $userpwd && $userpwd->isUserKnownOrVerified(360); + $_pwd_trusted = $userpwd && ($userpwd->postCount() > 10 || $userpwd->verifiedLevel()); + $_pwd_verified = $userpwd && $userpwd->verifiedLevel(); + + if (!$_pwd_known) { + $_threat_score = spam_filter_get_threat_score($country, !$resto, true); + } + + // Sample the user agent of known users + if (false && $userpwd && $userpwd->isUserKnownOrVerified(4320) && $userpwd->postCount() > 10) { + if (mt_rand(0, 9999) < 2000) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('log_safe_req', BOARD_DIR, $resto, $host, 1, $_bot_headers); + } + } + + if ($userpwd && !$userpwd->verifiedLevel() && $userpwd->postCount() > 0 && $userpwd->pwdLifetime() < 86400 && $userpwd->maskChanged()) { + log_spam_filter_trigger('log_mask_changed', BOARD_DIR, $resto, $host, 1); + } + + if (!$_pwd_known && ($_threat_score > 0.31)) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_txt_threat', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + if (false && !$_pwd_known && $resto == 18361689 && BOARD_DIR === 'fa' && mt_rand(0, 9) >= 1 && $country != 'US') { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_other', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'g' && strpos($_thread_sub, '/aicg/') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_aicg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_com, '/lolg/') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_lolg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + //show_post_successful_fake($resto); + //return; + } + + if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_com, '/overwatch') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_owg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + //show_post_successful_fake($resto); + //return; + } + + if (false && !$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'fa' && strpos($_thread_sub, 'Workwear General') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_denim', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + //show_post_successful_fake($resto); + //return; + } + + if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/bag/') !== false && $browser_id === '04d2237a2') { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_bag', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (false && !$_pwd_known && !$resto && (BOARD_DIR === 'co' || BOARD_DIR === 'a') && $country !== 'XX' && $browser_id === '02b99990d' && ($country == 'GB' || $country == 'DE' || $country == 'AU' || strpos($_COOKIE['_tcs'], $_SERVER['HTTP_X_TIMEZONE']) === false)) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_peridot', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, 'granblue') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_gbfg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + //show_post_successful_fake($resto); + //return; + } + + if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'v' && strpos($_thread_sub, 'gamesdonequick') !== false && $_threat_score >= 0.09 && mt_rand(0, 9) >= 1) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_adgq', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/zzz/') !== false && $_threat_score >= 0.09 && mt_rand(0, 9) >= 1) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_zzz', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + if (!$_pwd_verified && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/funkg/') !== false && $_threat_score >= 0.09) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_funkg', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + if (!$has_image) { + if (!$_pwd_verified && $_threat_score >= 0.09 && mt_rand(0, 9) >= 5 && BOARD_DIR !== 'f') { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_pub_prox', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + } + else { + if (!$resto) { + $_thres = 3; + } + else { + $_thres = 4; + } + + if (!$_pwd_verified && $_threat_score >= 0.09 && mt_rand(0, 9) >= $_thres && BOARD_DIR !== 'f') { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_pub_prox', BOARD_DIR, $resto, $host, 1, $_bot_headers); + if (mt_rand(0, 1)) { + error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + else { + show_post_successful_fake($resto); + return; + } + } + } + + if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'vg' && $country === 'US' && isset($_COOKIE['_tcs']) && strpos($_COOKIE['_tcs'], '.America/Bogota.') !== false) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('block_bag_scat', BOARD_DIR, $resto, $host, 1, $_bot_headers); + show_post_successful_fake($resto); + return; + } + + // FLOOD CHECK + $flood_status = 0;//spam_filter_is_post_flood($host, BOARD_DIR, $resto, null); + + if ($flood_status === 3) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + + log_spam_filter_trigger('block_flood_check', BOARD_DIR, $resto, $host, $flood_status, $_bot_headers); + + // Raw thread flood + if ($flood_status === 3) { + error(S_FAILEDUPLOAD); + } + else { + show_post_successful_fake($resto); + return; + } + } + + // Check Cloudflare's bot score and block using the lenient rangeban message + if ($userpwd && !$userpwd->isUserKnownOrVerified(1440) && isset($_SERVER['HTTP_X_BOT_SCORE'])) { + if (spam_filter_is_pwd_blocked($userpwd->getPwd(), 'block_bm_bot', 24)) { + log_spam_filter_trigger('block_bm_bot_pwd', BOARD_DIR, $resto, $host, $_SERVER['HTTP_X_BOT_SCORE']); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if (spam_filter_is_likely_automated($memcached)) { + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + write_to_event_log('block_bm_bot', $host, [ + 'pwd' => $userpwd->getPwd(), + 'arg_num' => $_SERVER['HTTP_X_BOT_SCORE'], + 'board' => BOARD_DIR, + 'thread_id' => $resto, + 'meta' => $_bot_headers + ]); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + } + + // If the country has changed, log the pwd and then block it for the next 24h + if ($userpwd && !$userpwd->verifiedLevel() && $userpwd->postCount() < 20) { + if (spam_filter_has_country_changed($userpwd->getPwd())) { + log_spam_filter_trigger('block_country_changed', BOARD_DIR, $resto, $host, 1); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + + if ($userpwd->envChanged()) { + write_to_event_log('country_changed', $host, [ + 'pwd' => $userpwd->getPwd(), + 'arg_str' => $country, + 'board' => BOARD_DIR, + 'thread_id' => $resto + ]); + + log_spam_filter_trigger('block_country_changed', BOARD_DIR, $resto, $host, 1); + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1); + } + } + } + + /** + * Custom flag selection + */ + if (ENABLE_BOARD_FLAGS) { + if ($_POST['flag'] === '0' || !isset($board_flags_array[$_POST['flag']])) { + $board_flag_code = ''; + + // FIXME: remove this eventually as we use localStorage now + if (isset($_COOKIE['4chan_flag'])) { + setcookie('4chan_flag', '', $time - 3600, '/', $cookie_domain); + } + } + else { + $board_flag_code = mysql_real_escape_string($_POST['flag']); + } + } + else { + $board_flag_code = ''; + } + + if ($board_flag_code) { + $board_flag_col = ',board_flag'; + $board_flag_val = ",'$board_flag_code'"; + } + else { + $board_flag_col = ''; + $board_flag_val = ''; + } + + if( $resto ) calculate_indexes_to_rebuild( $resto ); + + // Remove old replies if the thread is sticky+undead + if ($is_undead_sticky && STICKY_CAP > 1) { + $query = "SELECT MIN(no) FROM (SELECT no FROM `" . BOARD_DIR . "` WHERE resto = $resto ORDER BY no DESC LIMIT " . (STICKY_CAP - 1) . ") as subsel"; + $result = mysql_board_call($query); + if ($result) { + $prune_row = mysql_fetch_row($result); + + mysql_free_result($result); + + $min_no = (int)$prune_row[0]; + + if ($min_no > $resto) { + $query = "SELECT no FROM `" . BOARD_DIR . "` WHERE resto = $resto AND no < $min_no"; + $result = mysql_board_call($query); + + if ($result) { + while ($prune_row = mysql_fetch_assoc($result)) { + delete_post((int)$prune_row['no'], '', 0, 1, 1, 0); + } + + mysql_free_result($result); + } + } + } + } + + // April 2024 + /* + if ($_xa24_since4pass && !UPLOAD_BOARD && !JANITOR_BOARD) { + $name = april_2024_get_name(); + + if ($emails == '$DESU' && strlen($com) < 10000) { + $com .= '
    '; + } + } + */ + $user_meta = encode_user_meta($browser_id, substr($_req_sig, 0, 8), $userpwd); + + $insert_tries = 2; + do { + if( SKIP_DOUBLES == 1 ) mysql_board_call( "START TRANSACTION" ); + $query = "insert into `" . SQLLOG . "` (now,name,sub,com,host,pwd,email,filename,ext,w,h,tn_w,tn_h,tim,time,last_modified,md5,fsize,root,resto$flag_cols,tmd5,id,country$board_flag_col) values (" . + "'" . $now . "'," . + "'" . mysql_real_escape_string( $name ) . "'," . + mysql_nullify( mysql_real_escape_string( $sub ) ) . "," . + "'" . mysql_real_escape_string( $com ) . "'," . + "'" . mysql_real_escape_string( $host ) . "'," . + "'" . mysql_real_escape_string( $pass ) . "'," . + "'" . mysql_real_escape_string($user_meta) . "'," . + "'" . mysql_real_escape_string( $insfile ) . "'," . + mysql_nullify( $ext ) . "," . + (int)$W . "," . + (int)$H . "," . + (int)$TN_W . "," . + (int)$TN_H . "," . + "'" . $tim . "'," . + (int)$time . "," . + (int)$time . "," . + mysql_nullify( $md5 ) . "," . + (int)$fsize . "," . + $rootpredicate . "," . + (int)$resto . + $flag_vals . "," . + mysql_nullify( $tmd5 ) . "," . + mysql_nullify( $uid ) . "," . + "'$country'$board_flag_val)"; + + if( !$result = mysql_board_call( $query ) ) { + echo S_SQLFAIL; + } //post registration + time_log( "i" ); + + $insertid = mysql_board_insert_id(); + if( SKIP_DOUBLES == 1 ) { + if( has_doubles( $insertid ) ) { + mysql_board_call( "ROLLBACK" ); + // retry + } else { + mysql_board_call( "COMMIT" ); + $insert_tries = 0; + } + } else { + $insert_tries = 0; + } + } while( $insert_tries-- ); + + // Captcha bypass token + if ($captcha_bypass_allow_credits && $_threat_score < 0.15) { + set_twister_captcha_credits($memcached, $host, $userpwd, $time); + } + /* + if (!$captcha_bypass) { + if (mt_rand(0, 99) === 0) { + write_to_event_log('known_sample', $host, [ + 'board' => BOARD_DIR, + 'thread_id' => $resto, + 'arg_num' => $userpwd->isUserKnown(), + ]); + } + } + */ + + $userpwd->updatePostActivity(!$resto, $has_image); + + $userpwd->setCookie($cookie_domain); + + // April 2019 + /* + if (defined('LIKE_MAX_LIKES') && LIKE_MAX_LIKES > 0) { + like_update_post_score(); + } + */ + // Halloween 2017 + /* + if ($resto && defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky2017') { + process_halloween_score($com, $resto, $passid, $pass, $pass_is_bannable); + } + */ + // April 2018 + //update_april_team_scores(); + + // Log mod action if posted with html + if ($log_mod_action) { + $action_log_post = array( + 'no' => $insertid, + 'name' => $name, + 'sub' => $sub, + 'com' => $com, + 'filename' => $insfile, + 'ext' => $ext + ); + + if ($log_html_post) { + $action_log_post['com'] = htmlspecialchars($com, ENT_QUOTES); + log_mod_action(4, $action_log_post); + } + // capcode posting + if ($log_capcode_post) { + $action_log_post['name'] .= ' ## ' . ucfirst($capcode); + log_mod_action(5, $action_log_post, $capcode === 'verified'); + } + } + + if( $resto ) { //sage or age action + $resline = mysql_board_call( "select count(no) from `" . SQLLOG . "` where archived=0 and resto=" . $resto ); + $countres = mysql_result( $resline, 0, 0 ); + + $permasage_hours = (int)PERMASAGE_HOURS; + + if ($permasage_hours > 0) { + $time_col = 'time,'; + } + else { + $time_col = ''; + } + + // FIXME: a similar query is done at line ~4723 + $resline = mysql_board_call( "select {$time_col}sticky,permasage,permaage,root from `" . SQLLOG . "` where no=" . $resto ); + $resline = mysql_fetch_assoc($resline); + + if ($resline['sticky'] || $resline['permasage']) { + $root_col = ''; + } + else if ($resline['permaage']) { + $root_col = 'root=now(),'; + } + else if ($is_sage || $countres >= MAX_RES) { + $root_col = ''; + } + else if ($permasage_hours && ($time - ($permasage_hours * 3600) >= $resline['time'])) { + $root_col = ''; + } + else { + $root_col = 'root=now(),'; + + if (!$captcha_bypass && BOARD_DIR === 'jp') { + if (!spam_filter_can_bump_thread($resline['root'])) { + $root_col = ''; + $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig); + log_spam_filter_trigger('necrobump', BOARD_DIR, $resto, $host, 1, $_bot_headers); + } + } + } + + mysql_board_call("update `" . SQLLOG . "` set {$root_col}last_modified=%d where no=%d", $_SERVER['REQUEST_TIME'], $resto); + } + + if( defined( 'AUTOSTICKY' ) && AUTOSTICKY ) { + $autosticky = preg_split( "/,\s*/", AUTOSTICKY ); + if( $resto == 0 ) { + if( $insertid % 1000000 == 0 || in_array( $insertid, $autosticky ) ) { + $sticky = true; + mysql_board_call( "update " . SQLLOG . " set sticky=1,root=root where no=$insertid" ); + } + } + } + + if( SAVE_XFF == 1 && $xff ) { + mysql_global_do( "INSERT INTO xff (tim,board,xff,ip,postno,is_live) VALUES ('%s','%s','%s',%d,%d,1)", $tim, BOARD_DIR, $xff, ip2long( $host ), $insertid ); + } + + if (UPLOAD_BOARD && $md5 ) { + $result = mysql_board_call( "insert ignore into `%s` (filename,md5) values('%s','%s')", SQLLOGMD5, $insfile, $md5 ); + } + + // determine url to redirect to + $proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:"; + if( !$is_nonoko && !$resto ) { + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $insertid . PHP_EXT2; + } else if( !$is_nonoko ) { + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $resto . PHP_EXT2 . '#p' . $insertid; + } else { + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . '/'; + } + + // To let the JavaScript thread watcher know the newly created thread ID + if (!$resto && isset($_POST['awt'])) { + setcookie('4chan_awt', $insertid, 0, '/' . BOARD_DIR . '/', $cookie_domain); + } + + show_post_successful( $mes, $com, $insertid, $resto, $redirect, $delay_refresh ); + + $static_rebuild = ( STATIC_REBUILD == 1 ); + //logtime( "Before trim_db" ); + + // trim database + if (!$resto) { + + if (!$static_rebuild) { + trim_db(); + + if (ENABLE_ARCHIVE && ARCHIVE_MAX_AGE) { + trim_archive(); + } + } + } + + //logtime( "After trim_db" ); + //time_log( "tr" ); + + $_need_updatelog = true; + + if (AUTOARCHIVE_CAP && $resto && !$sticky && !$undead && ENABLE_ARCHIVE) { + if (count_thread_replies(BOARD_DIR, $resto) >= AUTOARCHIVE_CAP) { + $_need_updatelog = false; + archive_thread($resto); + } + } + + // update html + if ($_need_updatelog) { + updatelog( $resto ? $resto : $insertid ); + } + //logtime( "Pages rebuilt" ); + //time_log( "r" ); + + // late tasks happen below here + iplog_add( BOARD_DIR, $insertid, $host, $time, $resto == 0, $tim, $has_image ); + + // Auto-report possibly nsfw post + if ($tensorchan_score && $tensorchan_score > 0.5) { + tensorchan_log(BOARD_DIR, $insertid, $resto, $tim, $ext, $tensorchan_score); + } + } else { + // silent reject + $insertid = 0; + $noko = 0; + } + /* + if( STATS_USER_JS ) { + mysql_global_do( "UPDATE `user_stats` SET `count` = `count`+1 WHERE name='%s'", $stats_ok ); + } + */ +} + +// Redirects to the most rcently created thread +// This is to confuse spambots +function show_post_successful_fake($resto = 0, $captcha_passed = true) { + $thread_id = (int)$resto; + $insert_id = 0; + + if (!$resto) { + $query = 'SELECT resto FROM `' . BOARD_DIR . '` WHERE resto != 0 ORDER BY resto DESC LIMIT 1'; + $res = mysql_board_call($query); + if ($res) { + $row = mysql_fetch_row($res); + $insert_id = (int)$row[0]; + } + } + else { + $query = 'SELECT no FROM `' . BOARD_DIR . '` ORDER BY no DESC LIMIT 1'; + $res = mysql_board_call($query); + if ($res) { + $row = mysql_fetch_row($res); + $insert_id = (int)$row[0] + 1; + } + } + + if (!$thread_id) { + $redirect = 'https://boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $insert_id . PHP_EXT2; + } + else { + $redirect = 'https://boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $thread_id . PHP_EXT2 . '#p' . $insert_id; + } + + $cookie_domain = '.' . L::d(BOARD_DIR); + + $now = $_SERVER['REQUEST_TIME']; + + // Name cookie + $c_name = $_POST['name']; + setrawcookie('4chan_name', rawurlencode($c_name), $now + ($c_name ? (7 * 24 * 3600) : -3600), '/', $cookie_domain); + + // Password cookie + $userpwd = UserPwd::getSession(); + + if ($userpwd) { + if ($captcha_passed) { + $userpwd->setCookie($cookie_domain); + } + else if (!$userpwd->isFake() && !$userpwd->isNew()) { + $userpwd->setCookie($cookie_domain); + } + else { + UserPwd::setFakeCookie($now, $cookie_domain); + } + } + + show_post_successful(null, null, $insert_id, $thread_id, $redirect); +} + +function show_post_successful( $mes, $com, $insertid, $resto, $redirect, $delay_refresh = false ) { + if (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json') { + return show_post_successful_json($insertid, $resto); + } + + if( !$mes ) $mes = S_POSTING_DONE; + $time_to_refresh = ( $delay_refresh ) ? 10 : 1; + $script = ""; + if( defined( 'POST_SUCCESSFUL_FILE' ) ) { + $file = file_get_contents( POST_SUCCESSFUL_FILE ); + $success = str_replace( "@REDIRECT@", $redirect, $file ); + } else { + // FIXME templating + $icon = DEFAULT_BURICHAN ? 'favicon-ws.ico' : 'favicon.ico'; + $defaultcss = DEFAULT_BURICHAN ? 'yotsubluenew' : 'yotsubanew'; + $cssVersion = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION; + $sg = style_group(); + + $styles = array( + 'Yotsuba New' => "yotsubanew.$cssVersion.css", + 'Yotsuba B New' => "yotsubluenew.$cssVersion.css", + 'Futaba New' => "futabanew.$cssVersion.css", + 'Burichan New' => "burichannew.$cssVersion.css", + 'Photon' => "photon.$cssVersion.css", + 'Tomorrow' => "tomorrow.$cssVersion.css" + ); + + $css = ''; + + if( isset( $_COOKIE[$sg] ) ) { + if( isset( $styles[$_COOKIE[$sg]] ) ) { + $css = ''; + } + } else { + $dcssl = $defaultcss . '.' . $cssVersion . '.css'; + $css = ''; + } + + if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) { + $css = ''; + } + + if( BOARD_DIR == 'j' ) { + $css = ''; + } + + + $script .= ''; + $success = "$script" . S_POSTING_DONE . "$css

    $mes

    "; + } + echo $success; + + if ($resto) { + fastcgi_finish_request(); + } +} + +function show_post_successful_json($post_id, $thread_id) { + header('Content-Type: application/json'); + + echo '{"tid":' . $thread_id . ',"pid":' . $post_id . '}'; + + if ($thread_id) { + fastcgi_finish_request(); + } +} + +function resredir( $res, $delete = 0, $no_exit = false ) { + if (!$_SERVER["HTTP_REFERER"]) { + $proto = 'https:'; + } + else { + $proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:"; + } + + $res = (int)$res; + //mysql_board_lock( true ); + if( !$redir = mysql_board_call( "select no,resto from `" . SQLLOG . "` where no=" . $res ) ) { + echo S_SQLFAIL; + } + list( $no, $resto ) = mysql_fetch_row( $redir ); + + // if we're deleting and no post/resto (thread gone) + if( !$no && $delete ) { + // send us back to the board + updating_index(); + //mysql_board_unlock(); + if (!$no_exit) { + die; + } + else { + return; + } + } + + if( !JANITOR_BOARD ) { + header("Cache-Control: public, max-age=2"); + } + + if( !$no ) { + // If no < max(no) then this could be 410 Gone. + http_response_code(404); + error( S_NOTHREADERR, $dest ); + } + + if( $resto == "0" ) { // thread + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $no . PHP_EXT2 . '#p' . $no; + } else { + $redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $resto . PHP_EXT2 . '#p' . $no; + } + + $redirect = JANITOR_BOARD ? str_replace( 'boards.', 'sys.', $redirect ) : $redirect; + + header("Location: $redirect", true, 301); + echo ""; + //mysql_board_unlock(); +} + +function tensorchan_is_needed($userpwd, $resto, $W, $H, $ext) { + // Inference is disabled + if (!defined('TENSORCHAN_MODE') || !TENSORCHAN_MODE) { + return false; + } + + // Can't check + if (!$userpwd) { + return false; + } + + // User is known or verified + if ($userpwd->isUserKnownOrVerified(240)) { // 4 hours + return false; + } + + // OPs only but the post is a reply + if (TENSORCHAN_MODE == 1 && $resto) { + return false; + } + + if ($W < 150 || $H < 150 || $ext == '.pdf') { + return false; + } + + return true; +} + +function tensorchan_check_nsfw($tensor_png) { + if (!$tensor_png) { + return false; + } + + $tensor_res = tensorchan_predict($tensor_png); + + if (!$tensor_res) { + return false; + } + + if (isset($tensor_res['error'])) { + write_to_event_log('tensor_err', $_SERVER['REMOTE_ADDR'], [ + 'board' => BOARD_DIR, + 'meta' => htmlspecialchars($tensor_res['error']) + ]); + + return false; + } + else { + if (!isset($tensor_res['nsfw'])) { + return false; + } + + return (float)$tensor_res['nsfw']; + } +} + +function tensorchan_log($board, $post_id, $thread_id, $file_id, $file_ext, $score) { + $post_id = (int)$post_id; + $thread_id = (int)$thread_id; + $score = (float)$score; + + $sql =<< $_err]; + } + + $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($resp_status >= 300) { + return ["error" => "HTTP $resp_status: $resp"]; + } + + curl_close($curl); + + if ($resp[0] == '{') { + $resp = json_decode($resp, true); + } + else { + return ["error" => "Not a JSON response"]; + } + + return $resp; +} + +function background_color( $im, $is_thread ) +{ + if( DEFAULT_BURICHAN == 0 ) { + if( $is_thread ) { + list( $r, $g, $b ) = array(0xFF, 0xFF, 0xEE); + } else { + list( $r, $g, $b ) = array(0xF0, 0xE0, 0xD6); + } + } else { + if( $is_thread ) { + list( $r, $g, $b ) = array(0xEE, 0xF2, 0xFF); + } else { + list( $r, $g, $b ) = array(0xD6, 0xDA, 0xF0); + } + } + + return imagecolorallocate( $im, $r, $g, $b ); +} + +function optimize_thumb($tmppath) { + system("/usr/local/bin/jpegoptim -q --strip-all '$tmppath' >/dev/null 2>&1"); +} + +// Calculates perceptual hash for a thumbnail +// $img is a reference to a GD resource +function get_thumb_dhash(&$img, $width, $height) { + if (!$img) { + return false; + } + + $data = imagecreatetruecolor(9, 8); + imagecopyresampled($data, $img, 0, 0, 0, 0, 9, 8, $width, $height); + imagefilter($data, IMG_FILTER_GRAYSCALE); + + $hash = 0; + $bit = 1; + + for ($y = 0; $y < 8; $y++) { + $previous = imagecolorat($data, 0, $y) & 0xFF; + + for ($x = 1; $x < 9; $x++) { + $current = imagecolorat($data, $x, $y) & 0xFF; + + if ($previous > $current) { + $hash |= $bit; + } + + $bit = $bit << 1; + $previous = $current; + } + } + + imagedestroy($data); + + return sprintf("%016x", $hash); +} + +//thumbnails +function make_thumb( $fname, $tim, $ext, $resto, &$TN_W, &$TN_H, &$tmd5, $webm_sar = null, &$tensor_png = null ) +{ + $thumb_dir = THUMB_DIR; //thumbnail directory + $outpath = $thumb_dir . $tim . 's.jpg'; + if( !$resto ) { + $width = MAX_W; //output width + $height = MAX_H; //output height + $jpeg_quality = 50; + } else { + $width = MAXR_W; //output width (imgreply) + $height = MAXR_H; //output height (imgreply) + $jpeg_quality = 40; + } + + if( ENABLE_PDF == 1 && $ext == '.pdf' ) { + // create jpeg for the thumbnailer + $pdfjpeg = IMG_DIR . $tim . '.pdf.tmp'; + @exec( "/usr/local/bin/gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=jpeg -sOutputFile=$pdfjpeg $fname" ); + if( !file_exists( $pdfjpeg ) ) unlink( $fname ); + $fname = $pdfjpeg; + } + else if (ENABLE_WEBM && ($ext == '.webm' || $ext == '.mp4')) { + $webm_thumb = thumb_webm($fname, $ext); + + if (!$webm_thumb) { + unlink($fname); + return false; + } + + $fname = $webm_thumb; + } + + $size = @GetImageSize($fname); + + if ($size === false) { + return; + } + + // File size needs to be checked again because of cleanup_uploaded_file + if (defined('MAX_DIMENSION') && $ext == '.gif') { + if ($size[0] > MAX_DIMENSION || $size[1] > MAX_DIMENSION) { + error(S_TOOLARGERES); + } + } + + $memory_limit_increased = false; + $maybe_transparent = true; + // Don't increase memory limit on CLI so that the user can do it with a CLI parameter instead + if( $size[0] * $size[1] > 3000000 && isset($_SERVER['REMOTE_ADDR']) ) { + $memory_limit_increased = true; + ini_set( 'memory_limit', memory_get_usage() + $size[0] * $size[1] * 15 ); // for huge images + } + switch( $size[2] ) { + case 1 : + $im_in = ImageCreateFromGIF( $fname ); + if (!$im_in) { + return; + } + break; + case 2 : + $im_in = ImageCreateFromJPEG( $fname ); + if( !$im_in ) { + return; + } + $maybe_transparent = false; + break; + case 3 : + $im_in = ImageCreateFromPNG( $fname ); + if( !$im_in ) { + return; + } + break; + default : + return; + } + + $source_w = $size[0]; + $source_h = $size[1]; + + if ($webm_sar) { + if ($webm_sar > 1) { + $size[1] = round($size[1] / $webm_sar); + + if ($size[1] == 0) { + $size[1] = 1; + } + } + else { + $size[0] = round($size[0] * $webm_sar); + + if ($size[0] == 0) { + $size[0] = 1; + } + } + } + + // Resizing + if( $size[0] > $width || $size[1] > $height ) { + $key_w = $width / $size[0]; + $key_h = $height / $size[1]; + ( $key_w < $key_h ) ? $keys = $key_w : $keys = $key_h; + $out_w = floor( $size[0] * $keys ); + $out_h = floor( $size[1] * $keys ); + } else { + $out_w = $size[0]; + $out_h = $size[1]; + } + + // the thumbnail is created + $im_out = ImageCreateTrueColor( $out_w, $out_h ); + if( !$im_out ) return; + if( $maybe_transparent ) { + $background = background_color( $im_out, $resto == 0 ); + ImageFill( $im_out, 0, 0, $background ); + } + // copy resized original + ImageCopyResampled( $im_out, $im_in, 0, 0, 0, 0, $out_w, $out_h, $source_w, $source_h ); + $tmppath = tempnam( ini_get( "upload_tmp_dir" ), "thumb" ); + // thumbnail saved + ImageJPEG( $im_out, $tmppath, $jpeg_quality ); + + // Generate a perceptual hash from the original image + $tmd5 = Phash::hash($im_in, $source_w, $source_h); + + if ($tmd5 === false) { + $tmd5 = ''; + } + + // Create the PNG file for inference + if ($tensor_png !== null) { + $tensor_png_dim = TENSORCHAN_DIM; + $tensor_png_out = ImageCreateTrueColor($tensor_png_dim, $tensor_png_dim); + ImageCopyResampled($tensor_png_out, $im_in, + 0, 0, 0, 0, + $tensor_png_dim, $tensor_png_dim, $source_w, $source_h + ); + $stream = fopen('php://memory','r+'); + imagepng($tensor_png_out, $stream); + rewind($stream); + $tensor_png = stream_get_contents($stream); + fclose($stream); + ImageDestroy($tensor_png_out); + } + + // Cleanup + ImageDestroy( $im_in ); + ImageDestroy( $im_out ); + + optimize_thumb($tmppath); + + //$tmd5 = md5_file( $tmppath ); + rename_across_device( $tmppath, $outpath ); + + // if PDF was thumbnailed delete the orig jpeg + if (isset($pdfjpeg)) { + unlink($pdfjpeg); + } + // delete original webm frame + else if (isset($webm_thumb)) { + unlink($webm_thumb); + } + + if( $memory_limit_increased ) + ini_restore( 'memory_limit' ); + + $TN_W = $out_w; + $TN_H = $out_h; + + return $outpath; +} + +/* text plastic surgery */ +// you can call with skip_bidi=1 if cleaning a paragraph element (like $com) +function sanitize_text( $str, $skip_bidi = 0, $allow_html = false ) +{ + global $admin, $html; + // stupid unicode-hack removal + if( BOARD_DIR != 'jp' && BOARD_DIR != 'a' && BOARD_DIR != 'b' && !SJIS_TAGS ) { + $str = preg_replace( '#[\x{00A0}\x{3000}]#u', ' ', $str ); + + } + + if( !CODE_TAGS && !SJIS_TAGS) { + $str = preg_replace( "/([ \t\f]|\xE2\x80\x8B|\xE2\x80\xA9)+/", " ", $str ); //collapse multiple spaces like HTML does + } else { + // fix tabs for html compression + $str = str_replace( "\t", " ", $str ); + } + + $str = trim( $str ); //blankspace removal + if( get_magic_quotes_gpc() ) { //magic quotes is deleted (?) + $str = stripslashes( $str ); + } + + if ($allow_html && $html == 1 && (has_level('manager') || has_flag('html') || has_flag('developer'))) { + $str = purify_html($str); + } else { + $str = htmlspecialchars( $str, ENT_QUOTES ); + } + + if( $skip_bidi == 0 ) { + // fix malformed bidirectional overrides - insert as many PDFs as RLOs + //RLO + $str .= str_repeat( "\xE2\x80\xAC", substr_count( $str, "\xE2\x80\xAE" /* U+202E */ ) ); + $str .= str_repeat( "‬", substr_count( $str, "‮" ) ); + $str .= str_repeat( "‬", substr_count( $str, "‮" ) ); + //RLE + $str .= str_repeat( "\xE2\x80\xAC", substr_count( $str, "\xE2\x80\xAB" /* U+202B */ ) ); + $str .= str_repeat( "‬", substr_count( $str, "‫" ) ); + $str .= str_repeat( "‬", substr_count( $str, "‫" ) ); + } + + return $str; +} + +// TODO: rewrite this to use less SQL queries +function report() { + global $captcha_bypass; + + $host = $_SERVER['REMOTE_ADDR']; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($host, MAIN_DOMAIN, $_COOKIE['4chan_pass']); + } + else { + $userpwd = new UserPwd($host, MAIN_DOMAIN); + } + + if (BOARD_DIR === 'test') { + require_once('forms/report-test.php'); + require_once('modes/report-test.php'); + } + else { + require_once('forms/report.php'); + require_once('modes/report.php'); + } + + if (!CAN_REPORT_POSTS) { + fancydie(S_CANNOTREPORTPOSTS); + } + + $no = (int)$_GET['no']; + + if ($no <= 0) { + fancydie(S_POST_DEAD); + } + + $post = report_check_post(BOARD_DIR, $no); + + if (!isset($_COOKIE['4chan_auser']) && !isset($_COOKIE['pass_enabled'])) { + $no_captcha = report_can_bypass_captcha($host, $userpwd, $post); + } + else { + $no_captcha = false; + } + + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + header( 'Cache-Control: private, no-cache, must-revalidate' ); + header( 'Expires: -1' ); + + // Doesn't check bans here + report_check_ip( BOARD_DIR, $no, false); + + form_report(BOARD_DIR, $no, $no_captcha); + } + else { + if (valid_captcha_bypass() !== true && $no_captcha !== true) { + if (CAPTCHA_TWISTER) { + $_m = create_memcached_instance(); + + if (isset($_POST['t-challenge']) && $_POST['t-challenge'] === 'noop') { + if (use_twister_captcha_credit($_m, $host, $userpwd) === false) { + error(S_CAPTCHATIMEOUT); + } + } + else if (is_twister_captcha_valid($_m, $host, $userpwd, BOARD_DIR, 1) === false) { + error(S_BADCAPTCHA); + } + } + else { + start_recaptcha_verify(); + + if (!$captcha_bypass) { + end_recaptcha_verify(); + } + } + } + + // Also checks for bans + report_check_ip(BOARD_DIR, $no, true); + + if (!isset($_POST['cat']) && !isset($_POST['cat_id'])) { + fancydie('Invalid category selected.'); + } + + if ($_POST['cat']) { + $cat_id = (int)$_POST['cat']; + } + else if ($_POST['cat_id']) { + $cat_id = (int)$_POST['cat_id']; + } + else { + $cat_id = null; + } + + if (!$cat_id) { + fancydie('Invalid category selected.'); + } + /* + if ($no_captcha) { + write_to_event_log('skip_rep_captcha', $host, [ + 'board' => BOARD_DIR, + 'thread_id' => $post['resto'] ? $post['resto'] : $post['no'], + 'post_id' => $post['no'] + ]); + } + */ + report_submit(BOARD_DIR, $no, $cat_id); // script dies here + } + + die( '' ); +} + +/** + * Archive deletion function + * only works on archived posts, for authed users + */ +function arcdel($no, $redirect = false, $redirect_res = null) { + global $onlyimgdel; + + $delno = array(); + $time = $_SERVER['REQUEST_TIME']; + reset( $_POST ); + + while ($item = each($_POST)) { + if ($item[1] == 'delete') { + $delno[] = $item[0]; + } + } + + $numdeletions = count($delno); + + if (!$numdeletions) { + return; + } + + $rebuild_archive_json = false; + + $rebuild = array(); + + for ($i = 0; $i < $numdeletions; $i++) { + $resto = delete_post($delno[$i], '', $onlyimgdel, 0, 1, $numdeletions == 1, false, true); + if ($resto) { + $rebuild[$resto] = true; + } + else if (!$onlyimgdel) { + $rebuild_archive_json = true; + } + } + + if (!has_level('janitor')) { + mysql_global_call("INSERT INTO user_actions (ip,board,action,postno,time) VALUES (%d,'%s','delete',%d,now())", ip2long( $_SERVER["REMOTE_ADDR"] ), BOARD_DIR, $delno[0]); + } + + if ($redirect) { + if ($redirect_res) { + resredir($redirect_res, 1, true); + } + else { + updating_index(); + } + + fastcgi_finish_request(); + } + + foreach ($rebuild as $thread_id => $true) { + rebuild_archived_thread($thread_id); + } + + if ($rebuild_archive_json && ENABLE_JSON_THREADS) { + generate_board_archived_json(); + } +} + +function user_delete( $no, $pwd, $redirect = false, $redirect_res = null ) +{ + global $pwdc, $onlyimgdel, $captcha_bypass; + + if (UPLOAD_BOARD && $onlyimgdel) { + error("It doesn't make any sense to do a file-only delete on a file board!"); + } + + $delno = array(); + $time = $_SERVER['REQUEST_TIME']; + $delflag = false; + reset( $_POST ); + + while( $item = each( $_POST ) ) { + if( $item[1] == 'delete' ) { + array_push( $delno, $item[0] ); + $delflag = true; + } + } + + $user_is_known = false; + + if ($pwdc) { + $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], MAIN_DOMAIN, $pwdc); + + if ($userpwd) { + $pwd = $userpwd->getPwd(); + $user_is_known = $userpwd->maskLifetime() >= 900; + } + else { + $pwd = null; + } + } + else { + $pwd = null; + } + + $numdeletions = count( $delno ); + if( !$numdeletions ) return; + $flag = false; + + if( !has_level( 'janitor' ) ) { + $n = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='delete' and time >= subdate(now(), interval 1 hour)", RENZOKU_DEL_HOURLY, ip2long( $_SERVER['REMOTE_ADDR'] ) ); + list( $h ) = mysql_fetch_row( $n ); + + if( $h ) { + //check_fail_floodcheck($no); + error(S_FLOOD_DEL); + } + + $n = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='delete' and time >= subdate(now(), interval 1 day)", RENZOKU_DEL_DAILY, ip2long( $_SERVER['REMOTE_ADDR'] ) ); + list( $h ) = mysql_fetch_row( $n ); + + if( $h ) { + //check_fail_floodcheck($no); + error(S_FLOOD_DEL); + } + } + + $rebuild = array(); // keys are pages that need to be rebuilt (0 is index, of course) + + $lazy_rebuild = false; + + if (isset($_POST['tool']) && $_POST['tool']) { + $tool = $_POST['tool']; + } + else { + $tool = null; + } + + // Manual, single post deletion. Only rebuilds one page if deleting a reply. + if ($numdeletions == 1) { + $resto = delete_post( $delno[0], $pwd, $onlyimgdel, 0, 1, $numdeletions == 1, false, false, $tool, $user_is_known ); + if ($resto) { + $rebuild[$resto] = 1; + calculate_indexes_to_rebuild($resto); + $lazy_rebuild = true; + } + } + // Other (multi, automatic, etc...) + else { + for( $i = 0; $i < $numdeletions; $i++ ) { + $resto = delete_post( $delno[$i], $pwd, $onlyimgdel, 0, 1, $numdeletions == 1, false, false, $tool, $user_is_known ); + if( $resto ) { + $rebuild[$resto] = 1; + } + } + } + + if (!has_level('janitor')) { + mysql_global_call("INSERT INTO user_actions (ip,board,action,postno,time) VALUES (%d,'%s','delete',%d,now())", ip2long( $_SERVER["REMOTE_ADDR"] ), BOARD_DIR, $delno[0]); + } + + if ($redirect) { + if ($redirect_res) { + resredir($redirect_res, 1, true); + } + else { + updating_index(); + } + + fastcgi_finish_request(); + } + + rebuild_deletions($rebuild, $lazy_rebuild); +} + +function updatelog_remote( $no, $noidx ) +{ + if( !has_level() ) die( '' ); // anti dos + $no = intval( $no ); + $noidx = !!$noidx; + // FIXME, running this when $noidx is true breaks cross-thread quotelinks. + updatelog( $no, $noidx ); +} + +function _print( $s, $echo = 1 ) +{ + if( $echo ) { + echo $s; + + return; + } + + ob_flush(); + flush(); + + echo $s; + echo str_repeat( ' ', 256 ) . "\n"; + + ob_flush(); + flush(); +} + +function fancystyle() +{ + $style = << +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 12pt; +} + +h1 { + margin: 0; + padding: 0; +} + +HTML; + + + return $style; +} + +function rebuild_catalog( $shutup = false ) +{ + if( !has_level() ) die(); + if( !$shutup ) { + echo fancystyle(); + echo '

    Rebuilding catalog...

    '; + } + + $start = microtime( true ); + generate_catalog(); + $time = round( microtime( true ) - $start, 6 ); + + if( !$shutup ) { + echo 'Done!

    Rebuilding took ' . $time . ' seconds.

    Redirecting to catalog...

    '; + die(); + } +} + +function rebuild_boards_json() +{ + if( !has_level() ) die(); + echo fancystyle(); + echo '

    Rebuilding boards.json...

    '; + + $start = microtime( true ); + $query = mysql_global_call( "SELECT dir as board,name as title FROM boardlist ORDER BY board ASC" ); + + $boards = array(); + + $host = 'https://sys.int'; + + $post = array( + 'mode' => 'cataloginfo' + ); + + while( $row = mysql_fetch_assoc( $query ) ) { + if( $row['board'] == 'vp' ) $row['title'] = 'Pokémon'; + //cataloginfo + $url = "$host/{$row['board']}/imgboard.php"; + + $ch = rpc_start_request($url, $post, null, true); + + $response = rpc_finish_request($ch, $error, $httperror); + + if (!$response) { + die( 'Could not generate info for /' . $row['board'] . '/; ' . $error ); + } + + $json = json_decode($response, true); + + foreach( $json as $key => $val ) { + if( $key != 'board' && ctype_digit( $val ) ) $val = (int)$val; + $row[$key] = $val; + } + + $boards['boards'][] = $row; + } + + // Dump resulting json on /test/ + if (BOARD_DIR === 'test') { + echo '
    '; + echo json_encode($boards, JSON_HEX_AMP | JSON_PRETTY_PRINT); + echo '
    '; + } + else { + $_json = json_encode($boards, JSON_HEX_AMP); + print_page(BOARDS_ROOT . 'boards.json', $_json); + } + + $time = round( microtime( true ) - $start, 6 ); + echo 'Done!

    Rebuilding took ' . $time . ' seconds.

    No redirect here boss. Off you go.'; + die(); +} + +function rebuild( $all = 0 ) +{ + global $rebuildall, $fwritetimer; + if( !has_level() ) die( '' ); // anti dos + + if (has_flag('developer')) { + error_reporting(E_ALL); + } + + header( "Pragma: no-cache" ); + _print(fancystyle()); + $l = $all ? 'all' : 'missing'; + + _print( "Rebuilding $l replies and pages... Go back

    \n" ); + log_cache(); + trim_db(); + trim_archive(); + mysql_board_lock( true ); + $starttime = microtime( true ); + $query = "SELECT no, resto FROM `" . SQLLOG . "` WHERE resto = 0 AND archived = 0 ORDER BY root DESC"; + $treeline = mysql_board_call($query); + if (!$treeline) { + echo S_SQLFAIL; + } + mysql_board_unlock(); + _print( "Writing...
    \n" ); + if( $all ) { + while( list( $no, $resto ) = mysql_fetch_row( $treeline ) ) { + if( !$resto ) { + _print( "Writing No.$no... " ); + updatelog( $no, 1 ); + $ext = TEST_BOARD ? "rp cache: ".realpath_cache_size() : ""; + _print( "DONE!
    $ext\n" ); + } + } + _print( "Writing index pages... " ); + updatelog(); + _print( "DONE!
    " ); + + if (ENABLE_CATALOG) { + _print( "Writing catalog..." ); + generate_catalog( true ); + _print( "DONE!
    " ); + } + + if (ENABLE_ARCHIVE) { + _print( "Writing archive..." ); + rebuild_archive_list(); + _print( "DONE!
    " ); + } + } + + $totaltime = microtime( true ) - $starttime; + $proctimer = $totaltime - $fwritetimer; + + $peakmem = memory_get_peak_usage(true) / (1024*1024.0); + $usedmem = memory_get_usage(true) / (1024*1024.0); + + $redir = '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . '/'; + +echo <<Total running time (lock excluded): $totaltime seconds. +
    Composed of: +
    Time spent writing files: $fwritetimer seconds. +
    Time spent processing: $proctimer seconds. +
    Memory: Peak: $peakmem MB Final: $usedmem MB +
    Pages created. +

    +END; +/* +if (!TEST_BOARD) { +echo << +END; +} +*/ +} + +function rebuild_after_deletion( $no ) +{ + if( !has_level() ) die(); + + mysql_board_lock( true ); + + if( !$treeline = mysql_board_call( "SELECT no FROM `" . SQLLOG . "` WHERE no = %d", $no ) ) { + mysql_board_unlock(); + die( S_POSTGONE ); + } + + log_cache( 0, $no ); + mysql_board_unlock(); + + updatelog( $no, 1 ); + + die( $no . ' Rebuilt OK!' ); +} + +function updating_index() +{ + $proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:"; + echo "" + . S_UPDATING_INDEX . "
    " + . S_UPDATING_INDEX . "
    "; +} + +function require_request_method( $method ) +{ + if (!isset($_SERVER['REMOTE_ADDR'])) return; + $umethod = $_SERVER["REQUEST_METHOD"]; + //$req = htmlspecialchars( $_REQUEST["mode"] ); + if( $umethod == "OPTIONS" || ( $umethod == "HEAD" && $method == "GET" ) ) return; + if( $umethod != $method ) { + error( S_REJECTTEXTBAN ); + } +} + +function get_catalog_info() { + $arr = array( + 'ws_board' => (int)(CATEGORY == 'ws'), + 'per_page' => (int)DEF_PAGES, + 'pages' => (int)PAGE_MAX, + 'max_filesize' => ((int)MAX_KB) * 1024, + 'max_webm_filesize' => ((int)MAX_WEBM_FILESIZE) * 1024, + 'max_comment_chars' => (int)MAX_COM_CHARS, + 'max_webm_duration' => (int)MAX_WEBM_DURATION, + 'bump_limit' => (int)MAX_RES, + 'image_limit' => (int)MAX_IMGRES, + 'cooldowns' => array( + 'threads' => (int)RENZOKU3, + 'replies' => (int)RENZOKU, + 'images' => (int)RENZOKU2 + ) + ); + + if (defined('META_DESCRIPTION')) { + $arr['meta_description'] = META_DESCRIPTION; + } + + if (SPOILERS) { + $arr['spoilers'] = 1; + if (SPOILER_NUM) { + $arr['custom_spoilers'] = (int)SPOILER_NUM; + } + } + + if (DISP_ID) { + $arr['user_ids'] = 1; + } + + if (ENABLE_ARCHIVE) { + $arr['is_archived'] = 1; + } + + if (CODE_TAGS) { + $arr['code_tags'] = 1; + } + + if (SJIS_TAGS) { + $arr['sjis_tags'] = 1; + } + + if (JSMATH) { + $arr['math_tags'] = 1; + } + + if (SHOW_COUNTRY_FLAGS) { + $arr['country_flags'] = 1; + } + + if (ENABLE_BOARD_FLAGS) { + $arr['board_flags'] = get_board_flags_selector(); + } + + if (ENABLE_WEBM_AUDIO) { + $arr['webm_audio'] = 1; + } + + if (MIN_W > 1) { + $arr['min_image_width'] = (int)MIN_W; + } + + if (MIN_H > 1) { + $arr['min_image_height'] = (int)MIN_H; + } + + if (TEXT_ONLY) { + $arr['text_only'] = 1; + $arr['require_subject'] = 1; + } + + if (FORCED_ANON) { + $arr['forced_anon'] = 1; + } + + if (REQUIRE_SUBJECT) { + $arr['require_subject'] = 1; + } + + if (ENABLE_PAINTERJS) { + $arr['oekaki'] = 1; + } + + die(json_encode($arr)); +} + +/** + * Generates context to append to thread links. + */ +function generate_href_context($sub, $com) { + if (JANITOR_BOARD) { + return ''; + } + + $context = ''; + + if (strpos($sub, 'SPOILER<>') === 0) { + $sub = substr($sub, 9); + } + + if ($sub !== '') { + $context = cleanup_context_string($sub); + } + + if ($context === '' && $com !== '') { + $context = $com; + + if (strpos($context, '
    ') !== false) { + $context = str_replace('
    ', "\n", $context); + $has_br = true; + } + else { + $has_br = false; + } + + $context = preg_replace('/(^|\s)https?:\/\/[^\s]{4,}/', '', $context); + + if (strpos($context, '') !== false) { + $context = preg_replace('/.*<\/table>/', '', $context); // ??? + } + if (strpos($context, ']+>.*<\/strong>/', '', $context); // ??? + } + + if ($has_br) { + $context = ltrim($context); + $context = explode("\n", $context)[0]; + } + + $context = preg_replace('/<[^>]+>/', ' ', $context); + $context = cleanup_context_string($context); + } + + + return $context; +} + +/** + * Generates page title from subjects and comments + * params must be already html-escaped. + */ +function generate_page_title($thread_id, $sub, $com) { + if (JANITOR_BOARD) { + return strip_tags(TITLE); + } + + if (UPLOAD_BOARD) { + $sub = preg_replace('/^(\d+)\|/', '', $sub); + } + + $context = ''; + + if (strpos($sub, 'SPOILER<>') === 0) { + $sub = substr($sub, 9); + } + + if ($sub !== '') { + $context = $sub; + } + + if ($context === '' && $com !== '') { + if (SJIS_TAGS && strpos($com, '/', '[SJIS]', $com); + } + $context = str_replace('
    ', ' ', $com); + $context = htmlspecialchars_decode($context, ENT_QUOTES); + $context = mb_substr(strip_tags($context), 0, 50); + $context = htmlspecialchars($context, ENT_QUOTES); + } + + if ($context === '') { + $context = 'No.' . $thread_id; + } + + if (BOARD_DIR === 's4s') { + return '[' . BOARD_DIR . '] - ' . $context; + } + else { + return '/' . BOARD_DIR . '/ - ' . $context; + } +} + +/** + * Generates metatags from subjects and comments + * params must be already html-escaped. + */ +function generate_page_metatags($sub, $com) { + if (JANITOR_BOARD) { + return null; + } + + $context = ''; + + if (strpos($sub, 'SPOILER<>') === 0) { + $sub = substr($sub, 9); + } + + if ($sub !== '') { + $context = $sub; + } + + $ell = ''; + + if ($context === '' && $com !== '') { + $context = preg_replace('/(
    |\s)+/', ' ', $com); + $context = htmlspecialchars_decode(strip_tags($context), ENT_QUOTES); + + if (mb_strlen($context) > 100) { + $ell = '...'; + $context = mb_substr($context, 0, 100); + } + } + else { + $context = htmlspecialchars_decode($context, ENT_QUOTES); + } + + if (empty($context)) { + return null; + } + else { + $words = preg_split('/[[:punct:]\s]+/', $context); + $keywords = ''; + foreach ($words as $word) { + if (strlen($word) > 3) { + $keywords .= ',' . $word; + } + } + } + + $context = htmlspecialchars($context, ENT_QUOTES); + $keywords = htmlspecialchars($keywords, ENT_QUOTES); + + return array($context.$ell, $keywords); +} + +function cleanup_context_string($context) { + $context = htmlspecialchars_decode($context, ENT_QUOTES); + + $context = strtolower(preg_replace('/[^a-zA-Z0-9\s]+/', '', $context)); + + $length = 0; + + $words = explode(' ', $context); + + $context = array(); + + foreach ($words as $word) { + if ($word === '') { + continue; + } + + $length += strlen($word) + 1; + + if ($length > 50) { + break; + } + + $context[] = $word; + } + + $context = implode('-', $context); + + return htmlspecialchars($context, ENT_QUOTES); +} + +/** + * Embedded data detection + * Dies if the PNG or JPG file contains embedded data. + * Returns true if the GIF file was modified, otherwise returns false. + */ +function cleanup_uploaded_file($file, $type) { + $full_size = filesize($file); + + // 50KB + $max_delta = 51200; + + switch ($type) { + case '.png': + $clean_size = get_clean_png_size($file); + break; + case '.jpg': + if ($full_size < 204800) { + return false; + } + $clean_size = get_clean_jpg_size($file); + break; + case '.gif': + if ($full_size < 204800) { + return false; + } + $clean_size = get_clean_gif_size($file); + break; + case '.swf': + return true; // Why? + default: + return false; + } + + if ($clean_size === false) { + return false; + } + + // PNGs can still fail the check even if no size delta is found + if ($clean_size === -1) { + if ($type === '.gif') { + $file = escapeshellcmd($file); + $res = system("/usr/local/bin/gifsicle --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1"); + if ($res !== false) { + return true; + } + else { + return false; + } + } + else { + error(S_IMGCONTAINSFILE, $file); + } + } + + $delta_size = $full_size - $clean_size; + + if ($delta_size > $max_delta) { + if ($type === '.gif') { + $file = escapeshellcmd($file); + $res = system("/usr/local/bin/gifsicle --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1"); + if ($res !== false) { + return true; + } + } + else { + error(S_IMGCONTAINSFILE, $file); + } + } + + return false; +} + +// Returns the size of critical data or -1 if the file contains extensions. +function get_clean_gif_size($file) { + $file = escapeshellcmd($file); + + $binary = '/usr/local/bin/gifsicle'; + + $res = shell_exec("$binary --sinfo \"$file\" 2>&1"); + + if ($res !== null) { + $size = 0; + + if (preg_match('/ extensions [0-9]+/', $res)) { + return -1; + } + + if (preg_match_all('/compressed size ([0-9]+)/', $res, $m)) { + foreach ($m[1] as $frame_size) { + $size += (int)$frame_size; + } + + return $size; + } + } + + return false; +} + +// Returns the number of bytes in critical chunks or -1 if too many IDAT chunks are found +// Returns false on error +function get_clean_png_size($file) { + $file = escapeshellcmd($file); + + $binary = '/usr/local/bin/pngcrush'; + + $res = shell_exec("$binary -m 1 -n -v \"$file\" 2>&1 1>/dev/null"); + + if ($res !== null) { + if (preg_match('/Reading (?:iTXt|tEXt|zTXt) chunk,/', $res, $m)) { + if (preg_match('/ [a-z]oo: /i', $res)) { + return -1; + } + else if (preg_match('/ (?:Software|Creation Time): ([=a-zA-Z0-9]{10,})\n/', $res, $ct)) { + $_b64 = base64_decode($ct[0]); + if ($_b64 && preg_match('/[0-9]/', $_b64)) { + write_to_event_log('png_link', $_SERVER['REMOTE_ADDR'], [ + 'board' => BOARD_DIR, + 'meta' => htmlspecialchars($res) + ]); + return -1; + } + } + } + + if (preg_match('/in critical chunks\s+=\s+([0-9]+)/', $res, $m)) { + return (int)$m[1]; + } + } + + return false; +} + +function get_clean_jpg_size($file) { + $eof = false; + + $img = fopen($file, 'rb'); + + $data = fread($img, 2); + + if ($data !== "\xff\xd8") { + fclose($img); + return false; + } + + while (!feof($img)) { + $data = fread($img, 1); + + if ($data !== "\xff") { + continue; + } + + while (!feof($img)) { + $data = fread($img, 1); + + if ($data !== "\xff") { + break; + } + } + + if (feof($img)) { + break; + } + + $byte = unpack('C', $data)[1]; + + if ($byte === 217) { + $eof = ftell($img); + break; + } + + if ($byte === 0 || $byte === 1 || ($byte >= 208 && $byte <= 216)) { + continue; + } + + $data = fread($img, 2); + + $length = unpack('n', $data)[1]; + + if ($length < 1) { + break; + } + + fseek($img, $length - 2, SEEK_CUR); + } + + fclose($img); + + return $eof; +} + +/** + * Generates a "Pass User Since YEAR" string for pass users + */ +function get_since_4chan($pass_id) { + $query = "SELECT UNIX_TIMESTAMP(purchase_date) FROM pass_users WHERE user_hash = '%s' ORDER BY purchase_date ASC LIMIT 1"; + + $res = mysql_global_call($query, $pass_id); + + if (!$res) { + return 0; + } + + $row = mysql_fetch_row($res); + + $ts = (int)$row[0]; + + if (!$ts) { + return 0; + } + + $ts = date('Y', $ts); + + if (!$ts) { + return 0; + } + + return (int)$ts; +} +/* +// Halloween 2017 +function get_halloween_score($pass_id) { + $query = "SELECT score FROM halloween_tricks WHERE user_hash = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $pass_id); + + if (!$res) { + return 0; + } + + $row = mysql_fetch_row($res); + + if (!$row) { + return 0; + } + + return (int)$row[0]; +} + +// Halloween 2017 +/* +function get_halloween_dummy_pass($name) { + if (!$name) { + return ''; + } + + $hashed_bits = hash_hmac('sha1', $name, 'CIaPCUkJq9n3fskmKC06tquCV/2edWqbgBeY9pk7RlQ', true); + + $hashed_name = base64_encode($hashed_bits); + + if (!$hashed_name) { + return ''; + } + + return '_' . substr($hashed_name, 0, 9); +} + +// Halloween 2017 +function process_halloween_score($com, $thread_id, $this_pass_id, $this_pwd, $pass_is_bannable) { + if ($com === '') { + return; + } + + $thread_id = (int)$thread_id; + + if (!$thread_id) { + return; + } + + if (preg_match_all('/>>([0-9]{4,})/', $com, $m) === 1) { + $post_id = (int)$m[1][0]; + + if (!$post_id || $post_id == $thread_id) { + return; + } + + $query = 'SELECT host, pwd, 4pass_id FROM `' . SQLLOG . '` WHERE no = ' . $post_id . ' AND resto = ' . $thread_id; + + $res = mysql_board_call($query); + + if (!$res) { + return; + } + + $row = mysql_fetch_assoc($res); + + if (!$row) { + return; + } + + // Check if same person + if (!$row['4pass_id'] || $row['4pass_id'] == $this_pass_id || $row['pwd'] == $this_pwd || $row['host'] == $_SERVER['REMOTE_ADDR']) { + return; + } + + $long_ip = ip2long($_SERVER['REMOTE_ADDR']); + + if (!$long_ip) { + return; + } + + // Check if already gave points + $query = "SELECT 1 FROM `halloween_votes` WHERE long_ip = $long_ip AND board = '" . BOARD_DIR . "' AND post_id = $post_id"; + + $res = mysql_global_call($query); + + if (!$res) { + return; + } + + if (mysql_num_rows($res) > 0) { + return; + } + + // Check if the user is known + if (!spam_filter_is_user_known($long_ip, BOARD_DIR, $pass_is_bannable ? $this_pwd : null)) { + return; + } + + // Good to go + $query = <<= 1000) { + return " n-jol-6"; + } + if ($trick_count >= 500) { + return " n-jol-5"; + } + if ($trick_count >= 200) { + return " n-jol-4"; + } + if ($trick_count >= 100) { + return " n-jol-3"; + } + if ($trick_count >= 50) { + return " n-jol-2"; + } + if ($trick_count >= 25) { + return " n-jol-1"; + } + return ''; +} +*/ + +/** + * Checks if the user has a recent ban request. + * dies with an error if user needs to be blocked. + */ +function check_for_ban_request($ip, $pwd = null) { + $time_lim = BLOCK_ON_BR_LEN; + + $clauses = []; + + $clauses[] = "host = '" . mysql_real_escape_string($ip) . "'"; + + if ($pwd) { + $clauses[] = "pwd = '" . mysql_real_escape_string($pwd) . "'"; + } + + $board_sql = mysql_real_escape_string(BOARD_DIR); + + $clauses = implode(' OR ', $clauses); + + $query = << DATE_SUB(NOW(), $time_lim) OR ban_template IN (1, 2, 123, 126)) +LIMIT 1 +SQL; + + $res = mysql_global_call($query); + + if (!$res || mysql_num_rows($res) !== 1) { + return false; + } + + $row = mysql_fetch_assoc($res); + + $time = (int)$row['diff']; + + $tpl_name = rtrim(preg_replace('/\[[^\]]+\]/', '', $row['tpl_name'])); + + // Non-expiring blocks for some global templates + if ($time < 0) { + error(sprintf(S_BRBLOCKED_2, $tpl_name)); + } + + $str = $time <= 1 ? '1 minute' : "$time minutes"; + + error(sprintf(S_BRBLOCKED, $tpl_name, $str)); +} + +/** + * Checks if the IP is banned, also checks for ban evasion + * return 0 for not banned, 1 for banned, 2 for warned + * If the ban has expired, delete the banned thumbnail and de-activate the ban + */ +function check_for_ban($ip, $fields = array(), $thread_id = 0, $user_verified = false) { + global $captcha_bypass; + + // Skip IP bans when the user has a valid 4chan Pass + $skip_ip_bans = isset($fields['4pass_id']) && $captcha_bypass; + + if (!$skip_ip_bans) { + $fields['host'] = $ip; + } + + $expired = []; + + $is_banned = 0; + + foreach ($fields as $key => $value) { +$query =<< ['pipe', 'w'], 2 => ['pipe', 'w'] ]; + + $pipes = []; + + $proc = proc_open($cmd, $desc, $pipes); + + if ($proc === false || !is_resource($proc)) { + error(S_FAILEDUPLOAD, $file); + } + + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + $status = proc_close($proc); + + if ($status !== 0) { + error(S_FAILEDUPLOAD, $file); + } + + // Check stderr + if (stripos($stderr, 'invalid') !== false) { + error(S_NOREC, $file); + } + + // Check stdout + $res = json_decode($stdout, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + error(S_FAILEDUPLOAD, $file); + } + + //print_r($res); + + if ($res['format']['format_name'] === 'matroska,webm') { + $format = 'webm'; + } + else if ($res['format']['format_name'] === 'mov,mp4,m4a,3gp,3g2,mj2') { + $format = 'mp4'; + } + else { + error(S_NOREC, $file); + } + + // container duration, can be forged but we remux the file to fix this + $duration = (float)$res['format']['duration']; + + if ($duration <= 0 || $duration > MAX_WEBM_DURATION) { + error(S_VIDEOTOOLONG, $file); // Duration too long + } + + $has_audio = false; + $video_dims = false; + + foreach ($res['streams'] as $stream) { + $type = $stream['codec_type']; + + if ($type === 'audio') { + if (!ENABLE_WEBM_AUDIO) { + error(S_AUDIODISABLED, $file); // Audio streams are not allowed + } + + // Vorbis or Opus for webm audio + if ($format === 'webm') { + if ($stream['codec_name'] !== 'vorbis' && $stream['codec_name'] !== 'opus') { + error(S_BADAUDIO, $file); // Bad audio stream + } + } + // AAC for mp4 audio + else if ($format === 'mp4') { + if ($stream['codec_name'] !== 'aac') { + error(S_BADAUDIO, $file); // Bad audio stream + } + } + else { + error(S_BADAUDIO, $file); // Bad audio stream + } + + $has_audio = true; + } + else if ($type === 'video') { + if ($video_dims) { + error(S_BADSTREAM, $file); // Too many video streams + } + + // VP8 or VP9 for webm video + if ($format === 'webm') { + if ($stream['codec_name'] !== 'vp8' && $stream['codec_name'] !== 'vp9') { + error(S_BADVIDEO, $file); // Bad video stream + } + } + // H264 for mp4 video + else if ($format === 'mp4') { + if ($stream['codec_name'] !== 'h264') { + error(S_BADVIDEO, $file); // Bad video stream + } + + // Reject 10 bit streams + if ($stream['bits_per_raw_sample'] > 8) { + error(S_NOREC, $file); + } + + // Only accept yuv420p streams + if ($stream['pix_fmt'] !== 'yuv420p') { + error(S_NOREC, $file); + } + } + else { + error(S_BADVIDEO, $file); // Bad video stream + } + + $width = (int)$stream['width']; + $height = (int)$stream['height']; + + if (!$width || !$height || $width > MAX_WEBM_DIMENSION || $height > MAX_WEBM_DIMENSION) { + error(S_TOOLARGERES, $file); // Dimensions too big + } + + $sar = null; + + if (isset($stream['sample_aspect_ratio'])) { + $tmp_sar = explode(':', $stream['sample_aspect_ratio']); + + $tmp_sar[0] = (int)$tmp_sar[0]; + $tmp_sar[1] = (int)$tmp_sar[1]; + + if ($tmp_sar[1] && $tmp_sar[0] !== $tmp_sar[1]) { + $tmp_sar = $tmp_sar[0] / $tmp_sar[1]; + + if ($tmp_sar < 2 && $tmp_sar > 0.5) { + $sar = $tmp_sar; + } + } + } + + $video_dims = array($width, $height, $sar); + } + else { + error(S_BADSTREAM, $file); // Bad stream + } + } + + if (!$video_dims) { + error(S_NOVIDEOSTREAM, $file); // No video streams + } + + return $video_dims; +} + +/** + * Generates thumbnails for webm files + * Returns the thumbnail filename on success. + * Returns false if the thumbnail couldn't be generated. + */ +function thumb_webm($file, $ext) { + $binary = '/usr/local/bin/ffmpeg-mp4'; + + $out_file = $file . '.tmp.jpg'; + + if ($ext === '.webm') { + $format = 'webm'; + } + else if ($ext === '.mp4') { + $format = 'mp4'; + } + else { + return false; + } + + // $file and $format must be safe for shell_exec + $res = shell_exec("$binary -f $format -i \"$file\" -vframes 1 -an -y \"$out_file\" 2>&1"); + + if (file_exists($out_file)) { + return $out_file; + } + + quick_log_to( "/www/perhost/bad-upload.log", "webm failure on $file:\n$res"); + + return false; +} + +/** + * Contest banners 468x60 + */ +function get_contest_banner() { + $query = "SELECT file_id, file_ext, board FROM contest_banners WHERE is_live = 1 ORDER BY RAND() LIMIT 1"; + + $res = mysql_global_call($query); + + if (!$res) { + return ''; + } + + $banner = mysql_fetch_assoc($res); + + if (!$banner) { + return ''; + } + + $img_url = STATIC_SERVER . "image/contest_banners/{$banner['file_id']}.{$banner['file_ext']}"; + $link_url = '//boards.' . L::d($banner['board']) . '/' . $banner['board'] . '/'; + + return '
    '; +} + +/** + * Get latest post number from /j/ + * Returns a json { "no": 123 } + */ + +function get_last_post_no() { + $no = 0; + $query = "SELECT no FROM `j` ORDER BY no DESC LIMIT 1"; + $res = mysql_board_call($query); + if ($res) { + if ($row = mysql_fetch_row($res)) { + $no = (int)$row[0]; + } + } + echo "{\"no\":$no}"; +} + +/** + * Deletes partial jsons for all live threads + */ +function purge_json_tails() { + $query = 'SELECT no FROM `' . SQLLOG . '` WHERE resto = 0 AND archived = 0'; + + $res = mysql_board_call($query); + + if (!$res) { + return false; + } + + while ($row = mysql_fetch_row($res)) { + $thread_id = (int)$row[0]; + update_json_tail_deletion($thread_id, true); + } + + return true; +} + +/** + * Deletes, if necessary, the partial json tail file after post deletion. + */ +function update_json_tail_deletion($thread_id, $force = false) { + if (!JSON_TAIL_SIZE && !$force) { + return false; + } + + $tail_size = $force ? 0 : get_json_tail_size($thread_id); + + if (!$tail_size) { + $fname = RES_DIR . $thread_id . '-tail.json'; + + if (USE_GZIP) { + $fname = "$fname.gz"; + } + + if (file_exists($fname)) { + return unlink($fname); + } + } + + return false; +} + +/** + * Returns the number of posts in the partial -tail json. + * 0 if no tail json is available + */ +function get_json_tail_size($thread_id) { + global $log; + + $tail_size = (int)JSON_TAIL_SIZE; + + if (!$tail_size || !isset($log[$thread_id])) { + return 0; + } + + $th = $log[$thread_id]; + + if ($th['sticky'] && $th['undead']) { + $tail_size = $tail_size * 2; + } + + $post_count = count($th['children']); + + if ($post_count >= $tail_size * 2) { + return $tail_size; + } + else { + return 0; + } +} + +/** + * Test function for mobile image resizing + */ +function resize_mobile_image($path, $w, $h, $fsize, $tim, $ext) { + if ($ext !== '.jpg' && $ext !== '.png') { + return false; + } + + $MAX_W = 1024; + $MAX_H = 1024; + $MAX_PXL = 524288; + $MAX_PNG_BYTES = 524288; + + if ($ext === '.png' && $fsize <= $MAX_PNG_BYTES) { + return; + } + + if (($w > $MAX_W || $h > $MAX_H) && $w * $h > $MAX_PXL) { + $jpeg_quality = 80; + + $memory_limit_increased = false; + + if ($w * $h > 3000000) { + $memory_limit_increased = true; + ini_set('memory_limit', memory_get_usage() + $w * $h * 15); + } + + if ($ext === '.jpg') { + $img_in = ImageCreateFromJPEG($path); + } + else { + $img_in = ImageCreateFromPNG($path); + } + + if (!$img_in) { + error(S_FAILEDUPLOAD . ' (rmi)', $path); + } + + $ratio = $w / $h; + + if ($ratio > 1) { + $out_w = $MAX_W; + $out_h = round($MAX_W / $ratio); + } + else { + $out_w = round($MAX_H * $ratio); + $out_h = $MAX_H; + } + + $img_out = ImageCreateTrueColor($out_w, $out_h); + + ImageCopyResampled($img_out, $img_in, 0, 0, 0, 0, $out_w, $out_h, $w, $h); + ImageDestroy($img_in); + + $out_path = IMG_DIR . $tim . 'm.jpg'; + ImageJPEG($img_out, $out_path, $jpeg_quality); + ImageDestroy($img_out); + + if ($memory_limit_increased) { + ini_restore('memory_limit'); + } + + return $out_path; + } +} + +/** + * Returns the number of unique IPs for a given thread id + * $thread_id needs to be cached in $log. + */ +function get_unique_ip_count($thread_id) { + global $log; + + if (!isset($log[$thread_id]) || $log[$thread_id]['archived']) { + return false; + } + + $posts = $log[$thread_id]['children']; + + if (empty($posts)) { + return 1; + } + + $ip_map = array(); + $ip_count = 1; + $ip_map[$log[$thread_id]['host']] = true; + + foreach ($posts as $pid => $val) { + if (!isset($log[$pid])) { + continue; + } + $post = $log[$pid]; + if (!isset($ip_map[$post['host']])) { + ++$ip_count; + $ip_map[$post['host']] = true; + } + } + + return $ip_count; +} + +function generate_del_pwd() { + return '_' . substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 32); +} + +function get_hashed_mod_name($name) { + if (!$name) { + die('Internal Server Error (ghmn0)'); + } + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (ghmn1)'); + } + + $hashed_bits = hash_hmac('sha256', $name, $admin_salt, true); + + $hashed_name = base64_encode($hashed_bits); + + if (!$hashed_name) { + die('Internal Server Error (ghmn2)'); + } + + return $hashed_name; +} + +function create_memcached_instance() { + $m = new Memcached(); + //$m->setOption(Memcached::OPT_TCP_NODELAY, true); + $m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); + $m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms + $m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms + $m->addServer(MEMCACHED_HOST, MEMCACHED_PORT); + return $m; +} + +function forcearchive() { + if (!has_level()) { + error("Can't let you do that."); + } + + if (!ENABLE_ARCHIVE) { + error('Archives are disabled on this board.'); + } + + if (!isset($_POST['id'])) { + error('Bad Request.'); + } + + $tid = (int)$_POST['id']; + + $query = 'SELECT resto, sticky, archived, no, name, sub, com, filename, ext FROM `%s` WHERE no = %d'; + $res = mysql_board_call($query, BOARD_DIR, $tid); + + if (!$res) { + error('Database error.'); + } + + $thread = mysql_fetch_assoc($res); + + if (!$thread || $thread['resto']) { + error('Thread not found.'); + } + + if ($thread['archived']) { + error('This thread is already archived.'); + } + + if ($thread['sticky']) { + error(S_MAYNOTDELSTICKY); + } + + archive_thread($tid); + + // Log the action + $action_log_post = array( + 'no' => $thread['no'], + 'name' => $thread['name'], + 'sub' => $thread['sub'], + 'com' => $thread['com'], + 'filename' => $thread['filename'], + 'ext' => $thread['ext'] + ); + + log_mod_action(3, $action_log_post); + + if (ENABLE_JSON_THREADS) { + generate_board_archived_json(); + } + + updating_index(); +} + +// Called remotely by other tools +function rebuild_threads_by_id() { + header('Content-Type: text/plain'); + + if (!isset($_POST['ids']) || !is_array($_POST['ids']) || empty($_POST['ids'])) { + echo '0'; + return; + } + + $live_ids = array(); + + // Rebuild archived threads first + foreach ($_POST['ids'] as $id) { + $id = (int)$id; + + if (!$id) { + continue; + } + + $query = "SELECT archived FROM `" . SQLLOG . "` WHERE no = $id LIMIT 1"; + $res = mysql_board_call($query); + + if (!$res) { + echo '0'; + return; + } + + if (mysql_fetch_row($res)[0] === '1') { + rebuild_archived_thread($id); + } + else { + $live_ids[] = $id; + } + } + + // Rebuild live threads + if (!empty($live_ids)) { + + foreach ($live_ids as $id) { + updatelog($id, 1); + } + + if (STATIC_REBUILD) { + return; + } + + updatelog(0, 0); // rebuild indexes + } + + echo '1'; +} + +function rebuild_archive_list($print = false) { + $board = BOARD_DIR; + + $html = ''; + + $maxlen = 100; + + $max_age_in_days = 3; + + $hour_clause = $max_age_in_days * 24; + + $thread_limit = 3000; + + $query = <<= DATE_SUB(NOW(), INTERVAL $hour_clause HOUR) +ORDER BY root DESC +LIMIT $thread_limit +SQL; + + $res = mysql_board_call($query); + + $thread_count = mysql_num_rows($res); + + head($html, 0, 0, 0, 0, true); + + $html .= ''; + + $html .= '
    +
    '; + + $html .= '

    Displaying ' . number_format($thread_count) . ' expired thread' + . (!$thread_count || $thread_count > 1 ? 's' : '') . ' from the past ' . $max_age_in_days . ' day' + . ($max_age_in_days > 1 ? 's' : '') . '

    + + + + + '; + + while ($row = mysql_fetch_assoc($res)) { + if (strpos($row['sub'], 'SPOILER<>') === 0) { + $row['sub'] = substr($row['sub'], 9); + } + + if (!empty($row['sub'])) { + if ($row['com'] !== '') { + $teaser = '' . $row['sub'] . ': ' . $row['com']; + } + else { + $teaser = $row['sub']; + } + } + else { + $teaser = $row['com']; + } + + $teaser = preg_replace('/(?:
    )+/', ' ', str_replace('"', "'", $teaser)); + + $href_context = generate_href_context($row['sub'], $row['com']); + + if ($href_context !== '') { + $href_context = "/$href_context"; + } + + $html .= ' + + + +'; + } + + $html .= '
    No.Excerpt
    ' . $row['no'] . '' + . truncate_comment($teaser, $maxlen) . +'[View] +

    '; + + $html .= '
    '; + + $html .= '
    '; + + if (AD_BOTTOM_ENABLE == 1) { + $bottomad = ''; + + if (defined('AD_BOTTOM_TEXT') && AD_BOTTOM_TEXT) { + $bottomad .= '
    ' + . ad_text_for(AD_BOTTOM_TEXT) . '
    ' + . (defined('AD_BOTTOM_PLEA') ? AD_BOTTOM_PLEA : ''); + } + + if ($bottomad) { + $html .= "$bottomad
    "; + } + } + + $html .= '
    '; + + if (!defined('CSS_FORCE')) { + $html .= 'Style: + + '; + } + + $html .= '
    '; + + foot($html, false, true); + + if ($print) { + echo $html; + } + else { + print_page(INDEX_DIR . 'archive'. PHP_EXT, $html); + } +} + +function rebuild_syncframe_page($print = false) { + $tpl_file = YOTSUBA_DIR . 'views/syncframe.html'; + + if (!file_exists($tpl_file)) { + die('Template file not found'); + } + + $html = file_get_contents($tpl_file); + + if ($print) { + die($html); + } + else { + print_page(BOARDS_ROOT . 'syncframe' . PHP_EXT, $html); + } +} + +function rebuild_search_page($print = false) { + $html = ''; + + // board select box + + $board_select_html = ''; + + // header --- + + $cssVersion = $print ? CSS_VERSION_TEST : CSS_VERSION; + $defaultcss = 'yotsubanew'; + $mobilecss = 'yotsubamobile.' . $cssVersion . '.css'; + + $styles = array( + 'Yotsuba New' => "yotsubanew.$cssVersion.css", + 'Yotsuba B New' => "yotsubluenew.$cssVersion.css", + 'Futaba New' => "futabanew.$cssVersion.css", + 'Burichan New' => "burichannew.$cssVersion.css", + 'Photon' => "photon.$cssVersion.css", + 'Tomorrow' => "tomorrow.$cssVersion.css" + ); + + $dcssl = $defaultcss . '.' . $cssVersion . '.css'; + + $css = ''; + + foreach ($styles as $style => $stylecss) { + $css .= ''; + } + + $css .= ''; + + $scriptjs = ''; + + $testjs = $print ? 'test/core-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'core.min.' . JS_VERSION_CORE . '.js'; + $testextra = $print ? 'test/extension-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'extension.min.' . JS_VERSION_EXT . '.js'; + + $scriptjs .= ''; + $scriptjs .= ''; + + if (defined('FAVICON')) { + $favicon = ''; + } + else { + $favicon = ''; + } + + $includenav = file_get_contents_cached(NAV_TXT); + + $html .= ' + + + + + + + +' . $favicon . ' +' . $css . ' +Search 4chan' . $scriptjs . +' +' . $stylejs . $includenav . +'
    +
    4chan Search
    +
    '; + + // --- + + $html .= '
    ' . $board_select_html . '
    '; + + $html .= '
    +
    +
    '; + + $html .= '

    '; + + $html .= '
    '; + + if (!defined('CSS_FORCE')) { + $html .= 'Style: + + '; + } + + $html .= '
    '; + + foot($html); + + if ($print) { + echo $html; + } + else { + print_page(BOARDS_ROOT . 'globalsearch' . PHP_EXT, $html); + } +} + +/** + * Enforce the maximum number of allowed threads per user, per board. + * error() if limit has been reached + */ +function validate_user_thread_limit($ip, $password = null, $pass_id = null) { + $clauses = array(); + + $clauses[] = "host = '" . mysql_real_escape_string($ip) . "'"; + + if ($password) { + $clauses[] = "pwd = '" . mysql_real_escape_string($password) . "'"; + } + + if ($pass_id) { + $clauses[] = "4pass_id = '" . mysql_real_escape_string($pass_id) . "'"; + } + + $ts = $_SERVER['REQUEST_TIME'] - ((int)MAX_USER_THREADS_PERIOD * 3600); + + $clauses = implode(' OR ', $clauses); + + $query = 'SELECT COUNT(*) FROM `' . SQLLOG + . "` WHERE resto = 0 AND archived = 0 AND time > $ts AND ($clauses)"; + + $res = mysql_board_call($query); + + if (!$res) { + return true; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count >= (int)MAX_USER_THREADS) { + $plural = MAX_USER_THREADS > 1 ? 's' : ''; + error(sprintf(S_TOOMANYTHREADS, MAX_USER_THREADS, $plural)); + } + + return true; +} + +function is_poster_op($host, $hashed_pwd, $resto) { + $query = 'SELECT host, pwd FROM `%s` WHERE no = %d'; + $res = mysql_board_call($query, SQLLOG, $resto); + + if (!$res) { + return false; + } + + $post = mysql_fetch_assoc($res); + + if (!$post) { + return false; + } + + return $post['host'] === $host || $post['pwd'] === $hashed_pwd; +} + +function spam_filter_check_qa_bot($board, $resto, $ip, $country, $com, $captcha_resp) { + if (preg_match('/Edge|Safari|WebKit|Firefox|Mozilla/', $_SERVER['HTTP_USER_AGENT']) && $captcha_resp['hostname'] === 'boards.4chan.org') { + return true; + } + + // Check if IP is known + $long_ip = ip2long($ip); + + if (spam_filter_is_user_known($long_ip)) { + return false; + } + + if (!preg_match('/Edge|Mobile/', $_SERVER['HTTP_USER_AGENT']) && preg_match('/WebKit/', $_SERVER['HTTP_USER_AGENT']) !== preg_match('/WebKit/', $_SERVER['HTTP_CONTENT_TYPE'])) { + return true; + } + + $bot_countries = array( + 'AD','AE','AF','AG','AI','AL','AM','AN','AO','AR','AS','AW','AZ', + 'BB','BD','BF','BG','BH','BI','BJ','BM','BN','BO','BR','BS','BT','BV','BW','BY','BZ', + 'CC','CF','CG','CH','CI','CK','CL','CM','CN','CO','CR','CU','CV','CX','CY','CZ', + 'DJ','DM','DO','DZ','EC','EE','EG','EH','ER','ET','FJ','FM','FO', + 'GA','GD','GE','GF','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GY', + 'HK','HM','HN','HT','HU','HR','ID','IL','IN','IO','IQ','IR','IS','JM','JO', + 'KE','KG','KH','KI','KM','KN','KR','KW','KY','KZ', + 'LA','LB','LC','LI','LK','LR','LS','LU','LY', + 'MA','MD','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MY','MZ','NA', + 'NE','NF','NG','NI','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PM','PN','PR','PS','PT','PW', + 'QA','RE','RS','RO','RU','RW','SA','SB','SC','SD','SH','SI','SJ','SK','SL','SM','SN','SO','SR','ST','SV','SY','SZ', + 'TC','TD','TF','TG','TJ','TM','TN','TO','TP','TR','TT','TV','TW','TZ','UG','UM','UY','UZ', + 'VA','VE','VC','VG','VI','VN','VU','WF','WS','YE','YT','ZA','ZM','ZR','ZW' + ); + + // Check country + if (!in_array($country, $bot_countries)) { + return false; + } + + if ($resto) { + $query = 'SELECT time FROM %s WHERE resto = %d ORDER BY no DESC LIMIT 1'; + + $res = mysql_board_call($query, $board, $resto); + + if (!$res) { + return false; + } + + $last_time = mysql_fetch_row($res); + + if (!$last_time) { + return false; + } + + $last_time = (int)$last_time[0]; + + if ($_SERVER['REQUEST_TIME'] - $last_time < 14400) { + return false; + } + } + + return true; +} + +function log_qa_spam_filter($is_hit, $thread_id, $ip, $country, $captcha_resp) { + $_bot_headers = ''; + + foreach ($_SERVER as $_h_name => $_h_val) { + if (substr($_h_name, 0, 5) == 'HTTP_') { + $_bot_headers .= "$_h_name: $_h_val\n"; + } + } + + $_bot_headers .= "_Captcha: " . $captcha_resp['hostname'] . "\n"; + + $_bot_headers .= "_Country: $country\n"; + + log_spam_filter_trigger($is_hit ? 'blocked_qa' : 'ok_qa', BOARD_DIR, $thread_id, $ip, 1, $_bot_headers); +} + +function log_spam_filter_trigger($action, $board, $thread_id, $ip, $arg_num, $meta = '') { + $query = << 'error', 'message' => 'Nothing to do.'); + echo json_encode($data); + return; + } + + $html = purify_html($html); + + $data = array('status' => 'success', 'data' => $html); + + echo json_encode($data); +} + +function purify_html($html) { + static $purifier = null; + + if ($purifier === null) { + require_once 'lib/htmlpurifier/HTMLPurifier.standalone.php'; + + $config = HTMLPurifier_Config::createDefault(); + $config->set('Cache.DefinitionImpl', null); + + $config->set('HTML.Doctype', 'HTML 4.01 Transitional'); + + $config->set('URI.AllowedSchemes', array('http' => true, 'https' => true)); + $config->set('HTML.SafeIframe', true); + $config->set('URI.SafeIframeRegexp', HTML_IFRAME_WHITELIST); + $config->set('HTML.Allowed', HTML_WHITELIST); + $config->set('CSS.AllowTricky', true); + $config->set('CSS.Trusted', true); + $config->set('Attr.AllowedFrameTargets', array('_blank')); + + $def = $config->getHTMLDefinition(true); + $def->addAttribute('iframe', 'allowfullscreen', 'Bool'); + + $def->addElement('video', 'Block', 'Flow', 'Common', array( + 'controls' => 'Bool', + 'height' => 'Length', + 'width' => 'Length', + 'poster' => 'URI', + 'autoplay' => 'Bool', + 'loop' => 'Bool', + 'muted' => 'Bool', + 'src' => 'URI' + )); + + $css_def = $config->getDefinition('CSS'); + + $css_def->info['color'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('transparent')), + new HTMLPurifier_AttrDef_CSS_Color() + ) + ); + + $purifier = new HTMLPurifier($config); + + if (!$purifier) { + error('Internal Server Error'); + } + } + + return $purifier->purify($html); +} + +function count_thread_replies($board, $thread_id) { + $thread_id = (int)$thread_id; + + if ($thread_id <= 0) { + return 0; + } + + $sql = "SELECT COUNT(*) as cnt FROM `%s` WHERE resto = $thread_id"; + + $res = mysql_board_call($sql, $board); + + if (!$res) { + return 0; + } + + return (int)mysql_fetch_row($res)[0]; +} + +// TODO: remove later +function check_safe_ua_sig($ua, $sig) { + if (!$ua || !$sig) { + return true; + } + + $thres = 3; + + $ua_sig = "$ua.$sig"; + + $sql = "SELECT 1 FROM event_log WHERE type = 'log_safe_ua' AND ua_sig = '%s' LIMIT $thres"; + + $res = mysql_global_call($sql, $ua_sig); + + if (!$res) { + return true; + } + + if (mysql_num_rows($res) < $thres) { + return false; + } + + return true; +} + +// April 2024 +function april_2024_parse_email($email) { + if ($email[0] != '$') { + return 0; + } + + $tag = substr(trim($email), 1); + + $stocks = april_2024_get_stock_list(); + + $idx = array_search($tag, $stocks); + + if ($idx === false) { + return 0; + } + + $count = april_2024_get_stock_count($tag); + + if ($count < 10) { + return 0; + } + + return 10000 + $idx; +} + +function april_2024_get_stock_list() { + static $stocks = [ + 'PEPE', 'WOJK', 'ANIME', 'CHAD', 'CLOWN', 'LOL', 'SICP', 'AUTSM', 'BANE', + 'CIA', 'BOOB', 'RDDT', 'DESU', 'JANNY', 'GME', 'CHUCK', 'YTSB', 'GACHI' + ]; + + return $stocks; +} + +function april_2024_get_stock_from_s4p($since4pass) { + if ($since4pass < 10000) { + return false; + } + + $val = $since4pass - 10000; + + if ($val < 0) { + return false; + } + + $badges = april_2024_get_stock_list(); + + if ($val >= 0 && $val < count($badges)) { + return $badges[$val]; + } + else { + return false; + } +} + +function april_2024_get_name() { + $net_worth = april_2024_get_net_worth(); + + if ($net_worth < 500) { + return 'Destitute Investor'; + } + else if ($net_worth < 1500) { + return 'Helpless Investor'; + } + else if ($net_worth < 5000) { + return 'Poor Investor'; + } + else if ($net_worth < 50000) { + return 'Fledgling Investor'; + } + else if ($net_worth < 500000) { + return 'Aspiring Investor'; + } + else if ($net_worth < 2000000) { + return 'Rich Investor'; + } + else if ($net_worth < 5000000) { + return 'Anonymous Magnate'; + } + else { + return 'Anonymous Mogul'; + } +} + +function april_2024_get_post_cls($since4pass) { + $stock = april_2024_get_stock_from_s4p($since4pass); + + if ($stock) { + return " p-xa24-$stock"; + } + else { + return ''; + } +} + +function april_2024_get_name_badge($since4pass) { + $stock = april_2024_get_stock_from_s4p($since4pass); + + if ($stock) { + return " "; + } + else { + return ''; + } +} + +function april_2024_get_stock_count($stock) { + $userpwd = UserPwd::getSession(); + + if (!$userpwd || $userpwd->isNew()) { + return 0; + } + + $user_id = $userpwd->getPwd(); + + $sql =<<isNew()) { + return 0; + } + + $user_id = $userpwd->getPwd(); + + $sql =<< 0 +SQL; + + $res = mysql_global_call($sql, $user_id); + + if (!$res) { + return 0; + } + + $stocks = []; + + while ($row = mysql_fetch_row($res)) { + $stocks[$row[0]] = (int)$row[1]; + } + + $sql =<< $count) { + if (isset($prices[$stock])) { + $net_worth += ($prices[$stock] * $count); + } + } + + return $net_worth; +} + +// --- + +function clear_no_captcha_token() { + setcookie('_ct', null, -3600, '/', '.' . L::d(BOARD_DIR)); +} + +function generate_no_captcha_token() { + if (BOARD_DIR === 'pol' || BOARD_DIR === 'b' || BOARD_DIR === 'r9k' || BOARD_DIR === 'bant') { + return false; + } + + $long_ip = ip2long($_SERVER['REMOTE_ADDR']); + + if (!$long_ip) { + return false; + } + + if (!spam_filter_is_user_known($long_ip, BOARD_DIR, null, 15)) { + return false; + } + + $salt = file_get_contents_cached(SALTFILE); + + if (!$salt) { + return false; + } + + $time = $_SERVER['REQUEST_TIME']; + + $msg = $_SERVER['REMOTE_ADDR'] . '.' . $time; + + $msg = hash_hmac('sha1', $msg, $salt); + + if (!$msg) { + return false; + } + + $msg = substr($msg, 0, 20) . '.' . $time; + + setcookie('_ct', $msg, $time + 300, '/', '.' . L::d(BOARD_DIR)); // 5 minutes +} + +function verify_no_captcha_token($token) { + list($hash, $ts) = explode('.', $token); + + $ts = (int)$ts; + + if (!$hash || !$ts) { + return false; + } + + if ($ts < $_SERVER['REQUEST_TIME'] - 300) { // 5 minutes + return false; + } + + $salt = file_get_contents_cached(SALTFILE); + + if (!$salt) { + return false; + } + + $msg = $_SERVER['REMOTE_ADDR'] . '.' . $ts; + + if (substr(hash_hmac('sha1', $msg, $salt), 0, 20) === $hash) { + return true; + } + + return false; +} + +function get_random_real_name() { + $first_name_nid = mt_rand(1, 1000); + + if (mt_rand(0, 999) < 10) { + $type = 2; + } + else { + $type = 1; + } + + $query = "SELECT data FROM april_names WHERE nid = $first_name_nid AND type = $type"; + $res = mysql_global_call($query); + $first_name = mysql_fetch_row($res)[0]; + + if (!$first_name) { + $first_name = 'Alberto'; + } + + $last_name_nid = mt_rand(1, 1000); + + $query = "SELECT data FROM april_names WHERE nid = $last_name_nid AND type = 3"; + $res = mysql_global_call($query); + $last_name = mysql_fetch_row($res)[0]; + + if (!$last_name) { + $last_name = 'Barbosa'; + } + + return "$first_name $last_name"; +} + + +function log_mod_action($action_type, $post, $vip_capcode = false) { + $mask_shift = 128; + $action_id = $mask_shift + $action_type; + + $query =<<verifyCode($dec_secret, $otp, 1)) { + error("Incorrect or expired OTP."); + } +} + +function validate_csrf() { + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + error('Bad Request.'); + } + + if (!isset($_COOKIE['_tkn']) || !isset($_POST['_tkn']) + || $_COOKIE['_tkn'] == '' || $_POST['_tkn'] == '' + || $_COOKIE['_tkn'] !== $_POST['_tkn']) { + + if (!is_local()) { + error('Bad Request.'); + } + } +} + +function validate_referer($strict = false) { + if (!$strict && (!isset($_SERVER['HTTP_REFERER']) || $_SERVER['HTTP_REFERER'] == '')) { + return; + } + + if (!preg_match('/^https?:\/\/([_a-z0-9]+)\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) { + error('Bad Request.'); + } +} + +function dev_make_remote_thumbnail() { + if (!is_local()) { + die('403'); + } + + $infile = $_FILES['file']['tmp_name']; + $file_ext = $_POST['file_ext']; + $src_width = $_POST['src_width']; + $src_height = $_POST['src_height']; + $th_width = $_POST['th_width']; + $th_height = $_POST['th_height']; + + if (!$infile || !$file_ext || !$src_width || !$src_height || !$th_width || !$th_height) { + return; + } + + $jpeg_quality = 65; + + switch ($file_ext) { + case 'gif': + $img_in = ImageCreateFromGIF($infile); + break; + case 'jpg': + $img_in = ImageCreateFromJPEG($infile); + break; + case 'png': + $img_in = ImageCreateFromPNG($infile); + break; + default : + return; + } + + if (!$img_in) { + return; + } + + $img_out = ImageCreateTrueColor($th_width, $th_height); + + if (!$img_out) { + return; + } + + ImageCopyResampled($img_out, $img_in, 0, 0, 0, 0, $th_width, $th_height, $src_width, $src_height); + + ImageDestroy($img_in); + + ImageJPEG($img_out, NULL, $jpeg_quality); + + ImageDestroy($img_out); +} + +/*-----------Main-------------*/ +switch( $mode ) { + case 'make_remote_thumbnail': + dev_make_remote_thumbnail(); + die(); + case 'purgejsontails': + if (has_flag('developer')) { + echo purge_json_tails() ? 'OK' : 'ERROR'; + } + die(); + case 'listarchive': + if (has_flag('developer')) { + rebuild_archive_list(true); + } + die(); + case 'search': + if (has_flag('developer')) { + rebuild_search_page(true); + } + die(); + case 'rebuildsearchpage': + if (has_flag('developer') || has_level('manager')) { + rebuild_search_page(); + echo 'done'; + } + die(); + case 'rebuildsyncframepage': + if (has_flag('developer') || has_level('manager')) { + rebuild_syncframe_page(isset($_GET['print'])); + echo 'done'; + } + die(); + case 'rebuildarchivedthread': + if (has_flag('developer') || has_level('manager')) { + rebuild_archived_thread((int)$_GET['id']); + echo 'done'; + } + die(); + + case 'regist': + case 'post': + require_request_method( "POST" ); + validate_referer(); + new_post( $name, $email, $sub, $com, '', $pwd, $upfile, $upfile_name, $resto, $age, $filetag ); + break; + case 'report': + report(); + break; + + case 'preview_html': + preview_html(); + break; + + case 'rebuild': + require_request_method( "GET" ); + rebuild(); + break; + case 'rebuildall': + rebuild( 1 ); + break; + + case 'rebuildadmin': + rebuild_deletions( array($no => '1') ); + echo 'Rebuilt OK!'; // shut rpc up + break; + + case 'rebuildcatalog': + if (ENABLE_CATALOG) { + rebuild_catalog(); + } + break; + + case 'rebuildboardsjson': + if (has_flag('developer')) { + rebuild_boards_json(); + } + break; + + case 'rebuildthumb': + rebuildallthumb(isset($_GET['archiveonly']) || isset($_ENV['archiveonly'])); + break; + + case 'cataloginfo': + get_catalog_info(); + break; + + case 'admindel': case 'admindelete': + user_delete( $no, $pwd ); + echo ""; + break; + case 'updatelog': + updatelog_remote( $no, $noidx ); + break; + case 'nothing': + break; + case 'arcdel': + require_request_method( "POST" ); + validate_referer(); + arcdel($no, true, $res); + break; + case 'usrdel': case 'delete': + require_request_method( "POST" ); + validate_referer(); + user_delete( $no, $pwd, true, $res ); + break; + case 'rebuild_threads_by_id': + require_request_method( "POST" ); + if (is_local()) { + rebuild_threads_by_id(); + } + else { + updating_index(); + } + break; + case 'forcearchive': + require_request_method( "POST" ); + validate_referer(); //validate_csrf(); + forcearchive(); + break; + case 'copythreads': + require_request_method( "GET" ); + do_copy_threads(); + break; + case 'movethread': + validate_csrf(); + do_move_thread(); + break; + case 'latest': + if (has_level('janitor')) { + get_last_post_no(); + } + die(); + case 'rake_post': + //april_rake_commit(); + die('0\nNo'); + break; + default: + require_request_method( "GET" ); + if( JANITOR_BOARD == 1 && !has_level( 'janitor' ) ) { + die( '' ); + } + if( $res ) { + resredir( $res ); + } else { + updating_index(); + } +} diff --git a/imgtop/banned_dev.js b/imgtop/banned_dev.js new file mode 100644 index 0000000..d9e1ac5 --- /dev/null +++ b/imgtop/banned_dev.js @@ -0,0 +1,398 @@ +var Parser = {} + +Parser.init = function() { + var staticPath = '//static.4chan.org/image/'; + + var tail = window.devicePixelRatio >= 2 ? '@2x.gif' : '.gif'; + + this.icons = { + admin: staticPath + 'adminicon' + tail, + mod: staticPath + 'modicon' + tail, + dev: staticPath + 'developericon' + tail, + del: staticPath + 'filedeleted-res' + tail + }; +}; + +function buildHTMLFromJSON(data) { + var + container = document.createElement('div'), + isOP = false, + + userId, + fileDims = '', + imgSrc = '', + fileBuildStart = '', + fileBuildEnd = '', + fileInfo = '', + fileHtml = '', + fileThumb, + fileSize = '', + fileClass = '', + shortFile = '', + longFile = '', + tripcode = '', + capcodeStart = '', + capcodeClass = '', + capcode = '', + flag, + highlight = '', + emailStart = '', + emailEnd = '', + name, + subject, + noLink, + quoteLink, + noFilename, + maxSize = 150, + ratio, imgWidth, imgHeight, + + imgDir = '//images.4chan.org/' + data.board + '/src'; + + noLink = 'res/' + data.resto + '#p' + data.no; + quoteLink = 'res/' + data.resto + '#q' + data.no; + + if (!data.capcode && data.id) { + userId = ' (ID: ' + + data.id + ') '; + } + else { + userId = ''; + } + + switch (data.capcode) { + case 'admin_highlight': + highlight = ' highlightPost'; + case 'admin': + capcodeStart = ' ## Admin'; + capcodeClass = ' capcodeAdmin'; + + capcode = ' '; + break; + case 'mod': + capcodeStart = ' ## Mod'; + capcodeClass = ' capcodeMod'; + + capcode = ' '; + break; + case 'developer': + capcodeStart = ' ## Developer'; + capcodeClass = ' capcodeDeveloper'; + + capcode = ' '; + break; + } + + if (data.email) { + emailStart = ''; + emailEnd = ''; + } + + if (data.country) { + flag = ' '
+      + data.country + ''; + } + else { + flag = ''; + } + + if (data.ext) { + shortFile = longFile = data.filename + data.ext; + if (data.filename.length > 30) { + shortFile = data.filename.slice(0, 25) + '(...)' + data.ext; + } + + if (!data.tn_w && !data.tn_h && data.ext == '.gif') { + data.tn_w = data.w; + data.tn_h = data.h; + } + if (data.fsize >= 1048576) { + fileSize = ((0 | (data.fsize / 1048576 * 100 + 0.5)) / 100) + ' M'; + } + else if (data.fsize > 1024) { + fileSize = (0 | (data.fsize / 1024 + 0.5)) + ' K'; + } + else { + fileSize = data.fsize + ' '; + } + + fileThumb = '//images.4chan.org/bans/thumb/' + data.board + '/' + data.thumb + 's.jpg'; + + imgWidth = data.tn_w; + imgHeight = data.tn_h; + + if (imgWidth > maxSize) { + ratio = maxSize / imgWidth; + imgWidth = maxSize; + imgHeight = imgHeight * ratio; + } + if (imgHeight > maxSize) { + ratio = maxSize / imgHeight; + imgWidth = imgWidth * ratio; + imgHeight = maxSize; + } + + imgSrc = '' + fileSize + 'B'; + + fileDims = data.ext == '.pdf' ? 'PDF' : data.w + 'x' + data.h; + fileInfo = 'File: ' + data.tim + data.ext + '-(' + fileSize + + 'B, ' + fileDims + + (noFilename ? '' : (', ' + + shortFile + '')) + ')'; + + fileBuildStart = fileInfo ? '
    ' : ''; + fileBuildEnd = fileInfo ? '
    ' : ''; + + fileHtml = '
    ' + + fileBuildStart + fileInfo + fileBuildEnd + imgSrc + '
    '; + } + else if (data.filedeleted) { + fileHtml = '
    File deleted.
    '; + } + + if (data.trip) { + tripcode = ' ' + data.trip + ''; + } + + name = data.name || ''; + + subject = data.sub || ''; + + container.id = 'p' + data.no; + container.className = 'post reply' + highlight + (data.ws_board ? ' ws' : ' nws'); + container.innerHTML = + '' + fileHtml + + '
    ' + + (data.com || '') + '
    '; + + return container; +} + +function showPreview(e) { + var rect, postHeight, doc, docWidth, style, pos, top, scrollTop, link, post, match, bid; + + if (e.target.nodeName == 'A' && (match = e.target.className.match(/^bannedPost_([0-9]+)/))) { + link = e.target; + bid = match[1]; + } + else { + return; + } + + post = buildHTMLFromJSON(window['banjson_' + bid]); + + post.id = 'quote-preview'; + + rect = link.getBoundingClientRect(); + doc = document.documentElement; + docWidth = doc.offsetWidth; + style = post.style; + + document.body.appendChild(post); + + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + pos = docWidth - rect.left; + style.right = pos + 5 + 'px'; + } + else { + pos = rect.left + rect.width; + style.left = pos + 5 + 'px'; + } + + top = rect.top + link.offsetHeight + window.pageYOffset + - post.offsetHeight / 2 - rect.height / 2; + + postHeight = post.getBoundingClientRect().height; + + if (doc.scrollTop != document.body.scrollTop) { + scrollTop = doc.scrollTop + document.body.scrollTop; + } else { + scrollTop = document.body.scrollTop; + } + + if (top < scrollTop) { + style.top = scrollTop + 'px'; + } + else if (top + postHeight > scrollTop + doc.clientHeight) { + style.top = scrollTop + doc.clientHeight - postHeight + 'px'; + } + else { + style.top = top + 'px'; + } +} + +function removePreview() { + if (cnt = document.getElementById('quote-preview')) { + document.body.removeChild(cnt); + } +} + +function run() { + Parser.init(); + addCSS(); + document.addEventListener('mouseover', showPreview, false); + document.addEventListener('mouseout', removePreview, false); +} + +function addCSS() { + var style; + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = '\ +#quote-preview {\ + display: block;\ + position: absolute;\ + padding: 3px 6px 6px 3px;\ + margin: 0;\ + text-align: left;\ + border-width: 1px 2px 2px 1px;\ + border-style: solid;\ +}\ +#quote-preview.nws {\ + color: #800000;\ + border-color: #D9BFB7;\ +}\ +#quote-preview.ws {\ + color: #000;\ + border-color: #B7C5D9;\ +}\ +#quote-preview.ws a {\ + color: #34345C;\ +}\ +#quote-preview input {\ + margin: 3px 3px 3px 4px;\ +}\ +.ws.reply {\ + background-color: #D6DAF0;\ +}\ +.nws.reply {\ + background-color: #F0E0D6;\ +}\ +.subject {\ + font-weight: bold;\ +}\ +.ws .subject {\ + color: #0F0C5D;\ +}\ +.nws .subject {\ + color: #CC1105;\ +}\ +.quote {\ + color: #789922;\ +}\ +.quotelink,\ +.deadlink {\ + color: #789922 !important;\ +}\ +.ws .useremail .postertrip,\ +.ws .useremail .name {\ + color: #34345C !important;\ +}\ +.nws .useremail .postertrip,\ +.nws .useremail .name {\ + color: #0000EE !important;\ +}\ +.nameBlock {\ + display: inline-block;\ +}\ +.name {\ + color: #117743;\ + font-weight: bold;\ +}\ +.postertrip {\ + color: #117743;\ + font-weight: normal !important;\ +}\ +.postNum a {\ + text-decoration: none;\ +}\ +.ws .postNum a {\ + color: #000 !important;\ +}\ +.nws .postNum a {\ + color: #800000 !important;\ +}\ +.fileInfo {\ + margin-left: 20px;\ +}\ +.fileThumb {\ + float: left;\ + margin: 3px 20px 5px;\ +}\ +.fileThumb img {\ + border: none;\ + float: left;\ +}\ +s {\ + background-color: #000000 !important;\ +}\ +.capcode {\ + font-weight: bold !important;\ +}\ +.nameBlock.capcodeAdmin span.name, span.capcodeAdmin span.name a, span.capcodeAdmin span.postertrip, span.capcodeAdmin strong.capcode {\ + color: #FF0000 !important;\ +}\ +.nameBlock.capcodeMod span.name, span.capcodeMod span.name a, span.capcodeMod span.postertrip, span.capcodeMod strong.capcode {\ + color: #800080 !important;\ +}\ +.nameBlock.capcodeDeveloper span.name, span.capcodeDeveloper span.name a, span.capcodeDeveloper span.postertrip, span.capcodeDeveloper strong.capcode {\ + color: #0000F0 !important;\ +}\ +.identityIcon {\ + height: 16px;\ + margin-bottom: -3px;\ + width: 16px;\ +}\ +.postMessage {\ + margin: 13px 40px 13px 40px;\ +}\ +.countryFlag {\ + margin-bottom: -1px;\ + padding-top: 1px;\ +}\ +.fileDeletedRes {\ + height: 13px;\ + width: 127px;\ +}\ +span.fileThumb, span.fileThumb img {\ + float: none !important;\ + margin-bottom: 0 !important;\ + margin-top: 0 !important;\ +}\ +'; + + (document.head || document.getElementsByTagName('head')[0]).appendChild(style); +} + +document.addEventListener('DOMContentLoaded', run, false); diff --git a/js/catalog-test.js b/js/catalog-test.js new file mode 100644 index 0000000..0705a1e --- /dev/null +++ b/js/catalog-test.js @@ -0,0 +1,3613 @@ +var $ = {}; + +$.id = function(id) { + return document.getElementById(id); +}; + +$.cls = function(klass, root) { + return (root || document).getElementsByClassName(klass); +}; + +$.tag = function(tag, root) { + return (root || document).getElementsByTagName(tag); +}; + +$.extend = function(destination, source) { + for (var key in source) { + destination[key] = source[key]; + } +}; + +$.on = function(n, e, h) { + n.addEventListener(e, h, false); +}; + +$.off = function(n, e, h) { + n.removeEventListener(e, h, false); +}; + +$.readCookie = function(name) { + var i, c, ca, key; + + key = name + '='; + ca = document.cookie.split(';'); + + for (i = 0; c = ca[i]; ++i) { + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(key) === 0) { + return decodeURIComponent(c.substring(key.length, c.length)); + } + } + return null; +}; + +if (!document.documentElement.classList) { + $.hasClass = function(el, klass) { + return (' ' + el.className + ' ').indexOf(' ' + klass + ' ') != -1; + }; + + $.addClass = function(el, klass) { + el.className = (el.className === '') ? klass : el.className + ' ' + klass; + }; + + $.removeClass = function(el, klass) { + el.className = (' ' + el.className + ' ').replace(' ' + klass + ' ', ''); + }; +} +else { + $.hasClass = function(el, klass) { + return el.classList.contains(klass); + }; + + $.addClass = function(el, klass) { + el.classList.add(klass); + }; + + $.removeClass = function(el, klass) { + el.classList.remove(klass); + }; +} + +$.toggleClass = function(el, klass) { + if ($.hasClass(el, klass)) { + $.removeClass(el, klass); + } + else { + $.addClass(el, klass); + } +}; + +var UA = {}; + +UA.init = function() { + document.head = document.head || $.tag('head')[0]; + + this.hasContextMenu = 'HTMLMenuItemElement' in window; + + this.hasWebStorage = (function() { + var kv = 'catalog'; + try { + localStorage.setItem(kv, kv); + localStorage.removeItem(kv); + return true; + } catch (e) { + return false; + } + })(); + + this.hasSessionStorage = (function() { + var kv = 'catalog'; + try { + sessionStorage.setItem(kv, kv); + sessionStorage.removeItem(kv); + return true; + } catch (e) { + return false; + } + })(); + + this.hasCORS = 'withCredentials' in new XMLHttpRequest(); + + this.isMobileDevice = /Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent); +}; + +UA.dispatchEvent = function(name, detail) { + var e = document.createEvent('Event'); + e.initEvent(name, false, false); + if (detail) { + e.detail = detail; + } + document.dispatchEvent(e); +}; + +var FC = function() { + + var self = this, + + options = { + orderby: 'alt', + large: false, + extended: true, + imgdel: '//s.4cdn.org/image/filedeleted-res.gif', + imgspoiler: '//s.4cdn.org/image/spoiler', + nofile: '//s.4cdn.org/image/nofile.png', + smallsize: 150, + tipdelay: 250, + filterColors: [ + ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00'], + ['#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB'], + ['#00A550', '#007FFF', '#AF0A0F', '#B5BD68'] + ] + }, + + capcodeMap = { + admin: 'Administrator', + mod: 'Moderator', + developer: 'Developer', + manager: 'Manager', + founder: 'Founder', + verified: 'Verified' + }, + + keybinds = { + 83: focusQuickfilter, // S + 82: refreshWindow, // R + 88: cycleOrder // X + }, + + catalog = {}, + + basicSettings = [ 'orderby', 'large', 'extended' ], + + activeTheme = {}, + + activeStyleGroup, + activeStyleSheet, + + activeFilters = {}, + + hasTooltip = false, + tooltipTimeout = null, + + pinnedThreads = {}, + + hiddenThreads = {}, + hiddenThreadsCount = 0, + + filteredThreadsCount = 0, + + hasThreadWatcher = false, + hasDropDownNav = false, + hasClassicNav = false, + hasAutoHideNav = false, + altCaptcha = false, + + quickFilterPattern = false, + + hiddenMode = false, + + $threads, + $qfCtrl, + $teaserCtrl, + $sizeCtrl, + $orderCtrl, + $filterPalette, + + ctxCmds; + + if (window.devicePixelRatio >= 2) { + options.imgdel.replace('.', '@2x.'); + options.nofile.replace('.', '@2x.'); + } + + UA.init(); + + loadTheme(); + + self.init = function() { + var extConfig, el, val, fn; + + FC.hasMobileLayout = checkMobileLayout(); + + applyTheme(activeTheme, true); + + $threads = $.id('threads'); + $qfCtrl = $.id('qf-ctrl'); + $teaserCtrl = $.id('teaser-ctrl'); + $sizeCtrl = $.id('size-ctrl'); + $orderCtrl = $.id('order-ctrl'); + + $.on($qfCtrl, 'click', toggleQuickfilter); + $.on($.id('filters-clear-hidden'), 'click', toggleHiddenThreads); + $.on($.id('filters-clear-hidden-bottom'), 'click', toggleHiddenThreads); + $.on($.id('qf-clear'), 'click', toggleQuickfilter); + $.on($.id('settingsWindowLink'), 'click', showThemeEditor); + $.on($.id('settingsWindowLinkBot'), 'click', showThemeEditor); + $.on($.id('settingsWindowLinkMobile'), 'click', showThemeEditor); + $.on($.id('filters-ctrl'), 'click', showFilters); + $.on($teaserCtrl, 'change', onTeaserChange); + $.on($sizeCtrl, 'change', onSizeChange); + $.on($orderCtrl, 'change', onOrderChange); + $.on($threads, 'mouseover', onThreadMouseOver); + $.on($threads, 'mouseout', onThreadMouseOut); + $.on($.id('togglePostFormLinkMobile'), 'click', togglePostFormMobile); + $.on(document, 'click', onClick); + + loadSettings(); + + bindGlobalShortcuts(); + + initGlobalMessage(); + + if (UA.hasContextMenu) { + buildContextMenu(); + } + + window.Config = {}; + + if (UA.hasWebStorage) { + if (extConfig = localStorage.getItem('4chan-settings')) { + extConfig = JSON.parse(extConfig); + + window.Config = extConfig; + + if (!extConfig.disableAll) { + CustomMenu.initCtrl(extConfig.dropDownNav, extConfig.classicNav); + /* + if (location.host === 'boards.4channel.org' && extConfig.showNWSBoards) { + CustomMenu.showNWSBoards(); + } + */ + if (extConfig.filter) { + ThreadWatcher.hasFilters = true; + } + + if (extConfig.threadWatcher) { + hasThreadWatcher = true; + ThreadWatcher.init(); + } + + if (extConfig.customMenu) { + CustomMenu.apply(extConfig.customMenuList); + } + + if (extConfig.dropDownNav !== false && !FC.hasMobileLayout) { + hasDropDownNav = true; + hasClassicNav = extConfig.classicNav; + hasAutoHideNav = extConfig.autoHideNav; + showDropDownNav(); + } + + altCaptcha = extConfig.altCaptcha; + } + } + else if (UA.isMobileDevice && !FC.hasMobileLayout) { + hasDropDownNav = true; + showDropDownNav(); + } + else { + CustomMenu.initCtrl(false, false); + } + + if (window.css_event && activeStyleSheet === '_special') { + fn = window['fc_' + window.css_event + '_init']; + fn && fn(); + } + } + + if (el = document.forms.post.flag) { + if ((val = $.readCookie('4chan_flag')) && (el = el.querySelector('option[value="' + val + '"]'))) { + el.setAttribute('selected', 'selected'); + } + } + + setOrder(options.orderby, true); + setLarge(options.large, true); + setExtended(options.extended, true); + + UA.dispatchEvent('4chanMainInit'); + }; + + function showDropDownNav() { + var el, top, bottom; + + top = $.id('boardNavDesktop'); + bottom = $.id('boardNavDesktopFoot'); + + if (hasClassicNav) { + el = document.createElement('div'); + el.className = 'pageJump'; + el.innerHTML = '' + + 'Settings' + + 'Home'; + + top.appendChild(el); + + $.id('settingsWindowLinkClassic') + .addEventListener('click', showThemeEditor, false); + + $.addClass(top, 'persistentNav'); + } + else { + top.style.display = 'none'; + $.removeClass($.id('boardNavMobile'), 'mobile'); + } + + if (hasAutoHideNav) { + StickyNav.init(hasClassicNav); + } + + bottom.style.display = 'none'; + + $.addClass(document.body, 'hasDropDownNav'); + } + + function hideDropDownNav() { + var el, top, bottom; + + top = $.id('boardNavDesktop'); + bottom = $.id('boardNavDesktopFoot'); + + if (hasClassicNav) { + if (el = $.cls('pageJump', top)[0]) { + $.id('settingsWindowLinkClassic') + .removeEventListener('click', showThemeEditor, false); + top.removeChild(el); + } + + $.removeClass(top, 'persistentNav'); + } + else { + top.style.display = ''; + $.addClass($.id('boardNavMobile'), 'mobile'); + } + + if (hasAutoHideNav) { + StickyNav.destroy(hasClassicNav); + } + + bottom.style.display = ''; + + $.removeClass(document.body, 'hasDropDownNav'); + } + + self.loadCatalog = function(c) { + var query; + + catalog = c; + + $.addClass(document.body, activeStyleSheet.toLowerCase().replace(/ /g, '_')); + + initStyleSwitcher(); + loadFilters(); + loadStorage(); + + if (UA.hasSessionStorage && !location.hash && (query = sessionStorage.getItem('4chan-catalog-search'))) { + if (catalog.slug != sessionStorage.getItem('4chan-catalog-search-board')) { + sessionStorage.removeItem('4chan-catalog-search'); + sessionStorage.removeItem('4chan-catalog-search-board'); + query = null; + } + } + else if (location.hash && (query = location.hash.match(/#s=(.+)/))) { + query = decodeURIComponent(query[1].replace(/\+/g, ' ')); + } + + if (query) { + toggleQuickfilter(); + $.id('qf-box').value = query; + applyQuickfilter(); + } + else { + buildThreads(); + } + }; + + function initGlobalMessage() { + var msg, btn, thisTs, oldTs; + + if (!UA.hasWebStorage || FC.hasMobileLayout) { + return; + } + + if ((msg = $.id('globalMessage')) && msg.textContent) { + msg.nextElementSibling.style.clear = 'both'; + + btn = document.createElement('span'); + btn.id = 'toggleMsgBtn'; + btn.setAttribute('data-cmd', 'toggleMsg'); + btn.title = 'Toggle announcement'; + + oldTs = localStorage.getItem('4chan-global-msg'); + thisTs = msg.getAttribute('data-utc'); + + if (oldTs && thisTs <= oldTs) { + msg.style.display = 'none'; + btn.style.opacity = '0.5'; + btn.className = 'expandIcon'; + } + else { + btn.className = 'collapseIcon'; + } + + $.on(btn, 'click', toggleGlobalCatalogMessage); + msg.parentNode.insertBefore(btn, msg); + } + } + + function toggleGlobalCatalogMessage() { + var msg, btn; + + msg = $.id('globalMessage'); + btn = $.id('toggleMsgBtn'); + if (msg.style.display == 'none') { + msg.style.display = ''; + btn.className = 'collapseIcon'; + btn.style.opacity = '1'; + localStorage.removeItem('4chan-global-msg'); + } + else { + msg.style.display = 'none'; + btn.className = 'expandIcon'; + btn.innerHTML = 'View Important Announcement'; + btn.style.opacity = '0.5'; + localStorage.setItem('4chan-global-msg', msg.getAttribute('data-utc')); + } + + //StorageSync.sync('4chan-global-msg'); + } + + function togglePostFormMobile() { + var el = document.getElementById('postForm'); + + if (el.style.display == 'table') { + el.style.display = ''; + this.textContent = 'Start a New Thread'; + } + else { + el.style.display = 'table'; + this.textContent = 'Close Post Form'; + window.initRecaptcha(); + window.initTCaptcha(); + } + } + + function getRegexSpecials() { + var specials = ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\' ]; + return new RegExp('(\\' + specials.join('|\\') + ')', 'g'); + } + + function getThreadPage(tid) { + return (0 | (catalog.threads[tid].b / catalog.pagesize)) + 1; + } + + function initStyleSwitcher() { + var i, selector, nodes, ss; + + selector = $.id('styleSelector'); + nodes = selector.children; + + for (i = 0; ss = nodes[i]; ++i) { + if (ss.value == activeStyleSheet) { + selector.selectedIndex = i; + } + } + + $.on(selector, 'change', onStyleSheetChange); + } + + function onStyleSheetChange() { + var expires; + + if (this.value !== '_special') { + expires = new Date(); + + expires.setTime(expires.getTime() + 31536000000); + + document.cookie = activeStyleGroup + '=' + this.value + '; expires=' + + expires.toGMTString() + '; path=/; domain=' + $L.d(catalog.slug); + + if (window.css_event) { + fn = window['fc_' + window.css_event + '_cleanup']; + localStorage.setItem('4chan_stop_css_event', `${window.css_event}-${window.css_event_v}`); + } + } + else if (window.css_event) { + fn = window['fc_' + window.css_event + '_init']; + localStorage.removeItem('4chan_stop_css_event'); + } + + //StorageSync.sync('4chan_stop_css_event'); + + refreshWindow(); + } + + function refreshWindow(e) { + if (e && e.shiftKey) { + return; + } + location.href = location.href; + } + + function debounce(delay, fn) { + var timeout; + + return function() { + var args = arguments, context = this; + + clearTimeout(timeout); + timeout = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; + } + + function focusQuickfilter() { + if ($.hasClass($qfCtrl, 'active')) { + clearQuickfilter(true); + } + else { + toggleQuickfilter(); + } + } + + function toggleQuickfilter() { + var qfBox, qfcnt = $.id('qf-cnt'); + if ($.hasClass($qfCtrl, 'active')) { + clearQuickfilter(); + qfcnt.style.display = 'none'; + $.removeClass($qfCtrl, 'active'); + } + else { + qfcnt.style.display = 'inline'; + qfBox = $.id('qf-box'); + if (!qfcnt.hasAttribute('data-built')) { + qfcnt.setAttribute('data-built', '1'); + $.on(qfBox, 'keyup', debounce(250, applyQuickfilter)); + $.on(qfBox, 'keydown', function(e) { + if (e.keyCode == '27') { + toggleQuickfilter(); + } + }); + } + qfBox.focus(); + qfBox.value = ''; + $.addClass($qfCtrl, 'active'); + } + } + + function applyQuickfilter() { + var regexEscape, qfstr; + + if ((qfstr = $.id('qf-box').value) !== '') { + if (UA.hasSessionStorage) { + sessionStorage.setItem('4chan-catalog-search', qfstr); + sessionStorage.setItem('4chan-catalog-search-board', catalog.slug); + } + regexEscape = getRegexSpecials(); + $.id('search-term').textContent = $.id('search-term-bottom').textContent = qfstr; + $.id('search-label').style.display = $.id('search-label-bottom').style.display = 'inline'; + qfstr = qfstr.replace(regexEscape, '\\$1'); + quickFilterPattern = new RegExp(qfstr, 'i'); + buildThreads(); + } else { + clearQuickfilter(); + } + } + + function clearQuickfilter(focus) { + var qf = $.id('qf-box'); + $.id('search-label').style.display = $.id('search-label-bottom').style.display = 'none'; + if (focus) { + qf.value = ''; + qf.focus(); + } + else { + if (UA.hasSessionStorage) { + sessionStorage.removeItem('4chan-catalog-search'); + } + quickFilterPattern = false; + buildThreads(); + } + } + + function buildContextMenu() { + ctxCmds = { + pin: toggleThreadPin, + hide: toggleThreadHide, + report: reportThread + }; + + $.id('ctxmenu-main').innerHTML = + ''; + + $.id('ctxmenu-thread').innerHTML = + '' + + '' + + ''; + + $.on($.id('ctxmenu-main'), 'click', clearPinnedThreads); + $.on($.id('ctxmenu-thread'), 'click', onThreadContextClick); + } + + function bindGlobalShortcuts() { + var el, tid; + if (UA.hasWebStorage) { + $.on($threads, 'mousedown', function(e) { + el = e.target; + if (el.className.indexOf('thumb') != -1) { + tid = el.getAttribute('data-id'); + if (e.which == 3) { + $threads.setAttribute('contextmenu', 'ctxmenu-thread'); + $.id('ctxmenu-thread').target = tid; + } + else if (e.which == 1 && e.altKey) { + toggleThreadPin(tid); + return false; + } + else if (e.which == 1 && e.shiftKey) { + toggleThreadHide(tid); + return false; + } + } + else if (e.which == 3) { + $threads.setAttribute('contextmenu', 'ctxmenu-main'); + } + }); + } + if (!activeTheme.nobinds) { + $.on(document, 'keyup', processKeybind); + } + } + + function toggleThreadPin(tid) { + if (pinnedThreads[tid] >= 0) { + delete pinnedThreads[tid]; + } + else { + pinnedThreads[tid] = catalog.threads[tid].r || 0; + } + localStorage.setItem('4chan-pin-' + catalog.slug, JSON.stringify(pinnedThreads)); + buildThreads(); + } + + function toggleThreadHide(tid) { + if (hiddenMode) { + delete hiddenThreads[tid]; + --hiddenThreadsCount; + } + else { + hiddenThreads[tid] = true; + ++hiddenThreadsCount; + } + + localStorage.setItem('4chan-hide-t-' + catalog.slug, JSON.stringify(hiddenThreads)); + + $.id('thread-' + tid).style.display = 'none'; + + setHiddenCount(hiddenThreadsCount); + + if (hiddenThreadsCount === 0) { + setHiddenMode(false); + } + } + + function setHiddenMode(state) { + hiddenMode = state; + + $.id('filters-clear-hidden').textContent = + $.id('filters-clear-hidden-bottom').textContent = state ? 'Back' : 'Show'; + + buildThreads(); + } + + function setProcessedCount(type, num) { + var label = type + '-label', count = type + '-count'; + + if (num > 0) { + $.id(count).textContent = $.id(count + '-bottom').textContent = num; + $.id(label).style.display = $.id(label + '-bottom').style.display = 'inline'; + } + else { + $.id(label).style.display = $.id(label + '-bottom').style.display = 'none'; + } + } + + function setHiddenCount(num) { + setProcessedCount('hidden', num); + } + + function setFilteredCount(num) { + setProcessedCount('filtered', num); + } + + function reportThread(tid) { + var height, altc; + + if (window.passEnabled || !window.grecaptcha) { + height = 175; + } + else if (altCaptcha) { + height = 320; + altc = '&altc=1'; + } + else { + height = 510; + altc = ''; + } + + window.open( + 'https://sys.' + $L.d(catalog.slug) + '/' + catalog.slug + + '/imgboard.php?mode=report&no=' + tid + altc, + Date.now(), + 'toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height=' + height + ); + } + + function onThreadContextClick(e) { + var cmd = e.target.getAttribute('data-cmd'); + ctxCmds[cmd]($.id('ctxmenu-thread').target); + } + + function processKeybind(e) { + var el = e.target; + if (el.nodeName == 'TEXTAREA' || el.nodeName == 'INPUT') { + return; + } + if (keybinds[e.keyCode]) { + keybinds[e.keyCode](e); + } + } + + function toggleHiddenThreads(e) { + var el; + + e.preventDefault(); + + if (hiddenThreadsCount > 0) { + if ((el = $.id('filters-clear-hidden')).textContent == 'Show') { + setHiddenMode(true); + } + else { + setHiddenMode(false); + } + } + } + + function clearPinnedThreads() { + pinnedThreads = {}; + localStorage.removeItem('4chan-pin-' + catalog.slug); + buildThreads(); + return false; + } + + function onClick(e) { + var t = e.target, tid; + + if ((t = e.target) == document) { + return; + } + + if (tid = t.getAttribute('data-watch')) { + ThreadWatcher.toggle( + tid, + catalog.slug, + catalog.threads[tid].sub, + catalog.threads[tid].teaser, + catalog.threads[tid].lr.id + ); + } + else if (tid = t.getAttribute('data-hide')) { + e.preventDefault(); + toggleThreadHide(tid); + } + else if (tid = t.getAttribute('data-pin')) { + e.preventDefault(); + toggleThreadPin(tid); + } + else if (tid = t.getAttribute('data-report')) { + e.preventDefault(); + reportThread(tid); + } + else if (tid = t.getAttribute('data-post-menu')) { + e.preventDefault(); + PostMenu.open(t, tid, hasThreadWatcher, hiddenThreads[tid], pinnedThreads[tid]); + } + else if (t.hasAttribute('data-cm-edit')) { + e.preventDefault(); + CustomMenu.showEditor(true); + } + else if (t.id == 'backdrop') { + if (!panelHidden($.id('filters'))) { + if (!panelHidden($.id('filters-protip'))) { + closeFiltersHelp(); + } + else { + closeFilters(); + } + } + else if (!panelHidden($.id('theme'))) { + closeThemeEditor(); + } + } + else if (e.target.id == 'filter-palette') { + closeFilterPalette(); + } + } + + function buildFilterPalette() { + var i, j, table, palette, rows, cols, tr, td, foot; + + $filterPalette = $.id('filter-palette'); + + table = $.id('filter-color-table'); + palette = $.tag('tbody', table)[0]; + rows = options.filterColors.length; + + if (rows > 0) { + cols = options.filterColors[0].length; + foot = $.tag('tfoot', table)[0]; + for (i = foot.children.length - 1; i >= 0; i--) { + foot.children[i].firstElementChild.setAttribute('colspan', cols); + } + } + for (i = 0; i < rows; ++i) { + tr = document.createElement('tr'); + for (j = 0; j < cols; ++j) { + td = document.createElement('td'); + td.innerHTML = ''; + $.on(td.firstElementChild, 'click', selectFilterColor); + tr.appendChild(td); + } + palette.appendChild(tr); + } + } + + function showFilterPalette(el) { + var picker, pos = el.getBoundingClientRect(); + + if (!$filterPalette) { + buildFilterPalette(); + } + + $.removeClass($filterPalette, 'hidden'); + $filterPalette.setAttribute('data-target', el.id.split('-')[2]); + + picker = $filterPalette.firstElementChild; + picker.style.cssText = 'top:' + pos.top + 'px;left:' + + (pos.left - picker.clientWidth - 10) + 'px;'; + } + + function showFiltersHelp() { + var el = $.id('filters-protip'); + el.style.top = window.pageYOffset + 50 + 'px'; + $.removeClass(el, 'hidden'); + } + + function closeFiltersHelp() { + $.addClass($.id('filters-protip'), 'hidden'); + } + + function onFiltersClick(e) { + var t = e.target; + + if (t.id == 'filters-close') + closeFilters(); + else if (t.id == 'filters-add') + addEmptyFilter(); + else if (t.id == 'filters-save') { + saveFilters(); + closeFilters(); + } + else if (t.hasAttribute('data-active')) + toggleFilter(t, 'active'); + else if (t.hasAttribute('data-hide')) + toggleFilter(t, 'hide', 'top'); + else if (t.hasAttribute('data-top')) + toggleFilter(t, 'top', 'hide'); + else if ($.hasClass(t, 'filter-color')) + showFilterPalette(t); + else if (t.hasAttribute('data-target')) + deleteFilter(t); + else if (t.hasAttribute('data-up')) + moveFilterUp(t); + } + + function moveFilterUp(el) { + var tr, prev; + + tr = el.parentNode.parentNode; + prev = tr.previousElementSibling; + + if (prev) { + tr.parentNode.insertBefore(tr, prev); + } + } + + function onFiltersSearch(e) { + var i, el, nodes, cnt, str; + + if (e && (e.keyCode == 27)) { + this.value = ''; + } + + str = this.value.toLowerCase(); + + nodes = document.getElementsByClassName('filter-pattern'); + + cnt = document.getElementById('filter-list'); + + cnt.style.display = 'none'; + + for (i = 0; el = nodes[i]; ++i) { + if (el.value.toLowerCase().indexOf(str) === -1) { + el.parentNode.parentNode.style.display = 'none'; + } + else { + el.parentNode.parentNode.style.display = ''; + } + } + + cnt.style.display = ''; + } + + function showFilters() { + var i, filtersPanel, rawFilters, filterList, filterId, el; + + filtersPanel = $.id('filters'); + + if (!filtersPanel) { + filtersPanel = FC.panelHTML.build('filters', 'panel hidden'); + FC.panelHTML.build('filters-protip', 'panel hidden'); + FC.panelHTML.build('filter-palette', 'hidden'); + } + + if (!filtersPanel.hasAttribute('data-built')) { + $.on(filtersPanel, 'click', onFiltersClick); + + $.on($.id('filter-palette-close'), 'click', closeFilterPalette); + $.on($.id('filter-palette-clear'), 'click', clearFilterColor); + + $.on($.id('filters-help-open'), 'click', showFiltersHelp); + $.on($.id('filters-help-close'), 'click', closeFiltersHelp); + + $.on($.id('filter-rgb'), 'keyup', filterSetCustomColor); + $.on($.id('filter-rgb-ok'), 'click', selectFilterColor); + + $.on($.id('filters-search'), 'keyup', onFiltersSearch); + + filtersPanel.setAttribute('data-built', '1'); + } + else { + $.id('filters-search').value = ''; + } + + rawFilters = localStorage.getItem('catalog-filters'); + filterId = 0; + + if (rawFilters) { + filterList = $.id('filter-list'); + rawFilters = JSON.parse(rawFilters); + for (i in rawFilters) { + filterList.appendChild(buildFilter(rawFilters[i], filterId)); + ++filterId; + } + updateFilterHitCount(); + } + + filtersPanel.style.top = window.pageYOffset + 60 + 'px'; + + $.removeClass(filtersPanel, 'hidden'); + + if (el = $.cls('filter-active', filtersPanel)[0]) { + el.focus(); + } + + toggleBackdrop(); + } + + function closeFilters() { + var i, filterList, nodes; + + $.id('filters-msg').style.display = 'none'; + $.addClass($.id('filters'), 'hidden'); + + filterList = $.id('filter-list'); + nodes = $.tag('tr', filterList); + for (i = nodes.length - 1; i >= 0; i--) { + filterList.removeChild(nodes[i]); + } + + closeFilterPalette(); + toggleBackdrop(); + } + + function closeFilterPalette() { + if ($filterPalette && !$.hasClass($filterPalette, 'hidden')) { + $.addClass($filterPalette, 'hidden'); + } + } + + // Loads patterns from the localStorage and builds regexps + function loadFilters() { + if (!UA.hasWebStorage) return; + + activeFilters = {}; + + var rawFilters = localStorage.getItem('catalog-filters'); + if (!rawFilters) return; + + rawFilters = JSON.parse(rawFilters); + + var + rf, fid, v, w, wordcount, + wordSepS, wordSepE, + regexType = /^\/(.*)\/(i?)$/, + regexOrNorm = /\s*\|+\s*/g, + regexWc = /\\\*/g, replWc = '[^\\s]*', + regexEscape = getRegexSpecials(), + match, inner, words, rawPattern, pattern, orOp, orCluster, type; + + wordSepS = '(?=.*\\b'; + wordSepE = '\\b)'; + + try { + for (fid in rawFilters) { + rf = rawFilters[fid]; + if (rf.active && rf.pattern !== '') { + if (rf.boards && rf.boards.split(' ').indexOf(catalog.slug) == -1) { + continue; + } + rawPattern = rf.pattern; + if (rawPattern.charAt(0) == '#') { + type = (rawPattern.charAt(1) == '#') ? 2 : 1; + pattern = new RegExp(rawPattern.slice(type).replace(regexEscape, '\\$1')); + } + else { + type = 0; + if (match = rawPattern.match(regexType)) { + pattern = new RegExp(match[1], match[2]); + } + else if (rawPattern.charAt(0) == '"' && rawPattern.charAt(rawPattern.length - 1) == '"') { + pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); + } + else { + words = rawPattern.replace(regexOrNorm, '|').split(' '); + pattern = ''; + wordcount = words.length; + for (w = 0; w < wordcount; ++w) { + if (words[w].indexOf('|') != -1) { + orOp = words[w].split('|'); + orCluster = []; + for (v = orOp.length - 1; v >= 0; v--) { + if (orOp[v] !== '') { + orCluster.push(orOp[v].replace(regexEscape, '\\$1')); + } + } + inner = orCluster.join('|').replace(regexWc, replWc); + pattern += wordSepS + '(' + inner + ')' + wordSepE; + } + else { + inner = words[w].replace(regexEscape, '\\$1').replace(regexWc, replWc); + pattern += wordSepS + inner + wordSepE; + } + } + pattern = new RegExp('^' + pattern, 'i'); + } + } + //console.log('Resulting regex: ' + pattern); + activeFilters[fid] = { + type: type, + pattern: pattern, + boards: rf.boards, + fid: fid, + hidden: rf.hidden, + color: rf.color, + top: rf.top, + hits: 0 + }; + } + } + } + catch (err) { + alert('There was an error processing one of the filters: ' + + err + ' in: ' + rf.pattern); + } + } + + function saveFilters() { + var i, j, f, rawFilters, filterList, msg, rows, color; + + rawFilters = {}; + filterList = $.id('filter-list'); + rows = filterList.children; + + for (i = 0; j = rows[i]; ++i) { + f = { + active: $.cls('filter-active', j)[0].checked ? 1 : 0, + pattern: $.cls('filter-pattern', j)[0].value, + boards: $.cls('filter-boards', j)[0].value, + hidden: $.cls('filter-hide', j)[0].checked ? 1 : 0, + top: $.cls('filter-top', j)[0].checked ? 1 : 0 + }; + color = $.cls('filter-color', j)[0]; + if (!color.hasAttribute('data-nocolor')) { + f.color = color.style.backgroundColor; + } + rawFilters[i] = f; + } + + if (rawFilters[0]) { + localStorage.setItem('catalog-filters', JSON.stringify(rawFilters)); + } + else { + localStorage.removeItem('catalog-filters'); + } + + msg = $.id('filters-msg'); + msg.innerHTML = 'Done'; + msg.className = 'msg-ok'; + msg.style.display = 'inline'; + setTimeout(function() { msg.style.display = 'none'; }, 2000); + + loadFilters(); + buildThreads(); + updateFilterHitCount(); + } + + function filterSetCustomColor() { + var filterRgbOk; + + filterRgbOk = $.id('filter-rgb-ok'); + + filterRgbOk.style.backgroundColor = this.value; + } + + function buildFilter(filter, id) { + var td, tr, span, input; + + tr = document.createElement('tr'); + tr.id = 'filter-' + id; + + // Move up + td = document.createElement('td'); + span = document.createElement('span'); + span.setAttribute('data-up', id); + span.className = 'pointer'; + span.innerHTML = '↑'; + td.appendChild(span); + tr.appendChild(td); + + // On + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = !!filter.active; + input.className = 'filter-active'; + td.appendChild(input); + tr.appendChild(td); + + // Pattern + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'text'; + input.value = filter.pattern; + input.className = 'filter-pattern'; + td.appendChild(input); + tr.appendChild(td); + + // Boards + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'text'; + input.value = filter.boards; + input.className = 'filter-boards'; + td.appendChild(input); + tr.appendChild(td); + + // Color + td = document.createElement('td'); + span = document.createElement('span'); + span.id = 'filter-color-' + id; + span.title = 'Change Color'; + span.className = 'button clickbox filter-color'; + if (!filter.color) { + span.setAttribute('data-nocolor', '1'); + span.innerHTML = '∕'; + } + else { + span.style.background = filter.color; + } + td.appendChild(span); + tr.appendChild(td); + + // Hide + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = !!filter.hidden; + input.className = 'filter-hide'; + td.appendChild(input); + tr.appendChild(td); + + // Top + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = !!filter.top; + input.className = 'filter-top'; + td.appendChild(input); + tr.appendChild(td); + + // Del + td = document.createElement('td'); + span = document.createElement('span'); + span.setAttribute('data-target', id); + span.className = 'pointer'; + span.innerHTML = '×'; + td.appendChild(span); + tr.appendChild(td); + + // Match count + td = document.createElement('td'); + td.id = 'fhc-' + id; + td.className = 'filter-hits'; + tr.appendChild(td); + + return tr; + } + + function selectFilterColor(clear) { + var target = $.id('filter-color-' + $filterPalette.getAttribute('data-target')); + if (clear === true) { + target.setAttribute('data-nocolor', '1'); + target.innerHTML = '∕'; + target.style.background = ''; + } + else { + target.removeAttribute('data-nocolor'); + target.innerHTML = ''; + target.style.background = this.style.backgroundColor; + } + closeFilterPalette(); + } + + function clearFilterColor() { + selectFilterColor(true); + } + + function addEmptyFilter() { + var filter = { + active: 1, + pattern: '', + boards: '', + color: '', + hidden: 0, + top: 0, + hits: 0 + }; + $.id('filter-list').appendChild(buildFilter(filter, getNextFilterId())); + } + + function getNextFilterId() { + var i, j, max, rows = $.id('filter-list').children; + + if (!rows.length) { + return 0; + } + else { + max = 0; + for (i = 0; j = rows[i]; ++i) { + j = +j.id.slice(7); + if (j > max) { + max = j; + } + } + return max + 1; + } + } + + function deleteFilter(t) { + var el = $.id('filter-' + t.getAttribute('data-target')); + el.parentNode.removeChild(el); + } + + function toggleFilter(el, type, xor) { + var attr = 'data-' + type, xorEle; + + if (el.getAttribute(attr) == '0') { + el.setAttribute(attr, '1'); + $.addClass(el, 'active'); + el.innerHTML = '✔'; + if (xor) { + xorEle = $.cls('filter-' + xor, el.parentNode.parentNode)[0]; + xorEle.setAttribute('data-' + xor, '0'); + $.removeClass(xorEle, 'active'); + xorEle.innerHTML = ''; + } + } + else { + el.setAttribute(attr, '0'); + $.removeClass(el, 'active'); + el.innerHTML = ''; + } + } + + function updateFilterHitCount() { + var i, j, rows = $.id('filter-list').children; + for (i = 0; j = rows[i]; ++i) { + $.id('fhc-' + j.id.slice(7)) + .innerHTML = activeFilters[i] ? 'x' + activeFilters[i].hits : ''; + } + } + + function panelHidden(el) { + return el && $.hasClass(el, 'hidden'); + } + + function showThemeEditor() { + var themePanel, el, theme; + + if (!UA.hasWebStorage) { + alert("Your browser doesn't support Local Storage"); + return; + } + + themePanel = $.id('theme'); + + if (!themePanel) { + themePanel = FC.panelHTML.build('theme', 'panel hidden'); + } + + theme = localStorage.getItem('catalog-theme'); + theme = theme ? JSON.parse(theme) : {}; + + $.id('theme-nobinds').checked = !!theme.nobinds; + $.id('theme-nospoiler').checked = !!theme.nospoiler; + $.id('theme-newtab').checked = !!theme.newtab; + $.id('theme-tw').checked = hasThreadWatcher; + $.id('theme-ddn').checked = hasDropDownNav; + + if (theme.css) { + $.id('theme-css').value = theme.css; + } + + $.on($.id('theme-save'), 'click', saveTheme); + $.on($.id('theme-close'), 'click', closeThemeEditor); + + $.id('theme-msg').style.display = 'none'; + + themePanel.style.top = window.pageYOffset + 60 + 'px'; + $.removeClass(themePanel, 'hidden'); + + if (el = $.tag('input', themePanel)[0]) { + el.focus(); + } + + toggleBackdrop(); + + document.dispatchEvent(new CustomEvent('4chanCatalogThemeEditorReady')); + } + + function closeThemeEditor() { + $.off($.id('theme-save'), 'click', saveTheme); + $.off($.id('theme-close'), 'click', closeThemeEditor); + + $.addClass($.id('theme'), 'hidden'); + toggleBackdrop(); + } + + function toggleBackdrop() { + $.toggleClass($.id('backdrop'), 'hidden'); + } + + function loadTheme() { + var customTheme; + + if (UA.hasWebStorage && (customTheme = localStorage.getItem('catalog-theme'))) { + activeTheme = JSON.parse(customTheme); + } + } + + function applyTheme(customTheme, nocss) { + if (customTheme.nobinds) { + if (activeTheme.nobinds != customTheme.nobinds) { + $.off(document, 'keyup', processKeybind); + } + } + else { + if (activeTheme.nobinds != customTheme.nobinds) { + $.on(document, 'keyup', processKeybind); + } + } + + if (customTheme.nospoiler) { + $.addClass(document.body, 'reveal-img-spoilers'); + } + else { + $.removeClass(document.body, 'reveal-img-spoilers'); + } + + if (!nocss) { + self.applyCSS(customTheme); + } + + document.dispatchEvent(new CustomEvent('4chanCatalogThemeApplied')); + } + + self.applyCSS = function(customTheme, style_group, css_version) { + var style, ss; + + if (!customTheme) { + customTheme = activeTheme; + } + + // Preferred stylesheet + if (style_group !== undefined) { + if (!(ss = $.readCookie(style_group))) { + ss = style_group == 'nws_style' ? 'Yotsuba New' : 'Yotsuba B New'; + } + + activeStyleGroup = style_group; + + if (window.css_event && localStorage.getItem('4chan_stop_css_event') !== `${window.css_event}-${window.css_event_v}`) { + activeStyleSheet = '_special' + ss = window.css_event; + } + else { + activeStyleSheet = ss; + } + + style = document.createElement('link'); + style.type = 'text/css'; + style.id = 'base-css'; + style.rel = 'stylesheet'; + style.setAttribute('href', '//s.4cdn.org/css/catalog_' + + ss.toLowerCase().replace(/ /g, '_') + '.' + css_version + '.css'); + document.head.insertBefore(style, $.id('mobile-css')); + } + + // Custom CSS + if (style = $.id('custom-css')) { + document.head.removeChild(style); + } + + if (customTheme.css) { + style = document.createElement('style'); + style.type = 'text/css'; + style.id = 'custom-css'; + + if (style.styleSheet) { + style.styleSheet.cssText = customTheme.css; + } + else { + style.innerHTML = customTheme.css; + } + document.head.appendChild(style); + } + }; + + // Applies and saves the theme to localStorage + function saveTheme() { + var i, css, tw, ddn, extConfig, customTheme = {}; + + if ($.id('theme-nobinds').checked) { + customTheme.nobinds = true; + } + + if ($.id('theme-nospoiler').checked) { + customTheme.nospoiler = true; + } + + if ($.id('theme-newtab').checked) { + customTheme.newtab = true; + } + + tw = $.id('theme-tw').checked; + + ddn = $.id('theme-ddn').checked; + + if (extConfig = localStorage.getItem('4chan-settings')) { + extConfig = JSON.parse(extConfig); + } + else { + extConfig = {}; + } + + if (tw != hasThreadWatcher) { + if (tw) { + ThreadWatcher.init(); + extConfig.disableAll = false; + } + else { + ThreadWatcher.unInit(); + } + } + + if (ddn != hasDropDownNav) { + if (ddn) { + showDropDownNav(); + extConfig.disableAll = false; + } + else { + hideDropDownNav(); + } + } + + extConfig.threadWatcher = tw; + extConfig.dropDownNav = ddn; + localStorage.setItem('4chan-settings', JSON.stringify(extConfig)); + //StorageSync.sync('4chan-settings'); + + hasThreadWatcher = tw; + hasDropDownNav = ddn; + + if ((css = $.id('theme-css').value) !== '') { + customTheme.css = css; + } + + applyTheme(customTheme); + + localStorage.removeItem('catalog-theme'); + + for (i in customTheme) { + localStorage.setItem('catalog-theme', JSON.stringify(customTheme)); + break; + } + + //StorageSync.sync('catalog-theme'); + + activeTheme = customTheme; + + buildThreads(); + closeThemeEditor(); + } + + function loadThreadList(key) { + var i, threads, mod = false, ft = 0; + + if (threads = localStorage.getItem(key)) { + ft = +Object.keys(catalog.threads).pop(); + threads = JSON.parse(threads); + for (i in threads) { + if (!catalog.threads[i] && i < ft) { + delete threads[i]; + mod = true; + } + } + for (i in threads) { + if (mod) { localStorage.setItem(key, JSON.stringify(threads)); } + return threads; + } + localStorage.removeItem(key); + } + return {}; + } + + function loadStorage() { + if (UA.hasWebStorage) { + hiddenThreads = loadThreadList('4chan-hide-t-' + catalog.slug); + pinnedThreads = loadThreadList('4chan-pin-' + catalog.slug); + } + } + + function loadSettings() { + var settings; + if (UA.hasWebStorage && (settings = localStorage.getItem('catalog-settings'))) { + $.extend(options, JSON.parse(settings)); + } + } + + function saveSettings() { + var i, key, settings; + if (!UA.hasWebStorage) { + return; + } + settings = {}; + for (i = basicSettings.length - 1; i >= 0; i--) { + key = basicSettings[i]; + settings[key] = options[key]; + } + localStorage.setItem('catalog-settings', JSON.stringify(settings)); + //StorageSync.sync('catalog-settings'); + } + + function setExtended(mode, init) { + var cls = ''; + if (mode) { + $teaserCtrl.selectedIndex = 1; + cls = 'extended-'; + options.extended = true; + } + else { + $teaserCtrl.selectedIndex = 0; + options.extended = false; + } + if (options.large) { + cls += 'large'; + } + else { + cls += 'small'; + } + $threads.className = cls; + if (!init) { + saveSettings(); + } + } + + function setLarge(mode, init) { + var cls = options.extended ? 'extended-' : ''; + if (mode) { + $sizeCtrl.selectedIndex = 1; + cls += 'large'; + options.large = true; + } + else { + $sizeCtrl.selectedIndex = 0; + cls += 'small'; + options.large = false; + } + $threads.className = cls; + if (!init) { + saveSettings(); + buildThreads(); + } + } + + function setOrder(order, init) { + var o = { alt: 0, absdate: 1, date: 2, r: 3 }; + if (o[order] !== undefined) { + $orderCtrl.selectedIndex = o[order]; + options.orderby = order; + } + else { + $orderCtrl.selectedIndex = 0; + options.orderby = 'date'; + } + if (!init) { + saveSettings(); + buildThreads(); + } + } + + function onTeaserChange() { + setExtended($teaserCtrl.options[$teaserCtrl.selectedIndex].value == 'on'); + } + + function onOrderChange() { + setOrder($orderCtrl.options[$orderCtrl.selectedIndex].value); + } + + function onSizeChange() { + setLarge($sizeCtrl.options[$sizeCtrl.selectedIndex].value == 'large'); + } + + function cycleOrder() { + if (options.orderby == 'date') { + setOrder('alt'); + } + else if (options.orderby == 'alt') { + setOrder('r'); + } + else if (options.orderby == 'r') { + setOrder('absdate'); + } + else { + setOrder('date'); + } + } + + function sortThreadList(threadList) { + var order = options.orderby; + + if (order == 'date') { + threadList.sort(function(a, b) { + if (a.id > b.id) return -1; + if (a.id < b.id) return 1; + return 0; + }); + } + else if (order == 'absdate' && !catalog.no_lr) { + threadList.sort(function(a, b) { + a = a.entry.lr.id; + b = b.entry.lr.id; + if (a > b) return -1; + if (a < b) return 1; + return 0; + }); + } + else if (order == 'r') { + threadList.sort(function(a, b) { + var + a = a.entry.r || 0, + b = b.entry.r || 0; + if (a > b) return -1; + if (a < b) return 1; + return 0; + }); + } + else { // alt + threadList.sort(function(a, b) { + if (a.entry.b < b.entry.b) return -1; + if (a.entry.b > b.entry.b) return 1; + return 0; + }); + } + } + + function getFilteredThreads() { + var i, id, entry, hl, onTop, pinned, teaser, tripcode, af, threads, fid, + filtered; + + filtered = 0; + + threads = []; + + threadloop: for (id in catalog.threads) { + id = +id; + entry = catalog.threads[id]; + hl = onTop = pinned = false; + + if (entry.sub) { + teaser = '' + entry.sub + ''; + if (entry.teaser) { + teaser += ': ' + entry.teaser; + } + } + else { + teaser = entry.teaser; + } + + if (hiddenMode) { + if (!hiddenThreads[id]) { + continue; + } + ++hiddenThreadsCount; + } + else if(!quickFilterPattern) { + if (hiddenThreads[id]) { + ++hiddenThreadsCount; + continue; + } + if (pinnedThreads[id] >= 0) { + pinned = onTop = true; + } + else { + if (entry.capcode) { + tripcode = (entry.trip || '') + '!#' + entry.capcode; + } + else { + tripcode = entry.trip; + } + for (fid in activeFilters) { + af = activeFilters[fid]; + if ((af.type == 0 && (af.pattern.test(teaser) || af.pattern.test(entry.file))) + || (af.type == 1 && af.pattern.test(tripcode)) + || (af.type == 2 && af.pattern.test(entry.author))) { + if (af.hidden) { + ++filtered; + af.hits += 1; + continue threadloop; + } + hl = af; + onTop = !!af.top; + af.hits += 1; + break; + } + } + } + } + else if (!quickFilterPattern.test(teaser) && !quickFilterPattern.test(entry.file)) { + continue; + } + + if (pinnedThreads[id] >= 0) { + pinned = onTop = true; + } + + threads.push( + { + id: id, + entry: entry, + pinned: pinned, + onTop: onTop, + hl: hl + } + ); + } + + filteredThreadsCount = filtered; + + return threads; + } + + function formatImageThreads(threads) { + var + i, k, id, entry, item, thread, hl, onTop, pinned, spoiler, + rDiff, html, provider, contentUrl, + pinhl, newtab, watchKey, teaser, topHtml, stickyHtml, + ratio, maxSize, imgWidth, imgHeight, calcSize, + capcodeReplies, capcodeReply, capcodeTitle, page; + + provider = '//boards.' + $L.d(catalog.slug) + '/' + catalog.slug + '/thread/'; + contentUrl = 'i.4cdn.org/' + catalog.slug + '/'; + + calcSize = !options.large; + newtab = activeTheme.newtab ? 'target="_blank" ' : ''; + + if (catalog.custom_spoiler) { + spoiler = options.imgspoiler + '-' + catalog.slug + catalog.custom_spoiler + '.png'; + } + else { + spoiler = options.imgspoiler + '.png'; + } + + html = ''; + topHtml = ''; + stickyHtml = ''; + + for (i = 0; item = threads[i]; ++i) { + id = item.id; + entry = item.entry; + hl = item.hl; + onTop = item.onTop; + pinned = item.pinned; + + if (entry.sub) { + teaser = '' + entry.sub + ''; + if (entry.teaser) { + teaser += ': ' + entry.teaser; + } + } + else { + teaser = entry.teaser; + } + + thread = '
    '; + + if (hasThreadWatcher) { + watchKey = id + '-' + catalog.slug; + thread += '' : + 'title="Watch" class="watchIcon">'); + } + + thread += ''; + + if (entry.sticky || entry.closed || entry.capcodereps) { + thread += '
    '; + if (entry.sticky) { + thread += ''; + } + if (entry.closed) { + thread += ''; + } + if (entry.capcodereps) { + capcodeReplies = entry.capcodereps.split(','); + for (k = 0; capcodeReply = capcodeReplies[k]; ++k) { + if (capcodeTitle = capcodeMap[capcodeReply]) { + thread += ''; + } + } + } + thread += '
    '; + } + + thread += '
    '; + + if (entry.bumplimit) { + thread += 'R: ' + entry.r + ''; + } + else { + thread += 'R: ' + entry.r + ''; + } + if (pinned) { + rDiff = entry.r - pinnedThreads[id]; + if (rDiff > 0) { + thread += ' (+' + rDiff + ')'; + pinnedThreads[id] = entry.r; + } + else { + thread += '(+0)'; + } + } + if (entry.i) { + if (entry.imagelimit) { + thread += ' / I: ' + entry.i + ''; + } + else { + thread += ' / I: ' + entry.i + ''; + } + } + + if (onTop && (page = getThreadPage(id)) >= 0) { + thread += ' / P: ' + page + ''; + } + + thread += ''; + + thread += '
    '; + + if (teaser) { + thread += '
    '; + } + + if (window.partyHats) { + thread = '
    ' + thread + + '
    '; + } + else { + thread += '
    '; + } + + if (entry.sticky) { + stickyHtml += thread; + } + else if (onTop) { + topHtml += thread; + } + else { + html += thread; + } + } + + topHtml = stickyHtml + topHtml; + + if (quickFilterPattern && (html === '' && topHtml === '')) { + html = '
    Nothing Found
    '; + } + else if (topHtml) { + html = topHtml + html + '
    '; + } + else { + html += '
    '; + } + + return html; + } + + function formatTextThreads(threads) { + var + i, id, entry, item, thread, hl, onTop, pinned, + rDiff, html, provider, + pinhl, newtab, topHtml, aTag; + + provider = '//boards.' + $L.d(catalog.slug) + '/' + catalog.slug + '/thread/'; + + newtab = activeTheme.newtab ? 'target="_blank" ' : ''; + + html = ''; + topHtml = ''; + + for (i = 0; item = threads[i]; ++i) { + id = item.id; + entry = item.entry; + hl = item.hl; + onTop = item.onTop; + pinned = item.pinned; + + if (hl.color) { + pinhl = ' class="hl" style="box-shadow: -3px 0 ' + hl.color + '"'; + } + else if (pinned) { + pinhl = ' class="pinned"'; + } + else { + pinhl = ''; + } + + aTag = ''; + + thread = '' + aTag + + '»' + aTag + entry.sub + + ''; + + if (entry.bumplimit) { + thread += '' + entry.r + ''; + } + else { + thread += entry.r; + } + + if (pinned) { + rDiff = entry.r - pinnedThreads[id]; + if (rDiff > 0) { + thread += ' (+' + rDiff + ')'; + pinnedThreads[id] = entry.r; + } + else { + thread += '(+0)'; + } + } + + thread += '' + entry.date + + ''; + + if (onTop) { + topHtml += thread; + } + else { + html += thread; + } + } + + if (quickFilterPattern && (html === '' && topHtml === '')) { + html = '
    Nothing Found
    '; + } + else if (topHtml) { + html = topHtml + html + '
    '; + } + else { + html += '
    '; + } + + html = '' + + '' + html + '
    SubjectRepliesDate
    '; + + return html; + } + + function buildThreads() { + var i, tip, fid, threads; + + if (catalog.count === 0) { + return; + } + + if ($threads.hasChildNodes()) { + if (tip = document.getElementById('th-tip')) { + document.body.removeChild(tip); + } + $threads.textContent = ''; + } + + hiddenThreadsCount = 0; + filteredThreadsCount = 0; + + for (fid in activeFilters) { + activeFilters[fid].hits = 0; + } + + threads = getFilteredThreads(); + + sortThreadList(threads); + + if (!window.text_only) { + $threads.innerHTML = formatImageThreads(threads); + } + else { + $threads.innerHTML = formatTextThreads(threads); + } + + for (i in pinnedThreads) { + localStorage.setItem('4chan-pin-' + catalog.slug, JSON.stringify(pinnedThreads)); + break; + } + + setFilteredCount(filteredThreadsCount); + + setHiddenCount(hiddenThreadsCount); + } + + function onThreadMouseOver(e) { + var t = e.target; + + if ($.hasClass(t, 'thumb') || (window.text_only && $.hasClass(t, 'txt-date'))) { + clearTimeout(tooltipTimeout); + if (hasTooltip) { + hideTooltip(); + } + tooltipTimeout = setTimeout(showTooltip, options.tipdelay, t); + } + } + + function onThreadMouseOut() { + clearTimeout(tooltipTimeout); + if (hasTooltip) { + hideTooltip(); + } + } + + function showTooltip(t) { + var now, tip, el, rect, docWidth, style, page, tid, thread, top, + bottom, docHeight, left; + + now = Date.now() / 1000; + + rect = t.getBoundingClientRect(); + docWidth = document.documentElement.offsetWidth; + + tid = t.getAttribute('data-id'); + + if (!tid) { + return; + } + + thread = catalog.threads[tid]; + + if (page = getThreadPage(tid)) { + page = 'Page ' + page + ''; + } + else { + page = ''; + } + + if (thread.sub && !window.text_only) { + tip = '' + thread.sub + ''; + } + else { + tip = 'Posted'; + } + + tip += ' by ' + (thread.author || catalog.anon); + + if (thread.trip) { + tip += ' ' + thread.trip + ''; + } + + if (thread.capcode) { + tip += ' ## ' + + capcodeMap[thread.capcode]; + } + + tip += ' '; + + if (catalog.flags && thread.country) { + tip += '
    '; + } + + tip += '' + + getDuration(now - thread.date) + + ' ago' + page; + + if ((!options.extended && thread.teaser) || window.text_only) { + tip += ''; + } + + if (thread.lr.date) { + tip += '
    Last reply by ' + thread.lr.author; + + if (thread.lr.trip) { + tip += ' ' + thread.lr.trip + ''; + } + + if (thread.lr.capcode) { + tip += ' ## ' + + thread.lr.capcode.charAt(0).toUpperCase() + + thread.lr.capcode.slice(1); + } + + if (thread.lr.date) { + tip += ' ' + + getDuration(now - thread.lr.date) + + ' ago'; + } + else { + tip += ''; + } + } + + el = document.createElement('div'); + el.id = 'post-preview'; + el.innerHTML = tip; + document.body.appendChild(el); + + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + left = rect.left - el.offsetWidth - 5; + } + else { + left = rect.left + rect.width + 5; + } + + docHeight = document.documentElement.clientHeight; + + bottom = rect.top + el.offsetHeight; + + if (bottom > docHeight) { + top = rect.top - (bottom - docHeight) - 20; + } + else { + top = rect.top; + } + + if (top < 0) { + top = 3; + } + + style = el.style; + style.left = left + window.pageXOffset + 'px'; + style.top = top + window.pageYOffset + 'px'; + + hasTooltip = true; + } + + function hideTooltip() { + document.body.removeChild($.id('post-preview')); + hasTooltip = false; + } + + function getDuration(delta, precise) { + var count, head, tail; + if (delta < 2) { + return 'less than a second'; + } + if (precise && delta < 300) { + return (0 | delta) + ' seconds'; + } + if (delta < 60) { + return (0 | delta) + ' seconds'; + } + if (delta < 3600) { + count = 0 | (delta / 60); + if (count > 1) { + return count + ' minutes'; + } + else { + return 'one minute'; + } + } + if (delta < 86400) { + count = 0 | (delta / 3600); + if (count > 1) { + head = count + ' hours'; + } + else { + head = 'one hour'; + } + tail = 0 | (delta / 60 - count * 60); + if (tail > 1) { + head += ' and ' + tail + ' minutes'; + } + return head; + } + count = 0 | (delta / 86400); + if (count > 1) { + head = count + ' days'; + } + else { + head = 'one day'; + } + tail = 0 | (delta / 3600 - count * 24); + if (tail > 1) { + head += ' and ' + tail + ' hours'; + } + return head; + } +}; + +var Filter = {}; + +Filter.init = function() { + this.entities = document.createElement('div'); + Filter.load(); +}; + +Filter.match = function(post, board) { + var i, com, f, filters, hit; + + hit = false; + filters = Filter.activeFilters; + + for (i = 0; f = filters[i]; ++i) { + // boards + if (!f.boards[board]) { + continue; + } + // tripcode + if (f.type == 0) { + if (f.pattern === post.trip) { + hit = true; + break; + } + } + // name + else if (f.type == 1) { + if (f.pattern === post.name) { + hit = true; + break; + } + } + // comment + else if (f.type == 2 && post.com) { + if (com === undefined) { + this.entities.innerHTML + = post.com.replace(/
    /g, '\n').replace(/[<[^>]+>/g, ''); + com = this.entities.textContent; + } + if (f.pattern.test(com)) { + hit = true; + break; + } + } + // user id + else if (f.type == 4) { + if (f.pattern === post.id) { + hit = true; + break; + } + } + // subject + else if (f.type == 5) { + if (f.pattern.test(post.sub)) { + hit = true; + break; + } + } + // filename + else if (f.type == 6) { + if (f.pattern.test(post.filename)) { + hit = true; + break; + } + } + } + + return hit; +}; + +FC.getDocTopOffset = function() { + if (window.Config.dropDownNav && !window.Config.autoHideNav) { + return $.id( + window.Config.classicNav ? 'boardNavDesktop' : 'boardNavMobile' + ).offsetHeight; + } + else { + return 0; + } +}; + +Filter.load = function() { + var i, j, f, rawFilters, rawPattern, fid, regexEscape, regexType, + wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard, boards, + pattern, match, tmp; + + this.activeFilters = []; + + if (!(rawFilters = localStorage.getItem('4chan-filters'))) { + return; + } + + rawFilters = JSON.parse(rawFilters); + + regexEscape = new RegExp('(\\' + + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\') + + ')', 'g'); + regexType = /^\/(.*)\/(i?)$/; + wordSepS = '(?=.*\\b'; + wordSepE = '\\b)'; + regexWildcard = /\\\*/g; + replaceWildcard = '[^\\s]*'; + + try { + for (fid = 0; f = rawFilters[fid]; ++fid) { + if (f.active && f.pattern !== '') { + // Boards + if (f.boards) { + tmp = f.boards.split(/[^a-z0-9]+/i); + boards = {}; + for (i = 0; j = tmp[i]; ++i) { + boards[j] = true; + } + } + else { + boards = false; + } + + rawPattern = f.pattern; + // Name, Tripcode or ID, string comparison + if (!f.type || f.type == 1 || f.type == 4) { + pattern = rawPattern; + } + // /RegExp/ + else if (match = rawPattern.match(regexType)) { + pattern = new RegExp(match[1], match[2]); + } + // "Exact match" + else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') { + pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); + } + // Full words, AND operator + else { + words = rawPattern.split(' '); + pattern = ''; + for (i = 0, j = words.length; i < j; ++i) { + inner = words[i] + .replace(regexEscape, '\\$1') + .replace(regexWildcard, replaceWildcard); + pattern += wordSepS + inner + wordSepE; + } + pattern = new RegExp('^' + pattern, 'im'); + } + //console.log('Resulting pattern: ' + pattern); + this.activeFilters.push({ + type: f.type, + pattern: pattern, + boards: boards, + color: f.color, + hide: f.hide, + auto: f.auto + }); + } + } + } + catch (e) { + alert('There was an error processing one of the filters: ' + + e + ' in: ' + rawPattern); + } +}; + +/** + * Thread watcher + */ +var ThreadWatcher = { + hasFilters: false +}; + +ThreadWatcher.init = function() { + var cnt, pos, el; + + if (this.hasFilters) { + Filter.init(); + } + + this.listNode = null; + this.charLimit = 45; + this.watched = {}; + this.blacklisted = {}; + this.isRefreshing = false; + + if (FC.hasMobileLayout) { + el = document.createElement('a'); + el.href = '#'; + el.textContent = 'TW'; + el.addEventListener('click', ThreadWatcher.toggleList, false); + cnt = $.id('settingsWindowLinkMobile'); + cnt.parentNode.insertBefore(el, cnt); + cnt.parentNode.insertBefore(document.createTextNode(' '), cnt); + } + + cnt = document.createElement('div'); + cnt.id = 'threadWatcher'; + cnt.setAttribute('data-trackpos', 'TW-position'); + + if (FC.hasMobileLayout) { + cnt.style.display = 'none'; + } + else { + if (window.Config['TW-position']) { + cnt.style.cssText = window.Config['TW-position']; + } + else { + cnt.style.left = '10px'; + cnt.style.top = '75px'; + } + } + + cnt.innerHTML = '
    ' + + (FC.hasMobileLayout ? ('
    ') : '') + + 'Thread Watcher' + + (UA.hasCORS ? ('
    ') : '
    '); + + this.listNode = document.createElement('ul'); + this.listNode.id = 'watchList'; + + this.load(); + + this.build(); + + cnt.appendChild(this.listNode); + document.body.appendChild(cnt); + cnt.addEventListener('mouseup', this.onClick, false); + Draggable.set($.id('twHeader')); + window.addEventListener('storage', this.syncStorage, false); + + if (!FC.hasMobileLayout && this.canAutoRefresh()) { + this.refresh(); + } +}; + +ThreadWatcher.unInit = function() { + var cnt; + + if (cnt = $.id('threadWatcher')) { + cnt.removeEventListener('mouseup', this.onClick, false); + Draggable.unset($.id('twHeader')); + window.removeEventListener('storage', this.syncStorage, false); + document.body.removeChild(cnt); + } +}; + +ThreadWatcher.toggleList = function(e) { + var el = $.id('threadWatcher'); + + e && e.preventDefault(); + + if (ThreadWatcher.canAutoRefresh()) { + ThreadWatcher.refresh(); + } + + if (el.style.display == 'none') { + el.style.top = (window.pageYOffset + 30) + 'px'; + el.style.display = ''; + } + else { + el.style.display = 'none'; + } +}; + +ThreadWatcher.syncStorage = function(e) { + var key; + + if (!e.key) { + return; + } + + key = e.key.split('-'); + + if (key[0] == '4chan' && key[1] == 'watch' && e.newValue != e.oldValue) { + ThreadWatcher.load(); + ThreadWatcher.build(); + } +}; + +ThreadWatcher.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-watch')) { + this.watched = JSON.parse(storage); + } + if (storage = localStorage.getItem('4chan-watch-bl')) { + this.blacklisted = JSON.parse(storage); + } +}; + +ThreadWatcher.build = function() { + var html, tuid, key, cls; + + html = ''; + + for (key in this.watched) { + tuid = key.split('-'); + html += '
  • × (' + this.watched[key][2] + ') '; + } + else { + html += (cls[0] ? ('class="' + cls.join(' ') + '"') : '') + '>'; + } + } + + html += '/' + tuid[1] + '/ - ' + this.watched[key][0] + '
  • '; + } + + ThreadWatcher.listNode.innerHTML = html; +}; + +ThreadWatcher.onClick = function(e) { + var t = e.target; + + if (t.hasAttribute('data-id')) { + ThreadWatcher.toggle( + t.getAttribute('data-id'), + t.getAttribute('data-board') + ); + } + else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) { + ThreadWatcher.refreshWithAutoWatch(); + } + else if (t.id == 'twClose') { + ThreadWatcher.toggleList(); + } +}; + +ThreadWatcher.generateLabel = function(sub, com, tid) { + var label; + + if (label = sub) { + label = label.slice(0, this.charLimit); + } + else if (label = com) { + label = label.replace(/(?:
    )+/g, ' ') + .replace(/<[^>]*?>/g, '').slice(0, this.charLimit); + } + else { + label = 'No.' + tid; + } + + return label; +}; + +ThreadWatcher.toggle = function(tid, board, sub, com, lr) { + var key, label, lastReply, icon; + + key = tid + '-' + board; + icon = $.id('leaf-' + tid); + + if (this.watched[key]) { + delete this.watched[key]; + if (icon) { + icon.className = 'watchIcon'; + icon.title = 'Watch'; + } + } + else { + label = ThreadWatcher.generateLabel(sub, com, tid); + + lastReply = lr || tid; + + this.watched[key] = [ label, lastReply, 0 ]; + + icon.className = 'unwatchIcon'; + icon.title = 'Unwatch'; + } + this.save(); + this.load(); + this.build(); +}; + +ThreadWatcher.addRaw = function(post, board) { + var key, label; + + key = post.no + '-' + board; + + if (this.watched[key]) { + return; + } + + label = ThreadWatcher.generateLabel(post.sub, post.com, post.no); + + this.watched[key] = [ label, 0, 0 ]; +}; + +ThreadWatcher.save = function() { + var i; + + ThreadWatcher.sortByBoard(); + + localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched)); + + //StorageSync.sync('4chan-watch'); + + for (i in ThreadWatcher.blacklisted) { + localStorage.setItem('4chan-watch-bl', JSON.stringify(ThreadWatcher.blacklisted)); + //StorageSync.sync('4chan-watch-bl'); + break; + } +}; + +ThreadWatcher.sortByBoard = function() { + var i, self, key, sorted, keys; + + self = ThreadWatcher; + + sorted = {}; + keys = []; + + for (key in self.watched) { + keys.push(key); + } + + keys.sort(function(a, b) { + a = a.split('-')[1]; + b = b.split('-')[1]; + + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }); + + for (i = 0; key = keys[i]; ++i) { + sorted[key] = self.watched[key]; + } + + self.watched = sorted; +}; + +ThreadWatcher.canAutoRefresh = function() { + var time; + + if (time = localStorage.getItem('4chan-tw-timestamp')) { + return Date.now() - (+time) >= 60000; + } + return false; +}; + +ThreadWatcher.setRefreshTimestamp = function() { + localStorage.setItem('4chan-tw-timestamp', Date.now()); + //StorageSync.sync('4chan-tw-timestamp'); +}; + +ThreadWatcher.refreshWithAutoWatch = function() { + var i, f, count, board, boards, img; + + if (!this.hasFilters) { + this.refresh(); + return; + } + + Filter.load(); + + boards = {}; + count = 0; + + for (i = 0; f = Filter.activeFilters[i]; ++i) { + if (!f.auto || !f.boards) { + continue; + } + for (board in f.boards) { + if (boards[board]) { + continue; + } + boards[board] = true; + ++count; + } + } + + if (!count) { + this.refresh(); + return; + } + + img = $.id('twPrune'); + img.className = 'icon rotateIcon'; + this.isRefreshing = true; + + this.fetchCatalogs(boards, count); +}; + +ThreadWatcher.fetchCatalogs = function(boards, count) { + var to, board, catalogs, meta; + + catalogs = {}; + meta = { count: count }; + to = 0; + + for (board in boards) { + setTimeout(ThreadWatcher.fetchCatalog, to, board, catalogs, meta); + to += 200; + } +}; + +ThreadWatcher.parseCatalogJSON = function(data) { + var catalog; + + try { + catalog = JSON.parse(data); + } + catch (e) { + console.log(e); + catalog = []; + } + + return catalog; +}; + +ThreadWatcher.fetchCatalog = function(board, catalogs, meta) { + var xhr; + + xhr = new XMLHttpRequest(); + xhr.open('GET', '//a.4cdn.org/' + board + '/catalog.json'); + xhr.onload = function() { + meta.count--; + catalogs[board] = ThreadWatcher.parseCatalogJSON(this.responseText); + if (!meta.count) { + ThreadWatcher.onCatalogsLoaded(catalogs); + } + }; + xhr.onerror = function() { + meta.count--; + if (!meta.count) { + ThreadWatcher.onCatalogsLoaded(catalogs); + } + }; + xhr.send(null); +}; + +ThreadWatcher.onCatalogsLoaded = function(catalogs) { + var i, j, board, page, pages, threads, thread, key, blacklisted; + + $.id('twPrune').className = 'icon rotateIcon'; + this.isRefreshing = false; + + blacklisted = {}; + + for (board in catalogs) { + pages = catalogs[board]; + for (i = 0; page = pages[i]; ++i) { + threads = page.threads; + for (j = 0; thread = threads[j]; ++j) { + key = thread.no + '-' + board; + if (this.blacklisted[key]) { + blacklisted[key] = 1; + continue; + } + if (Filter.match(thread, board)) { + this.addRaw(thread, board); + } + } + } + } + + this.blacklisted = blacklisted; + this.build(true); + this.refresh(); +}; + +ThreadWatcher.refresh = function() { + var i, to, key, total, img; + + if (total = $.id('watchList').children.length) { + i = to = 0; + img = $.id('twPrune'); + img.className = 'icon rotateIcon'; + ThreadWatcher.isRefreshing = true; + ThreadWatcher.setRefreshTimestamp(); + for (key in ThreadWatcher.watched) { + setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null); + to += 200; + } + } +}; + +ThreadWatcher.onRefreshEnd = function(img) { + img.className = 'icon refreshIcon'; + this.isRefreshing = false; + this.save(); + this.load(); + this.build(); +}; + +ThreadWatcher.parseThreadJSON = function(data) { + var thread; + + try { + thread = JSON.parse(data).posts; + } + catch (e) { + console.log(e); + thread = []; + } + + return thread; +}; + +ThreadWatcher.getTrackedReplies = function(board, tid) { + var tracked = null; + + if (tracked = localStorage.getItem('4chan-track-' + board + '-' + tid)) { + tracked = JSON.parse(tracked); + } + + return tracked; +}; + +ThreadWatcher.fetch = function(key, img) { + var tuid, xhr, li; + + li = $.id('watch-' + key); + + if (ThreadWatcher.watched[key][1] == -1) { + delete ThreadWatcher.watched[key]; + li.parentNode.removeChild(li); + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + return; + } + + tuid = key.split('-'); // tid, board + + xhr = new XMLHttpRequest(); + xhr.onload = function() { + var i, newReplies, posts, lastReply, trackedReplies, dummy, quotelinks, q, j; + if (this.status == 200) { + posts = ThreadWatcher.parseThreadJSON(this.responseText); + lastReply = ThreadWatcher.watched[key][1]; + newReplies = 0; + + if (!ThreadWatcher.watched[key][4]) { + trackedReplies = ThreadWatcher.getTrackedReplies(tuid[1], tuid[0]); + + if (trackedReplies) { + dummy = document.createElement('div'); + } + } + else { + trackedReplies = null; + } + + for (i = posts.length - 1; i >= 1; i--) { + if (posts[i].no <= lastReply) { + break; + } + ++newReplies; + + if (trackedReplies) { + dummy.innerHTML = posts[i].com; + quotelinks = $.cls('quotelink', dummy); + + if (!quotelinks[0]) { + continue; + } + + for (j = 0; q = quotelinks[j]; ++j) { + if (trackedReplies[q.textContent]) { + ThreadWatcher.watched[key][4] = 1; + trackedReplies = null; + break; + } + } + } + } + if (newReplies > ThreadWatcher.watched[key][2]) { + ThreadWatcher.watched[key][2] = newReplies; + } + if (posts[0].archived) { + ThreadWatcher.watched[key][3] = 1; + } + } + else if (this.status == 404) { + ThreadWatcher.watched[key][1] = -1; + } + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + }; + if (img) { + xhr.onerror = xhr.onload; + } + xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json'); + xhr.send(null); +}; + +ThreadWatcher.linkToThread = function(tid, board, post) { + return '//' + location.host + '/' + + board + '/thread/' + + tid + (post > 0 ? ('#p' + post) : ''); +}; + +/** + * Draggable helper + */ +var Draggable = { + el: null, + key: null, + scrollX: null, + scrollY: null, + dx: null, dy: null, right: null, bottom: null, + + set: function(handle) { + handle.addEventListener('mousedown', Draggable.startDrag, false); + }, + + unset: function(handle) { + handle.removeEventListener('mousedown', Draggable.startDrag, false); + }, + + startDrag: function(e) { + var self, doc, offs; + + if (this.parentNode.hasAttribute('data-shiftkey') && !e.shiftKey) { + return; + } + + e.preventDefault(); + + self = Draggable; + doc = document.documentElement; + + self.el = this.parentNode; + + self.key = self.el.getAttribute('data-trackpos'); + offs = self.el.getBoundingClientRect(); + self.dx = e.clientX - offs.left; + self.dy = e.clientY - offs.top; + self.right = doc.clientWidth - offs.width; + self.bottom = doc.clientHeight - offs.height; + + if (getComputedStyle(self.el, null).position != 'fixed') { + self.scrollX = window.pageXOffset; + self.scrollY = window.pageYOffset; + } + else { + self.scrollX = self.scrollY = 0; + } + + self.offsetTop = FC.getDocTopOffset(); + + document.addEventListener('mouseup', self.endDrag, false); + document.addEventListener('mousemove', self.onDrag, false); + }, + + endDrag: function() { + document.removeEventListener('mouseup', Draggable.endDrag, false); + document.removeEventListener('mousemove', Draggable.onDrag, false); + if (Draggable.key && window.Config) { + window.Config[Draggable.key] = Draggable.el.style.cssText; + localStorage.setItem('4chan-settings', JSON.stringify(window.Config)); + //StorageSync.sync('4chan-settings'); + } + delete Draggable.el; + }, + + onDrag: function(e) { + var left, top, style; + + left = e.clientX - Draggable.dx + Draggable.scrollX; + top = e.clientY - Draggable.dy + Draggable.scrollY; + style = Draggable.el.style; + if (left < 1) { + style.left = '0'; + style.right = ''; + } + else if (Draggable.right < left) { + style.left = ''; + style.right = '0'; + } + else { + style.left = (left / document.documentElement.clientWidth * 100) + '%'; + style.right = ''; + } + if (top <= Draggable.offsetTop) { + style.top = Draggable.offsetTop + 'px'; + style.bottom = ''; + } + else if (Draggable.bottom < top && + Draggable.el.clientHeight < document.documentElement.clientHeight) { + style.bottom = '0'; + style.top = ''; + } + else { + style.top = (top / document.documentElement.clientHeight * 100) + '%'; + style.bottom = ''; + } + } +}; + +/** + * Custom Menu + */ +var CustomMenu = { + dropDownNav: false, + classicNav: false +}; + +CustomMenu.initCtrl = function(dropDownNav, classicNav) { + var el, cnt; + + CustomMenu.dropDownNav = dropDownNav; + CustomMenu.classicNav = classicNav; + + el = document.createElement('span'); + el.className = 'custom-menu-ctrl'; + el.innerHTML = '[Edit]'; + + if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { + cnt = $.id('boardSelectMobile').parentNode; + cnt.insertBefore(el, cnt.lastChild); + } + else { + cnt = $.cls('boardList'); + cnt[0] && cnt[0].appendChild(el); + cnt[1] && cnt[1].appendChild(el.cloneNode(true)); + } +}; +/* +CustomMenu.showNWSBoards = function() { + var i, el, nodes, len; + + nodes = $.cls('nwsb'); + len = nodes.length; + + for (i = len - 1; el = nodes[i]; i--) { + $.removeClass(el, 'nwsb'); + } +}; +*/ +CustomMenu.reset = function() { + var i, el, full, custom, navs; + + full = $.cls('boardList'); + custom = $.cls('customBoardList'); + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.removeEventListener('click', CustomMenu.reset, false); + } + + for (i = custom.length - 1; el = custom[i]; i--) { + full[i].style.display = null; + el.parentNode.removeChild(el); + } +}; + +CustomMenu.apply = function(str) { + var i, el, cntBottom, board, navs, boardList, cnt; + + if (!str) { + if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { + if (el = $.cls('customBoardList')[0]) { + el.parentNode.removeChild(el); + } + } + return; + } + + boardList = str.split(/[^0-9a-z]/i); + + cnt = document.createElement('span'); + cnt.className = 'customBoardList'; + + for (i = 0; board = boardList[i]; ++i) { + if (i) { + cnt.appendChild(document.createTextNode(' / ')); + } + else { + cnt.appendChild(document.createTextNode('[')); + } + el = document.createElement('a'); + el.textContent = board; + el.href = '//boards.' + $L.d(board) + '/' + board + (board !== 'f' ? '/catalog' : ''); + cnt.appendChild(el); + } + + cnt.appendChild(document.createTextNode(']')); + + if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { + if (el = $.cls('customBoardList')[0]) { + el.parentNode.removeChild(el); + } + navs = $.id('boardSelectMobile'); + navs && navs.parentNode.insertBefore(cnt, navs.nextSibling); + } + else { + cnt.appendChild(document.createTextNode(' [')); + el = document.createElement('a'); + el.textContent = '…'; + el.title = 'Show all'; + el.className = 'show-all-boards pointer'; + cnt.appendChild(el); + cnt.appendChild(document.createTextNode('] ')); + + cntBottom = cnt.cloneNode(true); + + navs = $.cls('boardList'); + + for (i = 0; el = navs[i]; ++i) { + el.style.display = 'none'; + el.parentNode.insertBefore(i ? cntBottom : cnt, el); + } + + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.addEventListener('click', CustomMenu.reset, false); + } + } +}; + +CustomMenu.onClick = function(e) { + var t; + + if ((t = e.target) == document) { + return; + } + + if (t.hasAttribute('data-close')) { + CustomMenu.closeEditor(); + } + else if (t.hasAttribute('data-save')) { + CustomMenu.save($.id('customMenu').hasAttribute('data-standalone')); + } +}; + +CustomMenu.showEditor = function(standalone) { + var cnt, extConfig; + + cnt = document.createElement('div'); + cnt.id = 'customMenu'; + cnt.className = 'panel'; + cnt.setAttribute('data-close', '1'); + + if (standalone === true) { + cnt.setAttribute('data-standalone', '1'); + } + + cnt.innerHTML = '\ +
    Custom Board List\ +
    \ +\ +
    '; + + document.body.appendChild(cnt); + + cnt.style.top = window.pageYOffset + + (0 | (document.documentElement.clientHeight / 2) - (cnt.offsetHeight / 2)) + 'px'; + + $.removeClass($.id('backdrop'), 'hidden'); + + extConfig = CustomMenu.getConfig(); + + if (extConfig.customMenuList) { + $.id('customMenuBox').value = extConfig.customMenuList; + } + + cnt.addEventListener('click', CustomMenu.onClick, false); +}; + +CustomMenu.closeEditor = function() { + var el; + + if (el = $.id('customMenu')) { + el.removeEventListener('click', CustomMenu.onClick, false); + document.body.removeChild(el); + $.addClass($.id('backdrop'), 'hidden'); + } +}; + +CustomMenu.save = function(standalone) { + var input, extConfig; + + if (input = $.id('customMenuBox')) { + if (standalone === true) { + CustomMenu.apply(input.value); + + extConfig = CustomMenu.getConfig(); + + extConfig.customMenu = true; + extConfig.customMenuList = input.value; + + localStorage.setItem('4chan-settings', JSON.stringify(extConfig)); + //StorageSync.sync('4chan-settings'); + } + } + + CustomMenu.closeEditor(); +}; + +CustomMenu.getConfig = function() { + var extConfig; + + if (extConfig = localStorage.getItem('4chan-settings')) { + return JSON.parse(extConfig); + } + else { + return {}; + } +}; + +function checkMobileLayout() { + var mobile, desktop; + + if (window.matchMedia) { + return window.matchMedia('(max-width: 480px)').matches + && localStorage.getItem('4chan_never_show_mobile') != 'true'; + } + + mobile = $.id('boardNavMobile'); + desktop = $.id('boardNavDesktop'); + + return mobile && desktop && mobile.offsetWidth > 0 && desktop.offsetWidth === 0; +} + +var StickyNav = { + thres: 5, + pos: 0, + timeout: null, + el: null, + + init: function(classicNav) { + this.el = classicNav ? $.id('boardNavDesktop') : $.id('boardNavMobile'); + $.addClass(this.el, 'autohide-nav'); + window.addEventListener('scroll', this.onScroll, false); + }, + + destroy: function(classicNav) { + this.el = classicNav ? $.id('boardNavDesktop') : $.id('boardNavMobile'); + $.removeClass(this.el, 'autohide-nav'); + window.removeEventListener('scroll', this.onScroll, false); + }, + + onScroll: function() { + clearTimeout(StickyNav.timeout); + StickyNav.timeout = setTimeout(StickyNav.checkScroll, 50); + }, + + checkScroll: function() { + var thisPos; + + thisPos = window.pageYOffset; + + if (Math.abs(StickyNav.pos - thisPos) <= StickyNav.thres) { + return; + } + + if (thisPos < StickyNav.pos) { + StickyNav.el.style.top = ''; + } + else { + StickyNav.el.style.top = '-' + StickyNav.el.offsetHeight + 'px'; + } + + StickyNav.pos = thisPos; + } +}; + +FC.panelHTML = { + build: function(id, cls) { + var el; + + el = document.createElement('div'); + el.id = id; + el.className = cls; + el.innerHTML = FC.panelHTML[id]; + + document.body.appendChild(el); + + return el; + }, + + 'theme': '
    Settings
    \ +

    Options

    \ +
      \ +
    • \ +
    • \ +
    • \ +
    • \ +
    • \ +
    \ +

    Shortcuts

    \ +
      \ +
    • R — Refresh current page
    • \ +
    • X — Reorder threads
    • \ +
    • S — Open search box, Esc to close
    • \ +
    • Shift LMB — Hide threads
    • \ +
    • Alt LMB — Pin threads
    • \ +
    • RMB — Threads context menu (Firefox only)
    • \ +
    \ +

    Custom CSS

    \ + \ +
    \ + \ +
    \ +
    ', + + 'filters-protip': '
    Filters & Highlights Help
    \ +

    Patterns

    \ +
      • \ +
      • Matching whole words:
      • \ +
      • feel — will match "feel" but not "feeling". This search is case-insensitive.
      • \ +
    • \ +
      • \ +
      • AND operator:
      • \ +
      • feel girlfriend — will match "feel" AND "girlfriend" in any order.
      • \ +
    • \ +
      • \ +
      • OR operator:
      • \ +
      • feel|girlfriend — will match "feel" OR "girlfriend".
      • \ +
    • \ +
      • \ +
      • Mixing both operators:
      • \ +
      • girlfriend|boyfriend feel — matches "feel" AND "girlfriend", or "feel" AND "boyfriend".
      • \ +
    • \ +
      • \ +
      • Exact match search:
      • \ +
      • "that feel when" — place double quotes around the pattern to search for an exact string
      • \ +
    • \ +
      • \ +
      • Wildcards:
      • \ +
      • feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc…
      • \ +
      • idolm*ster — this can match "idolmaster" or "idolm@ster", etc…
      • \ +
    • \ +
      • \ +
      • Filtering by name or tripcode:
      • \ +
      • Prefix the pattern with # to search by tripcode: #!Ep8pui8Vw2
      • \ +
      • Prefix the pattern with ## to search by name: ##Anonymous
      • \ +
      • To filter by capcode: #!#admin, #!#mod, #!#developer
      • \ +
    • \ +
      • \ +
      • It is also possible to filter by regular expression:
      • \ +
      • /^(?=.*detachable)(?=.*hats).*$/i — AND operator.
      • \ +
      • /^(?!.*touhou).*$/i — NOT operator.
      • \ +
      • /^&gt;/ — threads starting with a quote (">" character as an html entity).
      • \ +
      • /^$/ — threads with no text.
      • \ +
    • \ +
    \ +
    \ +

    Controls

    \ +
      \ +
    • On — enables or disables the filter.
    • \ +
    • Boards — space separated list of boards on which the filter will be active. Leave blank to apply to all boards.
    • \ +
    • Hide — hides matched threads.
    • \ +
    • Top — puts matched threads on top of the list.
    • \ +
    ', + + 'filter-palette': '
    \ + \ + \ + \ +
    Custom
    \ + Close\ + Clear\ +
    ', + + 'filters': '
    Filters & Highlights
    \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
    OrderOnPatternBoardsColorHideTopDel
    \ + \ + \ +
    ' +}; + +var PostMenu = { + activeBtn: null +}; + +PostMenu.open = function(btn, pid, hasThreadWatcher, hidden, pinned) { + var div, html, btnPos, left, limit, tr; + + if (PostMenu.activeBtn == btn) { + PostMenu.close(); + return; + } + + PostMenu.close(); + + tr = btn.parentNode.parentNode; + + html = '
    • Report thread
    • ' + + '
    • ' + + (pinned ? 'Unpin' : 'Pin') + ' thread
    • ' + + '
    • ' + + (hidden ? 'Unhide' : 'Hide') + ' thread
    • '; + + if (hasThreadWatcher) { + html += '
    • ' + + (ThreadWatcher.watched[pid + '-' + catalog.slug] ? 'Remove from' : 'Add to') + + ' watch list
    • '; + } + + div = document.createElement('div'); + div.id = 'post-menu'; + div.className = 'dd-menu'; + div.innerHTML = html + '
    '; + + btnPos = btn.getBoundingClientRect(); + + div.style.top = btnPos.bottom + 3 + window.pageYOffset + 'px'; + + document.addEventListener('click', PostMenu.close, false); + + $.addClass(btn, 'menuOpen'); + PostMenu.activeBtn = btn; + + UA.dispatchEvent('4chanPostMenuReady', { postId: pid, isOP: true, node: div.firstElementChild }); + + document.body.appendChild(div); + + left = btnPos.left + window.pageXOffset; + limit = document.documentElement.clientWidth - div.offsetWidth; + + if (left > (limit - 75)) { + div.className += ' dd-menu-left'; + } + + if (left > limit) { + left = limit; + } + + div.style.left = left + 'px'; +}; + +PostMenu.close = function() { + var el; + + if (el = $.id('post-menu')) { + el.parentNode.removeChild(el); + document.removeEventListener('click', PostMenu.close, false); + $.removeClass(PostMenu.activeBtn, 'menuOpen'); + PostMenu.activeBtn = null; + } +}; \ No newline at end of file diff --git a/js/catalog.js b/js/catalog.js new file mode 100644 index 0000000..0705a1e --- /dev/null +++ b/js/catalog.js @@ -0,0 +1,3613 @@ +var $ = {}; + +$.id = function(id) { + return document.getElementById(id); +}; + +$.cls = function(klass, root) { + return (root || document).getElementsByClassName(klass); +}; + +$.tag = function(tag, root) { + return (root || document).getElementsByTagName(tag); +}; + +$.extend = function(destination, source) { + for (var key in source) { + destination[key] = source[key]; + } +}; + +$.on = function(n, e, h) { + n.addEventListener(e, h, false); +}; + +$.off = function(n, e, h) { + n.removeEventListener(e, h, false); +}; + +$.readCookie = function(name) { + var i, c, ca, key; + + key = name + '='; + ca = document.cookie.split(';'); + + for (i = 0; c = ca[i]; ++i) { + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(key) === 0) { + return decodeURIComponent(c.substring(key.length, c.length)); + } + } + return null; +}; + +if (!document.documentElement.classList) { + $.hasClass = function(el, klass) { + return (' ' + el.className + ' ').indexOf(' ' + klass + ' ') != -1; + }; + + $.addClass = function(el, klass) { + el.className = (el.className === '') ? klass : el.className + ' ' + klass; + }; + + $.removeClass = function(el, klass) { + el.className = (' ' + el.className + ' ').replace(' ' + klass + ' ', ''); + }; +} +else { + $.hasClass = function(el, klass) { + return el.classList.contains(klass); + }; + + $.addClass = function(el, klass) { + el.classList.add(klass); + }; + + $.removeClass = function(el, klass) { + el.classList.remove(klass); + }; +} + +$.toggleClass = function(el, klass) { + if ($.hasClass(el, klass)) { + $.removeClass(el, klass); + } + else { + $.addClass(el, klass); + } +}; + +var UA = {}; + +UA.init = function() { + document.head = document.head || $.tag('head')[0]; + + this.hasContextMenu = 'HTMLMenuItemElement' in window; + + this.hasWebStorage = (function() { + var kv = 'catalog'; + try { + localStorage.setItem(kv, kv); + localStorage.removeItem(kv); + return true; + } catch (e) { + return false; + } + })(); + + this.hasSessionStorage = (function() { + var kv = 'catalog'; + try { + sessionStorage.setItem(kv, kv); + sessionStorage.removeItem(kv); + return true; + } catch (e) { + return false; + } + })(); + + this.hasCORS = 'withCredentials' in new XMLHttpRequest(); + + this.isMobileDevice = /Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent); +}; + +UA.dispatchEvent = function(name, detail) { + var e = document.createEvent('Event'); + e.initEvent(name, false, false); + if (detail) { + e.detail = detail; + } + document.dispatchEvent(e); +}; + +var FC = function() { + + var self = this, + + options = { + orderby: 'alt', + large: false, + extended: true, + imgdel: '//s.4cdn.org/image/filedeleted-res.gif', + imgspoiler: '//s.4cdn.org/image/spoiler', + nofile: '//s.4cdn.org/image/nofile.png', + smallsize: 150, + tipdelay: 250, + filterColors: [ + ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00'], + ['#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB'], + ['#00A550', '#007FFF', '#AF0A0F', '#B5BD68'] + ] + }, + + capcodeMap = { + admin: 'Administrator', + mod: 'Moderator', + developer: 'Developer', + manager: 'Manager', + founder: 'Founder', + verified: 'Verified' + }, + + keybinds = { + 83: focusQuickfilter, // S + 82: refreshWindow, // R + 88: cycleOrder // X + }, + + catalog = {}, + + basicSettings = [ 'orderby', 'large', 'extended' ], + + activeTheme = {}, + + activeStyleGroup, + activeStyleSheet, + + activeFilters = {}, + + hasTooltip = false, + tooltipTimeout = null, + + pinnedThreads = {}, + + hiddenThreads = {}, + hiddenThreadsCount = 0, + + filteredThreadsCount = 0, + + hasThreadWatcher = false, + hasDropDownNav = false, + hasClassicNav = false, + hasAutoHideNav = false, + altCaptcha = false, + + quickFilterPattern = false, + + hiddenMode = false, + + $threads, + $qfCtrl, + $teaserCtrl, + $sizeCtrl, + $orderCtrl, + $filterPalette, + + ctxCmds; + + if (window.devicePixelRatio >= 2) { + options.imgdel.replace('.', '@2x.'); + options.nofile.replace('.', '@2x.'); + } + + UA.init(); + + loadTheme(); + + self.init = function() { + var extConfig, el, val, fn; + + FC.hasMobileLayout = checkMobileLayout(); + + applyTheme(activeTheme, true); + + $threads = $.id('threads'); + $qfCtrl = $.id('qf-ctrl'); + $teaserCtrl = $.id('teaser-ctrl'); + $sizeCtrl = $.id('size-ctrl'); + $orderCtrl = $.id('order-ctrl'); + + $.on($qfCtrl, 'click', toggleQuickfilter); + $.on($.id('filters-clear-hidden'), 'click', toggleHiddenThreads); + $.on($.id('filters-clear-hidden-bottom'), 'click', toggleHiddenThreads); + $.on($.id('qf-clear'), 'click', toggleQuickfilter); + $.on($.id('settingsWindowLink'), 'click', showThemeEditor); + $.on($.id('settingsWindowLinkBot'), 'click', showThemeEditor); + $.on($.id('settingsWindowLinkMobile'), 'click', showThemeEditor); + $.on($.id('filters-ctrl'), 'click', showFilters); + $.on($teaserCtrl, 'change', onTeaserChange); + $.on($sizeCtrl, 'change', onSizeChange); + $.on($orderCtrl, 'change', onOrderChange); + $.on($threads, 'mouseover', onThreadMouseOver); + $.on($threads, 'mouseout', onThreadMouseOut); + $.on($.id('togglePostFormLinkMobile'), 'click', togglePostFormMobile); + $.on(document, 'click', onClick); + + loadSettings(); + + bindGlobalShortcuts(); + + initGlobalMessage(); + + if (UA.hasContextMenu) { + buildContextMenu(); + } + + window.Config = {}; + + if (UA.hasWebStorage) { + if (extConfig = localStorage.getItem('4chan-settings')) { + extConfig = JSON.parse(extConfig); + + window.Config = extConfig; + + if (!extConfig.disableAll) { + CustomMenu.initCtrl(extConfig.dropDownNav, extConfig.classicNav); + /* + if (location.host === 'boards.4channel.org' && extConfig.showNWSBoards) { + CustomMenu.showNWSBoards(); + } + */ + if (extConfig.filter) { + ThreadWatcher.hasFilters = true; + } + + if (extConfig.threadWatcher) { + hasThreadWatcher = true; + ThreadWatcher.init(); + } + + if (extConfig.customMenu) { + CustomMenu.apply(extConfig.customMenuList); + } + + if (extConfig.dropDownNav !== false && !FC.hasMobileLayout) { + hasDropDownNav = true; + hasClassicNav = extConfig.classicNav; + hasAutoHideNav = extConfig.autoHideNav; + showDropDownNav(); + } + + altCaptcha = extConfig.altCaptcha; + } + } + else if (UA.isMobileDevice && !FC.hasMobileLayout) { + hasDropDownNav = true; + showDropDownNav(); + } + else { + CustomMenu.initCtrl(false, false); + } + + if (window.css_event && activeStyleSheet === '_special') { + fn = window['fc_' + window.css_event + '_init']; + fn && fn(); + } + } + + if (el = document.forms.post.flag) { + if ((val = $.readCookie('4chan_flag')) && (el = el.querySelector('option[value="' + val + '"]'))) { + el.setAttribute('selected', 'selected'); + } + } + + setOrder(options.orderby, true); + setLarge(options.large, true); + setExtended(options.extended, true); + + UA.dispatchEvent('4chanMainInit'); + }; + + function showDropDownNav() { + var el, top, bottom; + + top = $.id('boardNavDesktop'); + bottom = $.id('boardNavDesktopFoot'); + + if (hasClassicNav) { + el = document.createElement('div'); + el.className = 'pageJump'; + el.innerHTML = '' + + 'Settings' + + 'Home'; + + top.appendChild(el); + + $.id('settingsWindowLinkClassic') + .addEventListener('click', showThemeEditor, false); + + $.addClass(top, 'persistentNav'); + } + else { + top.style.display = 'none'; + $.removeClass($.id('boardNavMobile'), 'mobile'); + } + + if (hasAutoHideNav) { + StickyNav.init(hasClassicNav); + } + + bottom.style.display = 'none'; + + $.addClass(document.body, 'hasDropDownNav'); + } + + function hideDropDownNav() { + var el, top, bottom; + + top = $.id('boardNavDesktop'); + bottom = $.id('boardNavDesktopFoot'); + + if (hasClassicNav) { + if (el = $.cls('pageJump', top)[0]) { + $.id('settingsWindowLinkClassic') + .removeEventListener('click', showThemeEditor, false); + top.removeChild(el); + } + + $.removeClass(top, 'persistentNav'); + } + else { + top.style.display = ''; + $.addClass($.id('boardNavMobile'), 'mobile'); + } + + if (hasAutoHideNav) { + StickyNav.destroy(hasClassicNav); + } + + bottom.style.display = ''; + + $.removeClass(document.body, 'hasDropDownNav'); + } + + self.loadCatalog = function(c) { + var query; + + catalog = c; + + $.addClass(document.body, activeStyleSheet.toLowerCase().replace(/ /g, '_')); + + initStyleSwitcher(); + loadFilters(); + loadStorage(); + + if (UA.hasSessionStorage && !location.hash && (query = sessionStorage.getItem('4chan-catalog-search'))) { + if (catalog.slug != sessionStorage.getItem('4chan-catalog-search-board')) { + sessionStorage.removeItem('4chan-catalog-search'); + sessionStorage.removeItem('4chan-catalog-search-board'); + query = null; + } + } + else if (location.hash && (query = location.hash.match(/#s=(.+)/))) { + query = decodeURIComponent(query[1].replace(/\+/g, ' ')); + } + + if (query) { + toggleQuickfilter(); + $.id('qf-box').value = query; + applyQuickfilter(); + } + else { + buildThreads(); + } + }; + + function initGlobalMessage() { + var msg, btn, thisTs, oldTs; + + if (!UA.hasWebStorage || FC.hasMobileLayout) { + return; + } + + if ((msg = $.id('globalMessage')) && msg.textContent) { + msg.nextElementSibling.style.clear = 'both'; + + btn = document.createElement('span'); + btn.id = 'toggleMsgBtn'; + btn.setAttribute('data-cmd', 'toggleMsg'); + btn.title = 'Toggle announcement'; + + oldTs = localStorage.getItem('4chan-global-msg'); + thisTs = msg.getAttribute('data-utc'); + + if (oldTs && thisTs <= oldTs) { + msg.style.display = 'none'; + btn.style.opacity = '0.5'; + btn.className = 'expandIcon'; + } + else { + btn.className = 'collapseIcon'; + } + + $.on(btn, 'click', toggleGlobalCatalogMessage); + msg.parentNode.insertBefore(btn, msg); + } + } + + function toggleGlobalCatalogMessage() { + var msg, btn; + + msg = $.id('globalMessage'); + btn = $.id('toggleMsgBtn'); + if (msg.style.display == 'none') { + msg.style.display = ''; + btn.className = 'collapseIcon'; + btn.style.opacity = '1'; + localStorage.removeItem('4chan-global-msg'); + } + else { + msg.style.display = 'none'; + btn.className = 'expandIcon'; + btn.innerHTML = 'View Important Announcement'; + btn.style.opacity = '0.5'; + localStorage.setItem('4chan-global-msg', msg.getAttribute('data-utc')); + } + + //StorageSync.sync('4chan-global-msg'); + } + + function togglePostFormMobile() { + var el = document.getElementById('postForm'); + + if (el.style.display == 'table') { + el.style.display = ''; + this.textContent = 'Start a New Thread'; + } + else { + el.style.display = 'table'; + this.textContent = 'Close Post Form'; + window.initRecaptcha(); + window.initTCaptcha(); + } + } + + function getRegexSpecials() { + var specials = ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\' ]; + return new RegExp('(\\' + specials.join('|\\') + ')', 'g'); + } + + function getThreadPage(tid) { + return (0 | (catalog.threads[tid].b / catalog.pagesize)) + 1; + } + + function initStyleSwitcher() { + var i, selector, nodes, ss; + + selector = $.id('styleSelector'); + nodes = selector.children; + + for (i = 0; ss = nodes[i]; ++i) { + if (ss.value == activeStyleSheet) { + selector.selectedIndex = i; + } + } + + $.on(selector, 'change', onStyleSheetChange); + } + + function onStyleSheetChange() { + var expires; + + if (this.value !== '_special') { + expires = new Date(); + + expires.setTime(expires.getTime() + 31536000000); + + document.cookie = activeStyleGroup + '=' + this.value + '; expires=' + + expires.toGMTString() + '; path=/; domain=' + $L.d(catalog.slug); + + if (window.css_event) { + fn = window['fc_' + window.css_event + '_cleanup']; + localStorage.setItem('4chan_stop_css_event', `${window.css_event}-${window.css_event_v}`); + } + } + else if (window.css_event) { + fn = window['fc_' + window.css_event + '_init']; + localStorage.removeItem('4chan_stop_css_event'); + } + + //StorageSync.sync('4chan_stop_css_event'); + + refreshWindow(); + } + + function refreshWindow(e) { + if (e && e.shiftKey) { + return; + } + location.href = location.href; + } + + function debounce(delay, fn) { + var timeout; + + return function() { + var args = arguments, context = this; + + clearTimeout(timeout); + timeout = setTimeout(function () { + fn.apply(context, args); + }, delay); + }; + } + + function focusQuickfilter() { + if ($.hasClass($qfCtrl, 'active')) { + clearQuickfilter(true); + } + else { + toggleQuickfilter(); + } + } + + function toggleQuickfilter() { + var qfBox, qfcnt = $.id('qf-cnt'); + if ($.hasClass($qfCtrl, 'active')) { + clearQuickfilter(); + qfcnt.style.display = 'none'; + $.removeClass($qfCtrl, 'active'); + } + else { + qfcnt.style.display = 'inline'; + qfBox = $.id('qf-box'); + if (!qfcnt.hasAttribute('data-built')) { + qfcnt.setAttribute('data-built', '1'); + $.on(qfBox, 'keyup', debounce(250, applyQuickfilter)); + $.on(qfBox, 'keydown', function(e) { + if (e.keyCode == '27') { + toggleQuickfilter(); + } + }); + } + qfBox.focus(); + qfBox.value = ''; + $.addClass($qfCtrl, 'active'); + } + } + + function applyQuickfilter() { + var regexEscape, qfstr; + + if ((qfstr = $.id('qf-box').value) !== '') { + if (UA.hasSessionStorage) { + sessionStorage.setItem('4chan-catalog-search', qfstr); + sessionStorage.setItem('4chan-catalog-search-board', catalog.slug); + } + regexEscape = getRegexSpecials(); + $.id('search-term').textContent = $.id('search-term-bottom').textContent = qfstr; + $.id('search-label').style.display = $.id('search-label-bottom').style.display = 'inline'; + qfstr = qfstr.replace(regexEscape, '\\$1'); + quickFilterPattern = new RegExp(qfstr, 'i'); + buildThreads(); + } else { + clearQuickfilter(); + } + } + + function clearQuickfilter(focus) { + var qf = $.id('qf-box'); + $.id('search-label').style.display = $.id('search-label-bottom').style.display = 'none'; + if (focus) { + qf.value = ''; + qf.focus(); + } + else { + if (UA.hasSessionStorage) { + sessionStorage.removeItem('4chan-catalog-search'); + } + quickFilterPattern = false; + buildThreads(); + } + } + + function buildContextMenu() { + ctxCmds = { + pin: toggleThreadPin, + hide: toggleThreadHide, + report: reportThread + }; + + $.id('ctxmenu-main').innerHTML = + ''; + + $.id('ctxmenu-thread').innerHTML = + '' + + '' + + ''; + + $.on($.id('ctxmenu-main'), 'click', clearPinnedThreads); + $.on($.id('ctxmenu-thread'), 'click', onThreadContextClick); + } + + function bindGlobalShortcuts() { + var el, tid; + if (UA.hasWebStorage) { + $.on($threads, 'mousedown', function(e) { + el = e.target; + if (el.className.indexOf('thumb') != -1) { + tid = el.getAttribute('data-id'); + if (e.which == 3) { + $threads.setAttribute('contextmenu', 'ctxmenu-thread'); + $.id('ctxmenu-thread').target = tid; + } + else if (e.which == 1 && e.altKey) { + toggleThreadPin(tid); + return false; + } + else if (e.which == 1 && e.shiftKey) { + toggleThreadHide(tid); + return false; + } + } + else if (e.which == 3) { + $threads.setAttribute('contextmenu', 'ctxmenu-main'); + } + }); + } + if (!activeTheme.nobinds) { + $.on(document, 'keyup', processKeybind); + } + } + + function toggleThreadPin(tid) { + if (pinnedThreads[tid] >= 0) { + delete pinnedThreads[tid]; + } + else { + pinnedThreads[tid] = catalog.threads[tid].r || 0; + } + localStorage.setItem('4chan-pin-' + catalog.slug, JSON.stringify(pinnedThreads)); + buildThreads(); + } + + function toggleThreadHide(tid) { + if (hiddenMode) { + delete hiddenThreads[tid]; + --hiddenThreadsCount; + } + else { + hiddenThreads[tid] = true; + ++hiddenThreadsCount; + } + + localStorage.setItem('4chan-hide-t-' + catalog.slug, JSON.stringify(hiddenThreads)); + + $.id('thread-' + tid).style.display = 'none'; + + setHiddenCount(hiddenThreadsCount); + + if (hiddenThreadsCount === 0) { + setHiddenMode(false); + } + } + + function setHiddenMode(state) { + hiddenMode = state; + + $.id('filters-clear-hidden').textContent = + $.id('filters-clear-hidden-bottom').textContent = state ? 'Back' : 'Show'; + + buildThreads(); + } + + function setProcessedCount(type, num) { + var label = type + '-label', count = type + '-count'; + + if (num > 0) { + $.id(count).textContent = $.id(count + '-bottom').textContent = num; + $.id(label).style.display = $.id(label + '-bottom').style.display = 'inline'; + } + else { + $.id(label).style.display = $.id(label + '-bottom').style.display = 'none'; + } + } + + function setHiddenCount(num) { + setProcessedCount('hidden', num); + } + + function setFilteredCount(num) { + setProcessedCount('filtered', num); + } + + function reportThread(tid) { + var height, altc; + + if (window.passEnabled || !window.grecaptcha) { + height = 175; + } + else if (altCaptcha) { + height = 320; + altc = '&altc=1'; + } + else { + height = 510; + altc = ''; + } + + window.open( + 'https://sys.' + $L.d(catalog.slug) + '/' + catalog.slug + + '/imgboard.php?mode=report&no=' + tid + altc, + Date.now(), + 'toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height=' + height + ); + } + + function onThreadContextClick(e) { + var cmd = e.target.getAttribute('data-cmd'); + ctxCmds[cmd]($.id('ctxmenu-thread').target); + } + + function processKeybind(e) { + var el = e.target; + if (el.nodeName == 'TEXTAREA' || el.nodeName == 'INPUT') { + return; + } + if (keybinds[e.keyCode]) { + keybinds[e.keyCode](e); + } + } + + function toggleHiddenThreads(e) { + var el; + + e.preventDefault(); + + if (hiddenThreadsCount > 0) { + if ((el = $.id('filters-clear-hidden')).textContent == 'Show') { + setHiddenMode(true); + } + else { + setHiddenMode(false); + } + } + } + + function clearPinnedThreads() { + pinnedThreads = {}; + localStorage.removeItem('4chan-pin-' + catalog.slug); + buildThreads(); + return false; + } + + function onClick(e) { + var t = e.target, tid; + + if ((t = e.target) == document) { + return; + } + + if (tid = t.getAttribute('data-watch')) { + ThreadWatcher.toggle( + tid, + catalog.slug, + catalog.threads[tid].sub, + catalog.threads[tid].teaser, + catalog.threads[tid].lr.id + ); + } + else if (tid = t.getAttribute('data-hide')) { + e.preventDefault(); + toggleThreadHide(tid); + } + else if (tid = t.getAttribute('data-pin')) { + e.preventDefault(); + toggleThreadPin(tid); + } + else if (tid = t.getAttribute('data-report')) { + e.preventDefault(); + reportThread(tid); + } + else if (tid = t.getAttribute('data-post-menu')) { + e.preventDefault(); + PostMenu.open(t, tid, hasThreadWatcher, hiddenThreads[tid], pinnedThreads[tid]); + } + else if (t.hasAttribute('data-cm-edit')) { + e.preventDefault(); + CustomMenu.showEditor(true); + } + else if (t.id == 'backdrop') { + if (!panelHidden($.id('filters'))) { + if (!panelHidden($.id('filters-protip'))) { + closeFiltersHelp(); + } + else { + closeFilters(); + } + } + else if (!panelHidden($.id('theme'))) { + closeThemeEditor(); + } + } + else if (e.target.id == 'filter-palette') { + closeFilterPalette(); + } + } + + function buildFilterPalette() { + var i, j, table, palette, rows, cols, tr, td, foot; + + $filterPalette = $.id('filter-palette'); + + table = $.id('filter-color-table'); + palette = $.tag('tbody', table)[0]; + rows = options.filterColors.length; + + if (rows > 0) { + cols = options.filterColors[0].length; + foot = $.tag('tfoot', table)[0]; + for (i = foot.children.length - 1; i >= 0; i--) { + foot.children[i].firstElementChild.setAttribute('colspan', cols); + } + } + for (i = 0; i < rows; ++i) { + tr = document.createElement('tr'); + for (j = 0; j < cols; ++j) { + td = document.createElement('td'); + td.innerHTML = ''; + $.on(td.firstElementChild, 'click', selectFilterColor); + tr.appendChild(td); + } + palette.appendChild(tr); + } + } + + function showFilterPalette(el) { + var picker, pos = el.getBoundingClientRect(); + + if (!$filterPalette) { + buildFilterPalette(); + } + + $.removeClass($filterPalette, 'hidden'); + $filterPalette.setAttribute('data-target', el.id.split('-')[2]); + + picker = $filterPalette.firstElementChild; + picker.style.cssText = 'top:' + pos.top + 'px;left:' + + (pos.left - picker.clientWidth - 10) + 'px;'; + } + + function showFiltersHelp() { + var el = $.id('filters-protip'); + el.style.top = window.pageYOffset + 50 + 'px'; + $.removeClass(el, 'hidden'); + } + + function closeFiltersHelp() { + $.addClass($.id('filters-protip'), 'hidden'); + } + + function onFiltersClick(e) { + var t = e.target; + + if (t.id == 'filters-close') + closeFilters(); + else if (t.id == 'filters-add') + addEmptyFilter(); + else if (t.id == 'filters-save') { + saveFilters(); + closeFilters(); + } + else if (t.hasAttribute('data-active')) + toggleFilter(t, 'active'); + else if (t.hasAttribute('data-hide')) + toggleFilter(t, 'hide', 'top'); + else if (t.hasAttribute('data-top')) + toggleFilter(t, 'top', 'hide'); + else if ($.hasClass(t, 'filter-color')) + showFilterPalette(t); + else if (t.hasAttribute('data-target')) + deleteFilter(t); + else if (t.hasAttribute('data-up')) + moveFilterUp(t); + } + + function moveFilterUp(el) { + var tr, prev; + + tr = el.parentNode.parentNode; + prev = tr.previousElementSibling; + + if (prev) { + tr.parentNode.insertBefore(tr, prev); + } + } + + function onFiltersSearch(e) { + var i, el, nodes, cnt, str; + + if (e && (e.keyCode == 27)) { + this.value = ''; + } + + str = this.value.toLowerCase(); + + nodes = document.getElementsByClassName('filter-pattern'); + + cnt = document.getElementById('filter-list'); + + cnt.style.display = 'none'; + + for (i = 0; el = nodes[i]; ++i) { + if (el.value.toLowerCase().indexOf(str) === -1) { + el.parentNode.parentNode.style.display = 'none'; + } + else { + el.parentNode.parentNode.style.display = ''; + } + } + + cnt.style.display = ''; + } + + function showFilters() { + var i, filtersPanel, rawFilters, filterList, filterId, el; + + filtersPanel = $.id('filters'); + + if (!filtersPanel) { + filtersPanel = FC.panelHTML.build('filters', 'panel hidden'); + FC.panelHTML.build('filters-protip', 'panel hidden'); + FC.panelHTML.build('filter-palette', 'hidden'); + } + + if (!filtersPanel.hasAttribute('data-built')) { + $.on(filtersPanel, 'click', onFiltersClick); + + $.on($.id('filter-palette-close'), 'click', closeFilterPalette); + $.on($.id('filter-palette-clear'), 'click', clearFilterColor); + + $.on($.id('filters-help-open'), 'click', showFiltersHelp); + $.on($.id('filters-help-close'), 'click', closeFiltersHelp); + + $.on($.id('filter-rgb'), 'keyup', filterSetCustomColor); + $.on($.id('filter-rgb-ok'), 'click', selectFilterColor); + + $.on($.id('filters-search'), 'keyup', onFiltersSearch); + + filtersPanel.setAttribute('data-built', '1'); + } + else { + $.id('filters-search').value = ''; + } + + rawFilters = localStorage.getItem('catalog-filters'); + filterId = 0; + + if (rawFilters) { + filterList = $.id('filter-list'); + rawFilters = JSON.parse(rawFilters); + for (i in rawFilters) { + filterList.appendChild(buildFilter(rawFilters[i], filterId)); + ++filterId; + } + updateFilterHitCount(); + } + + filtersPanel.style.top = window.pageYOffset + 60 + 'px'; + + $.removeClass(filtersPanel, 'hidden'); + + if (el = $.cls('filter-active', filtersPanel)[0]) { + el.focus(); + } + + toggleBackdrop(); + } + + function closeFilters() { + var i, filterList, nodes; + + $.id('filters-msg').style.display = 'none'; + $.addClass($.id('filters'), 'hidden'); + + filterList = $.id('filter-list'); + nodes = $.tag('tr', filterList); + for (i = nodes.length - 1; i >= 0; i--) { + filterList.removeChild(nodes[i]); + } + + closeFilterPalette(); + toggleBackdrop(); + } + + function closeFilterPalette() { + if ($filterPalette && !$.hasClass($filterPalette, 'hidden')) { + $.addClass($filterPalette, 'hidden'); + } + } + + // Loads patterns from the localStorage and builds regexps + function loadFilters() { + if (!UA.hasWebStorage) return; + + activeFilters = {}; + + var rawFilters = localStorage.getItem('catalog-filters'); + if (!rawFilters) return; + + rawFilters = JSON.parse(rawFilters); + + var + rf, fid, v, w, wordcount, + wordSepS, wordSepE, + regexType = /^\/(.*)\/(i?)$/, + regexOrNorm = /\s*\|+\s*/g, + regexWc = /\\\*/g, replWc = '[^\\s]*', + regexEscape = getRegexSpecials(), + match, inner, words, rawPattern, pattern, orOp, orCluster, type; + + wordSepS = '(?=.*\\b'; + wordSepE = '\\b)'; + + try { + for (fid in rawFilters) { + rf = rawFilters[fid]; + if (rf.active && rf.pattern !== '') { + if (rf.boards && rf.boards.split(' ').indexOf(catalog.slug) == -1) { + continue; + } + rawPattern = rf.pattern; + if (rawPattern.charAt(0) == '#') { + type = (rawPattern.charAt(1) == '#') ? 2 : 1; + pattern = new RegExp(rawPattern.slice(type).replace(regexEscape, '\\$1')); + } + else { + type = 0; + if (match = rawPattern.match(regexType)) { + pattern = new RegExp(match[1], match[2]); + } + else if (rawPattern.charAt(0) == '"' && rawPattern.charAt(rawPattern.length - 1) == '"') { + pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); + } + else { + words = rawPattern.replace(regexOrNorm, '|').split(' '); + pattern = ''; + wordcount = words.length; + for (w = 0; w < wordcount; ++w) { + if (words[w].indexOf('|') != -1) { + orOp = words[w].split('|'); + orCluster = []; + for (v = orOp.length - 1; v >= 0; v--) { + if (orOp[v] !== '') { + orCluster.push(orOp[v].replace(regexEscape, '\\$1')); + } + } + inner = orCluster.join('|').replace(regexWc, replWc); + pattern += wordSepS + '(' + inner + ')' + wordSepE; + } + else { + inner = words[w].replace(regexEscape, '\\$1').replace(regexWc, replWc); + pattern += wordSepS + inner + wordSepE; + } + } + pattern = new RegExp('^' + pattern, 'i'); + } + } + //console.log('Resulting regex: ' + pattern); + activeFilters[fid] = { + type: type, + pattern: pattern, + boards: rf.boards, + fid: fid, + hidden: rf.hidden, + color: rf.color, + top: rf.top, + hits: 0 + }; + } + } + } + catch (err) { + alert('There was an error processing one of the filters: ' + + err + ' in: ' + rf.pattern); + } + } + + function saveFilters() { + var i, j, f, rawFilters, filterList, msg, rows, color; + + rawFilters = {}; + filterList = $.id('filter-list'); + rows = filterList.children; + + for (i = 0; j = rows[i]; ++i) { + f = { + active: $.cls('filter-active', j)[0].checked ? 1 : 0, + pattern: $.cls('filter-pattern', j)[0].value, + boards: $.cls('filter-boards', j)[0].value, + hidden: $.cls('filter-hide', j)[0].checked ? 1 : 0, + top: $.cls('filter-top', j)[0].checked ? 1 : 0 + }; + color = $.cls('filter-color', j)[0]; + if (!color.hasAttribute('data-nocolor')) { + f.color = color.style.backgroundColor; + } + rawFilters[i] = f; + } + + if (rawFilters[0]) { + localStorage.setItem('catalog-filters', JSON.stringify(rawFilters)); + } + else { + localStorage.removeItem('catalog-filters'); + } + + msg = $.id('filters-msg'); + msg.innerHTML = 'Done'; + msg.className = 'msg-ok'; + msg.style.display = 'inline'; + setTimeout(function() { msg.style.display = 'none'; }, 2000); + + loadFilters(); + buildThreads(); + updateFilterHitCount(); + } + + function filterSetCustomColor() { + var filterRgbOk; + + filterRgbOk = $.id('filter-rgb-ok'); + + filterRgbOk.style.backgroundColor = this.value; + } + + function buildFilter(filter, id) { + var td, tr, span, input; + + tr = document.createElement('tr'); + tr.id = 'filter-' + id; + + // Move up + td = document.createElement('td'); + span = document.createElement('span'); + span.setAttribute('data-up', id); + span.className = 'pointer'; + span.innerHTML = '↑'; + td.appendChild(span); + tr.appendChild(td); + + // On + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = !!filter.active; + input.className = 'filter-active'; + td.appendChild(input); + tr.appendChild(td); + + // Pattern + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'text'; + input.value = filter.pattern; + input.className = 'filter-pattern'; + td.appendChild(input); + tr.appendChild(td); + + // Boards + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'text'; + input.value = filter.boards; + input.className = 'filter-boards'; + td.appendChild(input); + tr.appendChild(td); + + // Color + td = document.createElement('td'); + span = document.createElement('span'); + span.id = 'filter-color-' + id; + span.title = 'Change Color'; + span.className = 'button clickbox filter-color'; + if (!filter.color) { + span.setAttribute('data-nocolor', '1'); + span.innerHTML = '∕'; + } + else { + span.style.background = filter.color; + } + td.appendChild(span); + tr.appendChild(td); + + // Hide + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = !!filter.hidden; + input.className = 'filter-hide'; + td.appendChild(input); + tr.appendChild(td); + + // Top + td = document.createElement('td'); + input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = !!filter.top; + input.className = 'filter-top'; + td.appendChild(input); + tr.appendChild(td); + + // Del + td = document.createElement('td'); + span = document.createElement('span'); + span.setAttribute('data-target', id); + span.className = 'pointer'; + span.innerHTML = '×'; + td.appendChild(span); + tr.appendChild(td); + + // Match count + td = document.createElement('td'); + td.id = 'fhc-' + id; + td.className = 'filter-hits'; + tr.appendChild(td); + + return tr; + } + + function selectFilterColor(clear) { + var target = $.id('filter-color-' + $filterPalette.getAttribute('data-target')); + if (clear === true) { + target.setAttribute('data-nocolor', '1'); + target.innerHTML = '∕'; + target.style.background = ''; + } + else { + target.removeAttribute('data-nocolor'); + target.innerHTML = ''; + target.style.background = this.style.backgroundColor; + } + closeFilterPalette(); + } + + function clearFilterColor() { + selectFilterColor(true); + } + + function addEmptyFilter() { + var filter = { + active: 1, + pattern: '', + boards: '', + color: '', + hidden: 0, + top: 0, + hits: 0 + }; + $.id('filter-list').appendChild(buildFilter(filter, getNextFilterId())); + } + + function getNextFilterId() { + var i, j, max, rows = $.id('filter-list').children; + + if (!rows.length) { + return 0; + } + else { + max = 0; + for (i = 0; j = rows[i]; ++i) { + j = +j.id.slice(7); + if (j > max) { + max = j; + } + } + return max + 1; + } + } + + function deleteFilter(t) { + var el = $.id('filter-' + t.getAttribute('data-target')); + el.parentNode.removeChild(el); + } + + function toggleFilter(el, type, xor) { + var attr = 'data-' + type, xorEle; + + if (el.getAttribute(attr) == '0') { + el.setAttribute(attr, '1'); + $.addClass(el, 'active'); + el.innerHTML = '✔'; + if (xor) { + xorEle = $.cls('filter-' + xor, el.parentNode.parentNode)[0]; + xorEle.setAttribute('data-' + xor, '0'); + $.removeClass(xorEle, 'active'); + xorEle.innerHTML = ''; + } + } + else { + el.setAttribute(attr, '0'); + $.removeClass(el, 'active'); + el.innerHTML = ''; + } + } + + function updateFilterHitCount() { + var i, j, rows = $.id('filter-list').children; + for (i = 0; j = rows[i]; ++i) { + $.id('fhc-' + j.id.slice(7)) + .innerHTML = activeFilters[i] ? 'x' + activeFilters[i].hits : ''; + } + } + + function panelHidden(el) { + return el && $.hasClass(el, 'hidden'); + } + + function showThemeEditor() { + var themePanel, el, theme; + + if (!UA.hasWebStorage) { + alert("Your browser doesn't support Local Storage"); + return; + } + + themePanel = $.id('theme'); + + if (!themePanel) { + themePanel = FC.panelHTML.build('theme', 'panel hidden'); + } + + theme = localStorage.getItem('catalog-theme'); + theme = theme ? JSON.parse(theme) : {}; + + $.id('theme-nobinds').checked = !!theme.nobinds; + $.id('theme-nospoiler').checked = !!theme.nospoiler; + $.id('theme-newtab').checked = !!theme.newtab; + $.id('theme-tw').checked = hasThreadWatcher; + $.id('theme-ddn').checked = hasDropDownNav; + + if (theme.css) { + $.id('theme-css').value = theme.css; + } + + $.on($.id('theme-save'), 'click', saveTheme); + $.on($.id('theme-close'), 'click', closeThemeEditor); + + $.id('theme-msg').style.display = 'none'; + + themePanel.style.top = window.pageYOffset + 60 + 'px'; + $.removeClass(themePanel, 'hidden'); + + if (el = $.tag('input', themePanel)[0]) { + el.focus(); + } + + toggleBackdrop(); + + document.dispatchEvent(new CustomEvent('4chanCatalogThemeEditorReady')); + } + + function closeThemeEditor() { + $.off($.id('theme-save'), 'click', saveTheme); + $.off($.id('theme-close'), 'click', closeThemeEditor); + + $.addClass($.id('theme'), 'hidden'); + toggleBackdrop(); + } + + function toggleBackdrop() { + $.toggleClass($.id('backdrop'), 'hidden'); + } + + function loadTheme() { + var customTheme; + + if (UA.hasWebStorage && (customTheme = localStorage.getItem('catalog-theme'))) { + activeTheme = JSON.parse(customTheme); + } + } + + function applyTheme(customTheme, nocss) { + if (customTheme.nobinds) { + if (activeTheme.nobinds != customTheme.nobinds) { + $.off(document, 'keyup', processKeybind); + } + } + else { + if (activeTheme.nobinds != customTheme.nobinds) { + $.on(document, 'keyup', processKeybind); + } + } + + if (customTheme.nospoiler) { + $.addClass(document.body, 'reveal-img-spoilers'); + } + else { + $.removeClass(document.body, 'reveal-img-spoilers'); + } + + if (!nocss) { + self.applyCSS(customTheme); + } + + document.dispatchEvent(new CustomEvent('4chanCatalogThemeApplied')); + } + + self.applyCSS = function(customTheme, style_group, css_version) { + var style, ss; + + if (!customTheme) { + customTheme = activeTheme; + } + + // Preferred stylesheet + if (style_group !== undefined) { + if (!(ss = $.readCookie(style_group))) { + ss = style_group == 'nws_style' ? 'Yotsuba New' : 'Yotsuba B New'; + } + + activeStyleGroup = style_group; + + if (window.css_event && localStorage.getItem('4chan_stop_css_event') !== `${window.css_event}-${window.css_event_v}`) { + activeStyleSheet = '_special' + ss = window.css_event; + } + else { + activeStyleSheet = ss; + } + + style = document.createElement('link'); + style.type = 'text/css'; + style.id = 'base-css'; + style.rel = 'stylesheet'; + style.setAttribute('href', '//s.4cdn.org/css/catalog_' + + ss.toLowerCase().replace(/ /g, '_') + '.' + css_version + '.css'); + document.head.insertBefore(style, $.id('mobile-css')); + } + + // Custom CSS + if (style = $.id('custom-css')) { + document.head.removeChild(style); + } + + if (customTheme.css) { + style = document.createElement('style'); + style.type = 'text/css'; + style.id = 'custom-css'; + + if (style.styleSheet) { + style.styleSheet.cssText = customTheme.css; + } + else { + style.innerHTML = customTheme.css; + } + document.head.appendChild(style); + } + }; + + // Applies and saves the theme to localStorage + function saveTheme() { + var i, css, tw, ddn, extConfig, customTheme = {}; + + if ($.id('theme-nobinds').checked) { + customTheme.nobinds = true; + } + + if ($.id('theme-nospoiler').checked) { + customTheme.nospoiler = true; + } + + if ($.id('theme-newtab').checked) { + customTheme.newtab = true; + } + + tw = $.id('theme-tw').checked; + + ddn = $.id('theme-ddn').checked; + + if (extConfig = localStorage.getItem('4chan-settings')) { + extConfig = JSON.parse(extConfig); + } + else { + extConfig = {}; + } + + if (tw != hasThreadWatcher) { + if (tw) { + ThreadWatcher.init(); + extConfig.disableAll = false; + } + else { + ThreadWatcher.unInit(); + } + } + + if (ddn != hasDropDownNav) { + if (ddn) { + showDropDownNav(); + extConfig.disableAll = false; + } + else { + hideDropDownNav(); + } + } + + extConfig.threadWatcher = tw; + extConfig.dropDownNav = ddn; + localStorage.setItem('4chan-settings', JSON.stringify(extConfig)); + //StorageSync.sync('4chan-settings'); + + hasThreadWatcher = tw; + hasDropDownNav = ddn; + + if ((css = $.id('theme-css').value) !== '') { + customTheme.css = css; + } + + applyTheme(customTheme); + + localStorage.removeItem('catalog-theme'); + + for (i in customTheme) { + localStorage.setItem('catalog-theme', JSON.stringify(customTheme)); + break; + } + + //StorageSync.sync('catalog-theme'); + + activeTheme = customTheme; + + buildThreads(); + closeThemeEditor(); + } + + function loadThreadList(key) { + var i, threads, mod = false, ft = 0; + + if (threads = localStorage.getItem(key)) { + ft = +Object.keys(catalog.threads).pop(); + threads = JSON.parse(threads); + for (i in threads) { + if (!catalog.threads[i] && i < ft) { + delete threads[i]; + mod = true; + } + } + for (i in threads) { + if (mod) { localStorage.setItem(key, JSON.stringify(threads)); } + return threads; + } + localStorage.removeItem(key); + } + return {}; + } + + function loadStorage() { + if (UA.hasWebStorage) { + hiddenThreads = loadThreadList('4chan-hide-t-' + catalog.slug); + pinnedThreads = loadThreadList('4chan-pin-' + catalog.slug); + } + } + + function loadSettings() { + var settings; + if (UA.hasWebStorage && (settings = localStorage.getItem('catalog-settings'))) { + $.extend(options, JSON.parse(settings)); + } + } + + function saveSettings() { + var i, key, settings; + if (!UA.hasWebStorage) { + return; + } + settings = {}; + for (i = basicSettings.length - 1; i >= 0; i--) { + key = basicSettings[i]; + settings[key] = options[key]; + } + localStorage.setItem('catalog-settings', JSON.stringify(settings)); + //StorageSync.sync('catalog-settings'); + } + + function setExtended(mode, init) { + var cls = ''; + if (mode) { + $teaserCtrl.selectedIndex = 1; + cls = 'extended-'; + options.extended = true; + } + else { + $teaserCtrl.selectedIndex = 0; + options.extended = false; + } + if (options.large) { + cls += 'large'; + } + else { + cls += 'small'; + } + $threads.className = cls; + if (!init) { + saveSettings(); + } + } + + function setLarge(mode, init) { + var cls = options.extended ? 'extended-' : ''; + if (mode) { + $sizeCtrl.selectedIndex = 1; + cls += 'large'; + options.large = true; + } + else { + $sizeCtrl.selectedIndex = 0; + cls += 'small'; + options.large = false; + } + $threads.className = cls; + if (!init) { + saveSettings(); + buildThreads(); + } + } + + function setOrder(order, init) { + var o = { alt: 0, absdate: 1, date: 2, r: 3 }; + if (o[order] !== undefined) { + $orderCtrl.selectedIndex = o[order]; + options.orderby = order; + } + else { + $orderCtrl.selectedIndex = 0; + options.orderby = 'date'; + } + if (!init) { + saveSettings(); + buildThreads(); + } + } + + function onTeaserChange() { + setExtended($teaserCtrl.options[$teaserCtrl.selectedIndex].value == 'on'); + } + + function onOrderChange() { + setOrder($orderCtrl.options[$orderCtrl.selectedIndex].value); + } + + function onSizeChange() { + setLarge($sizeCtrl.options[$sizeCtrl.selectedIndex].value == 'large'); + } + + function cycleOrder() { + if (options.orderby == 'date') { + setOrder('alt'); + } + else if (options.orderby == 'alt') { + setOrder('r'); + } + else if (options.orderby == 'r') { + setOrder('absdate'); + } + else { + setOrder('date'); + } + } + + function sortThreadList(threadList) { + var order = options.orderby; + + if (order == 'date') { + threadList.sort(function(a, b) { + if (a.id > b.id) return -1; + if (a.id < b.id) return 1; + return 0; + }); + } + else if (order == 'absdate' && !catalog.no_lr) { + threadList.sort(function(a, b) { + a = a.entry.lr.id; + b = b.entry.lr.id; + if (a > b) return -1; + if (a < b) return 1; + return 0; + }); + } + else if (order == 'r') { + threadList.sort(function(a, b) { + var + a = a.entry.r || 0, + b = b.entry.r || 0; + if (a > b) return -1; + if (a < b) return 1; + return 0; + }); + } + else { // alt + threadList.sort(function(a, b) { + if (a.entry.b < b.entry.b) return -1; + if (a.entry.b > b.entry.b) return 1; + return 0; + }); + } + } + + function getFilteredThreads() { + var i, id, entry, hl, onTop, pinned, teaser, tripcode, af, threads, fid, + filtered; + + filtered = 0; + + threads = []; + + threadloop: for (id in catalog.threads) { + id = +id; + entry = catalog.threads[id]; + hl = onTop = pinned = false; + + if (entry.sub) { + teaser = '' + entry.sub + ''; + if (entry.teaser) { + teaser += ': ' + entry.teaser; + } + } + else { + teaser = entry.teaser; + } + + if (hiddenMode) { + if (!hiddenThreads[id]) { + continue; + } + ++hiddenThreadsCount; + } + else if(!quickFilterPattern) { + if (hiddenThreads[id]) { + ++hiddenThreadsCount; + continue; + } + if (pinnedThreads[id] >= 0) { + pinned = onTop = true; + } + else { + if (entry.capcode) { + tripcode = (entry.trip || '') + '!#' + entry.capcode; + } + else { + tripcode = entry.trip; + } + for (fid in activeFilters) { + af = activeFilters[fid]; + if ((af.type == 0 && (af.pattern.test(teaser) || af.pattern.test(entry.file))) + || (af.type == 1 && af.pattern.test(tripcode)) + || (af.type == 2 && af.pattern.test(entry.author))) { + if (af.hidden) { + ++filtered; + af.hits += 1; + continue threadloop; + } + hl = af; + onTop = !!af.top; + af.hits += 1; + break; + } + } + } + } + else if (!quickFilterPattern.test(teaser) && !quickFilterPattern.test(entry.file)) { + continue; + } + + if (pinnedThreads[id] >= 0) { + pinned = onTop = true; + } + + threads.push( + { + id: id, + entry: entry, + pinned: pinned, + onTop: onTop, + hl: hl + } + ); + } + + filteredThreadsCount = filtered; + + return threads; + } + + function formatImageThreads(threads) { + var + i, k, id, entry, item, thread, hl, onTop, pinned, spoiler, + rDiff, html, provider, contentUrl, + pinhl, newtab, watchKey, teaser, topHtml, stickyHtml, + ratio, maxSize, imgWidth, imgHeight, calcSize, + capcodeReplies, capcodeReply, capcodeTitle, page; + + provider = '//boards.' + $L.d(catalog.slug) + '/' + catalog.slug + '/thread/'; + contentUrl = 'i.4cdn.org/' + catalog.slug + '/'; + + calcSize = !options.large; + newtab = activeTheme.newtab ? 'target="_blank" ' : ''; + + if (catalog.custom_spoiler) { + spoiler = options.imgspoiler + '-' + catalog.slug + catalog.custom_spoiler + '.png'; + } + else { + spoiler = options.imgspoiler + '.png'; + } + + html = ''; + topHtml = ''; + stickyHtml = ''; + + for (i = 0; item = threads[i]; ++i) { + id = item.id; + entry = item.entry; + hl = item.hl; + onTop = item.onTop; + pinned = item.pinned; + + if (entry.sub) { + teaser = '' + entry.sub + ''; + if (entry.teaser) { + teaser += ': ' + entry.teaser; + } + } + else { + teaser = entry.teaser; + } + + thread = '
    '; + + if (hasThreadWatcher) { + watchKey = id + '-' + catalog.slug; + thread += '' : + 'title="Watch" class="watchIcon">'); + } + + thread += ''; + + if (entry.sticky || entry.closed || entry.capcodereps) { + thread += '
    '; + if (entry.sticky) { + thread += ''; + } + if (entry.closed) { + thread += ''; + } + if (entry.capcodereps) { + capcodeReplies = entry.capcodereps.split(','); + for (k = 0; capcodeReply = capcodeReplies[k]; ++k) { + if (capcodeTitle = capcodeMap[capcodeReply]) { + thread += ''; + } + } + } + thread += '
    '; + } + + thread += '
    '; + + if (entry.bumplimit) { + thread += 'R: ' + entry.r + ''; + } + else { + thread += 'R: ' + entry.r + ''; + } + if (pinned) { + rDiff = entry.r - pinnedThreads[id]; + if (rDiff > 0) { + thread += ' (+' + rDiff + ')'; + pinnedThreads[id] = entry.r; + } + else { + thread += '(+0)'; + } + } + if (entry.i) { + if (entry.imagelimit) { + thread += ' / I: ' + entry.i + ''; + } + else { + thread += ' / I: ' + entry.i + ''; + } + } + + if (onTop && (page = getThreadPage(id)) >= 0) { + thread += ' / P: ' + page + ''; + } + + thread += ''; + + thread += '
    '; + + if (teaser) { + thread += '
    '; + } + + if (window.partyHats) { + thread = '
    ' + thread + + '
    '; + } + else { + thread += '
    '; + } + + if (entry.sticky) { + stickyHtml += thread; + } + else if (onTop) { + topHtml += thread; + } + else { + html += thread; + } + } + + topHtml = stickyHtml + topHtml; + + if (quickFilterPattern && (html === '' && topHtml === '')) { + html = '
    Nothing Found
    '; + } + else if (topHtml) { + html = topHtml + html + '
    '; + } + else { + html += '
    '; + } + + return html; + } + + function formatTextThreads(threads) { + var + i, id, entry, item, thread, hl, onTop, pinned, + rDiff, html, provider, + pinhl, newtab, topHtml, aTag; + + provider = '//boards.' + $L.d(catalog.slug) + '/' + catalog.slug + '/thread/'; + + newtab = activeTheme.newtab ? 'target="_blank" ' : ''; + + html = ''; + topHtml = ''; + + for (i = 0; item = threads[i]; ++i) { + id = item.id; + entry = item.entry; + hl = item.hl; + onTop = item.onTop; + pinned = item.pinned; + + if (hl.color) { + pinhl = ' class="hl" style="box-shadow: -3px 0 ' + hl.color + '"'; + } + else if (pinned) { + pinhl = ' class="pinned"'; + } + else { + pinhl = ''; + } + + aTag = ''; + + thread = '' + aTag + + '»' + aTag + entry.sub + + ''; + + if (entry.bumplimit) { + thread += '' + entry.r + ''; + } + else { + thread += entry.r; + } + + if (pinned) { + rDiff = entry.r - pinnedThreads[id]; + if (rDiff > 0) { + thread += ' (+' + rDiff + ')'; + pinnedThreads[id] = entry.r; + } + else { + thread += '(+0)'; + } + } + + thread += '' + entry.date + + ''; + + if (onTop) { + topHtml += thread; + } + else { + html += thread; + } + } + + if (quickFilterPattern && (html === '' && topHtml === '')) { + html = '
    Nothing Found
    '; + } + else if (topHtml) { + html = topHtml + html + '
    '; + } + else { + html += '
    '; + } + + html = '' + + '' + html + '
    SubjectRepliesDate
    '; + + return html; + } + + function buildThreads() { + var i, tip, fid, threads; + + if (catalog.count === 0) { + return; + } + + if ($threads.hasChildNodes()) { + if (tip = document.getElementById('th-tip')) { + document.body.removeChild(tip); + } + $threads.textContent = ''; + } + + hiddenThreadsCount = 0; + filteredThreadsCount = 0; + + for (fid in activeFilters) { + activeFilters[fid].hits = 0; + } + + threads = getFilteredThreads(); + + sortThreadList(threads); + + if (!window.text_only) { + $threads.innerHTML = formatImageThreads(threads); + } + else { + $threads.innerHTML = formatTextThreads(threads); + } + + for (i in pinnedThreads) { + localStorage.setItem('4chan-pin-' + catalog.slug, JSON.stringify(pinnedThreads)); + break; + } + + setFilteredCount(filteredThreadsCount); + + setHiddenCount(hiddenThreadsCount); + } + + function onThreadMouseOver(e) { + var t = e.target; + + if ($.hasClass(t, 'thumb') || (window.text_only && $.hasClass(t, 'txt-date'))) { + clearTimeout(tooltipTimeout); + if (hasTooltip) { + hideTooltip(); + } + tooltipTimeout = setTimeout(showTooltip, options.tipdelay, t); + } + } + + function onThreadMouseOut() { + clearTimeout(tooltipTimeout); + if (hasTooltip) { + hideTooltip(); + } + } + + function showTooltip(t) { + var now, tip, el, rect, docWidth, style, page, tid, thread, top, + bottom, docHeight, left; + + now = Date.now() / 1000; + + rect = t.getBoundingClientRect(); + docWidth = document.documentElement.offsetWidth; + + tid = t.getAttribute('data-id'); + + if (!tid) { + return; + } + + thread = catalog.threads[tid]; + + if (page = getThreadPage(tid)) { + page = 'Page ' + page + ''; + } + else { + page = ''; + } + + if (thread.sub && !window.text_only) { + tip = '' + thread.sub + ''; + } + else { + tip = 'Posted'; + } + + tip += ' by ' + (thread.author || catalog.anon); + + if (thread.trip) { + tip += ' ' + thread.trip + ''; + } + + if (thread.capcode) { + tip += ' ## ' + + capcodeMap[thread.capcode]; + } + + tip += ' '; + + if (catalog.flags && thread.country) { + tip += '
    '; + } + + tip += '' + + getDuration(now - thread.date) + + ' ago' + page; + + if ((!options.extended && thread.teaser) || window.text_only) { + tip += ''; + } + + if (thread.lr.date) { + tip += '
    Last reply by ' + thread.lr.author; + + if (thread.lr.trip) { + tip += ' ' + thread.lr.trip + ''; + } + + if (thread.lr.capcode) { + tip += ' ## ' + + thread.lr.capcode.charAt(0).toUpperCase() + + thread.lr.capcode.slice(1); + } + + if (thread.lr.date) { + tip += ' ' + + getDuration(now - thread.lr.date) + + ' ago'; + } + else { + tip += ''; + } + } + + el = document.createElement('div'); + el.id = 'post-preview'; + el.innerHTML = tip; + document.body.appendChild(el); + + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + left = rect.left - el.offsetWidth - 5; + } + else { + left = rect.left + rect.width + 5; + } + + docHeight = document.documentElement.clientHeight; + + bottom = rect.top + el.offsetHeight; + + if (bottom > docHeight) { + top = rect.top - (bottom - docHeight) - 20; + } + else { + top = rect.top; + } + + if (top < 0) { + top = 3; + } + + style = el.style; + style.left = left + window.pageXOffset + 'px'; + style.top = top + window.pageYOffset + 'px'; + + hasTooltip = true; + } + + function hideTooltip() { + document.body.removeChild($.id('post-preview')); + hasTooltip = false; + } + + function getDuration(delta, precise) { + var count, head, tail; + if (delta < 2) { + return 'less than a second'; + } + if (precise && delta < 300) { + return (0 | delta) + ' seconds'; + } + if (delta < 60) { + return (0 | delta) + ' seconds'; + } + if (delta < 3600) { + count = 0 | (delta / 60); + if (count > 1) { + return count + ' minutes'; + } + else { + return 'one minute'; + } + } + if (delta < 86400) { + count = 0 | (delta / 3600); + if (count > 1) { + head = count + ' hours'; + } + else { + head = 'one hour'; + } + tail = 0 | (delta / 60 - count * 60); + if (tail > 1) { + head += ' and ' + tail + ' minutes'; + } + return head; + } + count = 0 | (delta / 86400); + if (count > 1) { + head = count + ' days'; + } + else { + head = 'one day'; + } + tail = 0 | (delta / 3600 - count * 24); + if (tail > 1) { + head += ' and ' + tail + ' hours'; + } + return head; + } +}; + +var Filter = {}; + +Filter.init = function() { + this.entities = document.createElement('div'); + Filter.load(); +}; + +Filter.match = function(post, board) { + var i, com, f, filters, hit; + + hit = false; + filters = Filter.activeFilters; + + for (i = 0; f = filters[i]; ++i) { + // boards + if (!f.boards[board]) { + continue; + } + // tripcode + if (f.type == 0) { + if (f.pattern === post.trip) { + hit = true; + break; + } + } + // name + else if (f.type == 1) { + if (f.pattern === post.name) { + hit = true; + break; + } + } + // comment + else if (f.type == 2 && post.com) { + if (com === undefined) { + this.entities.innerHTML + = post.com.replace(/
    /g, '\n').replace(/[<[^>]+>/g, ''); + com = this.entities.textContent; + } + if (f.pattern.test(com)) { + hit = true; + break; + } + } + // user id + else if (f.type == 4) { + if (f.pattern === post.id) { + hit = true; + break; + } + } + // subject + else if (f.type == 5) { + if (f.pattern.test(post.sub)) { + hit = true; + break; + } + } + // filename + else if (f.type == 6) { + if (f.pattern.test(post.filename)) { + hit = true; + break; + } + } + } + + return hit; +}; + +FC.getDocTopOffset = function() { + if (window.Config.dropDownNav && !window.Config.autoHideNav) { + return $.id( + window.Config.classicNav ? 'boardNavDesktop' : 'boardNavMobile' + ).offsetHeight; + } + else { + return 0; + } +}; + +Filter.load = function() { + var i, j, f, rawFilters, rawPattern, fid, regexEscape, regexType, + wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard, boards, + pattern, match, tmp; + + this.activeFilters = []; + + if (!(rawFilters = localStorage.getItem('4chan-filters'))) { + return; + } + + rawFilters = JSON.parse(rawFilters); + + regexEscape = new RegExp('(\\' + + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\') + + ')', 'g'); + regexType = /^\/(.*)\/(i?)$/; + wordSepS = '(?=.*\\b'; + wordSepE = '\\b)'; + regexWildcard = /\\\*/g; + replaceWildcard = '[^\\s]*'; + + try { + for (fid = 0; f = rawFilters[fid]; ++fid) { + if (f.active && f.pattern !== '') { + // Boards + if (f.boards) { + tmp = f.boards.split(/[^a-z0-9]+/i); + boards = {}; + for (i = 0; j = tmp[i]; ++i) { + boards[j] = true; + } + } + else { + boards = false; + } + + rawPattern = f.pattern; + // Name, Tripcode or ID, string comparison + if (!f.type || f.type == 1 || f.type == 4) { + pattern = rawPattern; + } + // /RegExp/ + else if (match = rawPattern.match(regexType)) { + pattern = new RegExp(match[1], match[2]); + } + // "Exact match" + else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') { + pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); + } + // Full words, AND operator + else { + words = rawPattern.split(' '); + pattern = ''; + for (i = 0, j = words.length; i < j; ++i) { + inner = words[i] + .replace(regexEscape, '\\$1') + .replace(regexWildcard, replaceWildcard); + pattern += wordSepS + inner + wordSepE; + } + pattern = new RegExp('^' + pattern, 'im'); + } + //console.log('Resulting pattern: ' + pattern); + this.activeFilters.push({ + type: f.type, + pattern: pattern, + boards: boards, + color: f.color, + hide: f.hide, + auto: f.auto + }); + } + } + } + catch (e) { + alert('There was an error processing one of the filters: ' + + e + ' in: ' + rawPattern); + } +}; + +/** + * Thread watcher + */ +var ThreadWatcher = { + hasFilters: false +}; + +ThreadWatcher.init = function() { + var cnt, pos, el; + + if (this.hasFilters) { + Filter.init(); + } + + this.listNode = null; + this.charLimit = 45; + this.watched = {}; + this.blacklisted = {}; + this.isRefreshing = false; + + if (FC.hasMobileLayout) { + el = document.createElement('a'); + el.href = '#'; + el.textContent = 'TW'; + el.addEventListener('click', ThreadWatcher.toggleList, false); + cnt = $.id('settingsWindowLinkMobile'); + cnt.parentNode.insertBefore(el, cnt); + cnt.parentNode.insertBefore(document.createTextNode(' '), cnt); + } + + cnt = document.createElement('div'); + cnt.id = 'threadWatcher'; + cnt.setAttribute('data-trackpos', 'TW-position'); + + if (FC.hasMobileLayout) { + cnt.style.display = 'none'; + } + else { + if (window.Config['TW-position']) { + cnt.style.cssText = window.Config['TW-position']; + } + else { + cnt.style.left = '10px'; + cnt.style.top = '75px'; + } + } + + cnt.innerHTML = '
    ' + + (FC.hasMobileLayout ? ('
    ') : '') + + 'Thread Watcher' + + (UA.hasCORS ? ('
    ') : '
    '); + + this.listNode = document.createElement('ul'); + this.listNode.id = 'watchList'; + + this.load(); + + this.build(); + + cnt.appendChild(this.listNode); + document.body.appendChild(cnt); + cnt.addEventListener('mouseup', this.onClick, false); + Draggable.set($.id('twHeader')); + window.addEventListener('storage', this.syncStorage, false); + + if (!FC.hasMobileLayout && this.canAutoRefresh()) { + this.refresh(); + } +}; + +ThreadWatcher.unInit = function() { + var cnt; + + if (cnt = $.id('threadWatcher')) { + cnt.removeEventListener('mouseup', this.onClick, false); + Draggable.unset($.id('twHeader')); + window.removeEventListener('storage', this.syncStorage, false); + document.body.removeChild(cnt); + } +}; + +ThreadWatcher.toggleList = function(e) { + var el = $.id('threadWatcher'); + + e && e.preventDefault(); + + if (ThreadWatcher.canAutoRefresh()) { + ThreadWatcher.refresh(); + } + + if (el.style.display == 'none') { + el.style.top = (window.pageYOffset + 30) + 'px'; + el.style.display = ''; + } + else { + el.style.display = 'none'; + } +}; + +ThreadWatcher.syncStorage = function(e) { + var key; + + if (!e.key) { + return; + } + + key = e.key.split('-'); + + if (key[0] == '4chan' && key[1] == 'watch' && e.newValue != e.oldValue) { + ThreadWatcher.load(); + ThreadWatcher.build(); + } +}; + +ThreadWatcher.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-watch')) { + this.watched = JSON.parse(storage); + } + if (storage = localStorage.getItem('4chan-watch-bl')) { + this.blacklisted = JSON.parse(storage); + } +}; + +ThreadWatcher.build = function() { + var html, tuid, key, cls; + + html = ''; + + for (key in this.watched) { + tuid = key.split('-'); + html += '
  • × (' + this.watched[key][2] + ') '; + } + else { + html += (cls[0] ? ('class="' + cls.join(' ') + '"') : '') + '>'; + } + } + + html += '/' + tuid[1] + '/ - ' + this.watched[key][0] + '
  • '; + } + + ThreadWatcher.listNode.innerHTML = html; +}; + +ThreadWatcher.onClick = function(e) { + var t = e.target; + + if (t.hasAttribute('data-id')) { + ThreadWatcher.toggle( + t.getAttribute('data-id'), + t.getAttribute('data-board') + ); + } + else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) { + ThreadWatcher.refreshWithAutoWatch(); + } + else if (t.id == 'twClose') { + ThreadWatcher.toggleList(); + } +}; + +ThreadWatcher.generateLabel = function(sub, com, tid) { + var label; + + if (label = sub) { + label = label.slice(0, this.charLimit); + } + else if (label = com) { + label = label.replace(/(?:
    )+/g, ' ') + .replace(/<[^>]*?>/g, '').slice(0, this.charLimit); + } + else { + label = 'No.' + tid; + } + + return label; +}; + +ThreadWatcher.toggle = function(tid, board, sub, com, lr) { + var key, label, lastReply, icon; + + key = tid + '-' + board; + icon = $.id('leaf-' + tid); + + if (this.watched[key]) { + delete this.watched[key]; + if (icon) { + icon.className = 'watchIcon'; + icon.title = 'Watch'; + } + } + else { + label = ThreadWatcher.generateLabel(sub, com, tid); + + lastReply = lr || tid; + + this.watched[key] = [ label, lastReply, 0 ]; + + icon.className = 'unwatchIcon'; + icon.title = 'Unwatch'; + } + this.save(); + this.load(); + this.build(); +}; + +ThreadWatcher.addRaw = function(post, board) { + var key, label; + + key = post.no + '-' + board; + + if (this.watched[key]) { + return; + } + + label = ThreadWatcher.generateLabel(post.sub, post.com, post.no); + + this.watched[key] = [ label, 0, 0 ]; +}; + +ThreadWatcher.save = function() { + var i; + + ThreadWatcher.sortByBoard(); + + localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched)); + + //StorageSync.sync('4chan-watch'); + + for (i in ThreadWatcher.blacklisted) { + localStorage.setItem('4chan-watch-bl', JSON.stringify(ThreadWatcher.blacklisted)); + //StorageSync.sync('4chan-watch-bl'); + break; + } +}; + +ThreadWatcher.sortByBoard = function() { + var i, self, key, sorted, keys; + + self = ThreadWatcher; + + sorted = {}; + keys = []; + + for (key in self.watched) { + keys.push(key); + } + + keys.sort(function(a, b) { + a = a.split('-')[1]; + b = b.split('-')[1]; + + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }); + + for (i = 0; key = keys[i]; ++i) { + sorted[key] = self.watched[key]; + } + + self.watched = sorted; +}; + +ThreadWatcher.canAutoRefresh = function() { + var time; + + if (time = localStorage.getItem('4chan-tw-timestamp')) { + return Date.now() - (+time) >= 60000; + } + return false; +}; + +ThreadWatcher.setRefreshTimestamp = function() { + localStorage.setItem('4chan-tw-timestamp', Date.now()); + //StorageSync.sync('4chan-tw-timestamp'); +}; + +ThreadWatcher.refreshWithAutoWatch = function() { + var i, f, count, board, boards, img; + + if (!this.hasFilters) { + this.refresh(); + return; + } + + Filter.load(); + + boards = {}; + count = 0; + + for (i = 0; f = Filter.activeFilters[i]; ++i) { + if (!f.auto || !f.boards) { + continue; + } + for (board in f.boards) { + if (boards[board]) { + continue; + } + boards[board] = true; + ++count; + } + } + + if (!count) { + this.refresh(); + return; + } + + img = $.id('twPrune'); + img.className = 'icon rotateIcon'; + this.isRefreshing = true; + + this.fetchCatalogs(boards, count); +}; + +ThreadWatcher.fetchCatalogs = function(boards, count) { + var to, board, catalogs, meta; + + catalogs = {}; + meta = { count: count }; + to = 0; + + for (board in boards) { + setTimeout(ThreadWatcher.fetchCatalog, to, board, catalogs, meta); + to += 200; + } +}; + +ThreadWatcher.parseCatalogJSON = function(data) { + var catalog; + + try { + catalog = JSON.parse(data); + } + catch (e) { + console.log(e); + catalog = []; + } + + return catalog; +}; + +ThreadWatcher.fetchCatalog = function(board, catalogs, meta) { + var xhr; + + xhr = new XMLHttpRequest(); + xhr.open('GET', '//a.4cdn.org/' + board + '/catalog.json'); + xhr.onload = function() { + meta.count--; + catalogs[board] = ThreadWatcher.parseCatalogJSON(this.responseText); + if (!meta.count) { + ThreadWatcher.onCatalogsLoaded(catalogs); + } + }; + xhr.onerror = function() { + meta.count--; + if (!meta.count) { + ThreadWatcher.onCatalogsLoaded(catalogs); + } + }; + xhr.send(null); +}; + +ThreadWatcher.onCatalogsLoaded = function(catalogs) { + var i, j, board, page, pages, threads, thread, key, blacklisted; + + $.id('twPrune').className = 'icon rotateIcon'; + this.isRefreshing = false; + + blacklisted = {}; + + for (board in catalogs) { + pages = catalogs[board]; + for (i = 0; page = pages[i]; ++i) { + threads = page.threads; + for (j = 0; thread = threads[j]; ++j) { + key = thread.no + '-' + board; + if (this.blacklisted[key]) { + blacklisted[key] = 1; + continue; + } + if (Filter.match(thread, board)) { + this.addRaw(thread, board); + } + } + } + } + + this.blacklisted = blacklisted; + this.build(true); + this.refresh(); +}; + +ThreadWatcher.refresh = function() { + var i, to, key, total, img; + + if (total = $.id('watchList').children.length) { + i = to = 0; + img = $.id('twPrune'); + img.className = 'icon rotateIcon'; + ThreadWatcher.isRefreshing = true; + ThreadWatcher.setRefreshTimestamp(); + for (key in ThreadWatcher.watched) { + setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null); + to += 200; + } + } +}; + +ThreadWatcher.onRefreshEnd = function(img) { + img.className = 'icon refreshIcon'; + this.isRefreshing = false; + this.save(); + this.load(); + this.build(); +}; + +ThreadWatcher.parseThreadJSON = function(data) { + var thread; + + try { + thread = JSON.parse(data).posts; + } + catch (e) { + console.log(e); + thread = []; + } + + return thread; +}; + +ThreadWatcher.getTrackedReplies = function(board, tid) { + var tracked = null; + + if (tracked = localStorage.getItem('4chan-track-' + board + '-' + tid)) { + tracked = JSON.parse(tracked); + } + + return tracked; +}; + +ThreadWatcher.fetch = function(key, img) { + var tuid, xhr, li; + + li = $.id('watch-' + key); + + if (ThreadWatcher.watched[key][1] == -1) { + delete ThreadWatcher.watched[key]; + li.parentNode.removeChild(li); + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + return; + } + + tuid = key.split('-'); // tid, board + + xhr = new XMLHttpRequest(); + xhr.onload = function() { + var i, newReplies, posts, lastReply, trackedReplies, dummy, quotelinks, q, j; + if (this.status == 200) { + posts = ThreadWatcher.parseThreadJSON(this.responseText); + lastReply = ThreadWatcher.watched[key][1]; + newReplies = 0; + + if (!ThreadWatcher.watched[key][4]) { + trackedReplies = ThreadWatcher.getTrackedReplies(tuid[1], tuid[0]); + + if (trackedReplies) { + dummy = document.createElement('div'); + } + } + else { + trackedReplies = null; + } + + for (i = posts.length - 1; i >= 1; i--) { + if (posts[i].no <= lastReply) { + break; + } + ++newReplies; + + if (trackedReplies) { + dummy.innerHTML = posts[i].com; + quotelinks = $.cls('quotelink', dummy); + + if (!quotelinks[0]) { + continue; + } + + for (j = 0; q = quotelinks[j]; ++j) { + if (trackedReplies[q.textContent]) { + ThreadWatcher.watched[key][4] = 1; + trackedReplies = null; + break; + } + } + } + } + if (newReplies > ThreadWatcher.watched[key][2]) { + ThreadWatcher.watched[key][2] = newReplies; + } + if (posts[0].archived) { + ThreadWatcher.watched[key][3] = 1; + } + } + else if (this.status == 404) { + ThreadWatcher.watched[key][1] = -1; + } + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + }; + if (img) { + xhr.onerror = xhr.onload; + } + xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json'); + xhr.send(null); +}; + +ThreadWatcher.linkToThread = function(tid, board, post) { + return '//' + location.host + '/' + + board + '/thread/' + + tid + (post > 0 ? ('#p' + post) : ''); +}; + +/** + * Draggable helper + */ +var Draggable = { + el: null, + key: null, + scrollX: null, + scrollY: null, + dx: null, dy: null, right: null, bottom: null, + + set: function(handle) { + handle.addEventListener('mousedown', Draggable.startDrag, false); + }, + + unset: function(handle) { + handle.removeEventListener('mousedown', Draggable.startDrag, false); + }, + + startDrag: function(e) { + var self, doc, offs; + + if (this.parentNode.hasAttribute('data-shiftkey') && !e.shiftKey) { + return; + } + + e.preventDefault(); + + self = Draggable; + doc = document.documentElement; + + self.el = this.parentNode; + + self.key = self.el.getAttribute('data-trackpos'); + offs = self.el.getBoundingClientRect(); + self.dx = e.clientX - offs.left; + self.dy = e.clientY - offs.top; + self.right = doc.clientWidth - offs.width; + self.bottom = doc.clientHeight - offs.height; + + if (getComputedStyle(self.el, null).position != 'fixed') { + self.scrollX = window.pageXOffset; + self.scrollY = window.pageYOffset; + } + else { + self.scrollX = self.scrollY = 0; + } + + self.offsetTop = FC.getDocTopOffset(); + + document.addEventListener('mouseup', self.endDrag, false); + document.addEventListener('mousemove', self.onDrag, false); + }, + + endDrag: function() { + document.removeEventListener('mouseup', Draggable.endDrag, false); + document.removeEventListener('mousemove', Draggable.onDrag, false); + if (Draggable.key && window.Config) { + window.Config[Draggable.key] = Draggable.el.style.cssText; + localStorage.setItem('4chan-settings', JSON.stringify(window.Config)); + //StorageSync.sync('4chan-settings'); + } + delete Draggable.el; + }, + + onDrag: function(e) { + var left, top, style; + + left = e.clientX - Draggable.dx + Draggable.scrollX; + top = e.clientY - Draggable.dy + Draggable.scrollY; + style = Draggable.el.style; + if (left < 1) { + style.left = '0'; + style.right = ''; + } + else if (Draggable.right < left) { + style.left = ''; + style.right = '0'; + } + else { + style.left = (left / document.documentElement.clientWidth * 100) + '%'; + style.right = ''; + } + if (top <= Draggable.offsetTop) { + style.top = Draggable.offsetTop + 'px'; + style.bottom = ''; + } + else if (Draggable.bottom < top && + Draggable.el.clientHeight < document.documentElement.clientHeight) { + style.bottom = '0'; + style.top = ''; + } + else { + style.top = (top / document.documentElement.clientHeight * 100) + '%'; + style.bottom = ''; + } + } +}; + +/** + * Custom Menu + */ +var CustomMenu = { + dropDownNav: false, + classicNav: false +}; + +CustomMenu.initCtrl = function(dropDownNav, classicNav) { + var el, cnt; + + CustomMenu.dropDownNav = dropDownNav; + CustomMenu.classicNav = classicNav; + + el = document.createElement('span'); + el.className = 'custom-menu-ctrl'; + el.innerHTML = '[Edit]'; + + if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { + cnt = $.id('boardSelectMobile').parentNode; + cnt.insertBefore(el, cnt.lastChild); + } + else { + cnt = $.cls('boardList'); + cnt[0] && cnt[0].appendChild(el); + cnt[1] && cnt[1].appendChild(el.cloneNode(true)); + } +}; +/* +CustomMenu.showNWSBoards = function() { + var i, el, nodes, len; + + nodes = $.cls('nwsb'); + len = nodes.length; + + for (i = len - 1; el = nodes[i]; i--) { + $.removeClass(el, 'nwsb'); + } +}; +*/ +CustomMenu.reset = function() { + var i, el, full, custom, navs; + + full = $.cls('boardList'); + custom = $.cls('customBoardList'); + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.removeEventListener('click', CustomMenu.reset, false); + } + + for (i = custom.length - 1; el = custom[i]; i--) { + full[i].style.display = null; + el.parentNode.removeChild(el); + } +}; + +CustomMenu.apply = function(str) { + var i, el, cntBottom, board, navs, boardList, cnt; + + if (!str) { + if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { + if (el = $.cls('customBoardList')[0]) { + el.parentNode.removeChild(el); + } + } + return; + } + + boardList = str.split(/[^0-9a-z]/i); + + cnt = document.createElement('span'); + cnt.className = 'customBoardList'; + + for (i = 0; board = boardList[i]; ++i) { + if (i) { + cnt.appendChild(document.createTextNode(' / ')); + } + else { + cnt.appendChild(document.createTextNode('[')); + } + el = document.createElement('a'); + el.textContent = board; + el.href = '//boards.' + $L.d(board) + '/' + board + (board !== 'f' ? '/catalog' : ''); + cnt.appendChild(el); + } + + cnt.appendChild(document.createTextNode(']')); + + if (CustomMenu.dropDownNav && !CustomMenu.classicNav && !FC.hasMobileLayout) { + if (el = $.cls('customBoardList')[0]) { + el.parentNode.removeChild(el); + } + navs = $.id('boardSelectMobile'); + navs && navs.parentNode.insertBefore(cnt, navs.nextSibling); + } + else { + cnt.appendChild(document.createTextNode(' [')); + el = document.createElement('a'); + el.textContent = '…'; + el.title = 'Show all'; + el.className = 'show-all-boards pointer'; + cnt.appendChild(el); + cnt.appendChild(document.createTextNode('] ')); + + cntBottom = cnt.cloneNode(true); + + navs = $.cls('boardList'); + + for (i = 0; el = navs[i]; ++i) { + el.style.display = 'none'; + el.parentNode.insertBefore(i ? cntBottom : cnt, el); + } + + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.addEventListener('click', CustomMenu.reset, false); + } + } +}; + +CustomMenu.onClick = function(e) { + var t; + + if ((t = e.target) == document) { + return; + } + + if (t.hasAttribute('data-close')) { + CustomMenu.closeEditor(); + } + else if (t.hasAttribute('data-save')) { + CustomMenu.save($.id('customMenu').hasAttribute('data-standalone')); + } +}; + +CustomMenu.showEditor = function(standalone) { + var cnt, extConfig; + + cnt = document.createElement('div'); + cnt.id = 'customMenu'; + cnt.className = 'panel'; + cnt.setAttribute('data-close', '1'); + + if (standalone === true) { + cnt.setAttribute('data-standalone', '1'); + } + + cnt.innerHTML = '\ +
    Custom Board List\ +
    \ +\ +
    '; + + document.body.appendChild(cnt); + + cnt.style.top = window.pageYOffset + + (0 | (document.documentElement.clientHeight / 2) - (cnt.offsetHeight / 2)) + 'px'; + + $.removeClass($.id('backdrop'), 'hidden'); + + extConfig = CustomMenu.getConfig(); + + if (extConfig.customMenuList) { + $.id('customMenuBox').value = extConfig.customMenuList; + } + + cnt.addEventListener('click', CustomMenu.onClick, false); +}; + +CustomMenu.closeEditor = function() { + var el; + + if (el = $.id('customMenu')) { + el.removeEventListener('click', CustomMenu.onClick, false); + document.body.removeChild(el); + $.addClass($.id('backdrop'), 'hidden'); + } +}; + +CustomMenu.save = function(standalone) { + var input, extConfig; + + if (input = $.id('customMenuBox')) { + if (standalone === true) { + CustomMenu.apply(input.value); + + extConfig = CustomMenu.getConfig(); + + extConfig.customMenu = true; + extConfig.customMenuList = input.value; + + localStorage.setItem('4chan-settings', JSON.stringify(extConfig)); + //StorageSync.sync('4chan-settings'); + } + } + + CustomMenu.closeEditor(); +}; + +CustomMenu.getConfig = function() { + var extConfig; + + if (extConfig = localStorage.getItem('4chan-settings')) { + return JSON.parse(extConfig); + } + else { + return {}; + } +}; + +function checkMobileLayout() { + var mobile, desktop; + + if (window.matchMedia) { + return window.matchMedia('(max-width: 480px)').matches + && localStorage.getItem('4chan_never_show_mobile') != 'true'; + } + + mobile = $.id('boardNavMobile'); + desktop = $.id('boardNavDesktop'); + + return mobile && desktop && mobile.offsetWidth > 0 && desktop.offsetWidth === 0; +} + +var StickyNav = { + thres: 5, + pos: 0, + timeout: null, + el: null, + + init: function(classicNav) { + this.el = classicNav ? $.id('boardNavDesktop') : $.id('boardNavMobile'); + $.addClass(this.el, 'autohide-nav'); + window.addEventListener('scroll', this.onScroll, false); + }, + + destroy: function(classicNav) { + this.el = classicNav ? $.id('boardNavDesktop') : $.id('boardNavMobile'); + $.removeClass(this.el, 'autohide-nav'); + window.removeEventListener('scroll', this.onScroll, false); + }, + + onScroll: function() { + clearTimeout(StickyNav.timeout); + StickyNav.timeout = setTimeout(StickyNav.checkScroll, 50); + }, + + checkScroll: function() { + var thisPos; + + thisPos = window.pageYOffset; + + if (Math.abs(StickyNav.pos - thisPos) <= StickyNav.thres) { + return; + } + + if (thisPos < StickyNav.pos) { + StickyNav.el.style.top = ''; + } + else { + StickyNav.el.style.top = '-' + StickyNav.el.offsetHeight + 'px'; + } + + StickyNav.pos = thisPos; + } +}; + +FC.panelHTML = { + build: function(id, cls) { + var el; + + el = document.createElement('div'); + el.id = id; + el.className = cls; + el.innerHTML = FC.panelHTML[id]; + + document.body.appendChild(el); + + return el; + }, + + 'theme': '
    Settings
    \ +

    Options

    \ +
      \ +
    • \ +
    • \ +
    • \ +
    • \ +
    • \ +
    \ +

    Shortcuts

    \ +
      \ +
    • R — Refresh current page
    • \ +
    • X — Reorder threads
    • \ +
    • S — Open search box, Esc to close
    • \ +
    • Shift LMB — Hide threads
    • \ +
    • Alt LMB — Pin threads
    • \ +
    • RMB — Threads context menu (Firefox only)
    • \ +
    \ +

    Custom CSS

    \ + \ +
    \ + \ +
    \ +
    ', + + 'filters-protip': '
    Filters & Highlights Help
    \ +

    Patterns

    \ +
      • \ +
      • Matching whole words:
      • \ +
      • feel — will match "feel" but not "feeling". This search is case-insensitive.
      • \ +
    • \ +
      • \ +
      • AND operator:
      • \ +
      • feel girlfriend — will match "feel" AND "girlfriend" in any order.
      • \ +
    • \ +
      • \ +
      • OR operator:
      • \ +
      • feel|girlfriend — will match "feel" OR "girlfriend".
      • \ +
    • \ +
      • \ +
      • Mixing both operators:
      • \ +
      • girlfriend|boyfriend feel — matches "feel" AND "girlfriend", or "feel" AND "boyfriend".
      • \ +
    • \ +
      • \ +
      • Exact match search:
      • \ +
      • "that feel when" — place double quotes around the pattern to search for an exact string
      • \ +
    • \ +
      • \ +
      • Wildcards:
      • \ +
      • feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc…
      • \ +
      • idolm*ster — this can match "idolmaster" or "idolm@ster", etc…
      • \ +
    • \ +
      • \ +
      • Filtering by name or tripcode:
      • \ +
      • Prefix the pattern with # to search by tripcode: #!Ep8pui8Vw2
      • \ +
      • Prefix the pattern with ## to search by name: ##Anonymous
      • \ +
      • To filter by capcode: #!#admin, #!#mod, #!#developer
      • \ +
    • \ +
      • \ +
      • It is also possible to filter by regular expression:
      • \ +
      • /^(?=.*detachable)(?=.*hats).*$/i — AND operator.
      • \ +
      • /^(?!.*touhou).*$/i — NOT operator.
      • \ +
      • /^&gt;/ — threads starting with a quote (">" character as an html entity).
      • \ +
      • /^$/ — threads with no text.
      • \ +
    • \ +
    \ +
    \ +

    Controls

    \ +
      \ +
    • On — enables or disables the filter.
    • \ +
    • Boards — space separated list of boards on which the filter will be active. Leave blank to apply to all boards.
    • \ +
    • Hide — hides matched threads.
    • \ +
    • Top — puts matched threads on top of the list.
    • \ +
    ', + + 'filter-palette': '
    \ + \ + \ + \ +
    Custom
    \ + Close\ + Clear\ +
    ', + + 'filters': '
    Filters & Highlights
    \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
    OrderOnPatternBoardsColorHideTopDel
    \ + \ + \ +
    ' +}; + +var PostMenu = { + activeBtn: null +}; + +PostMenu.open = function(btn, pid, hasThreadWatcher, hidden, pinned) { + var div, html, btnPos, left, limit, tr; + + if (PostMenu.activeBtn == btn) { + PostMenu.close(); + return; + } + + PostMenu.close(); + + tr = btn.parentNode.parentNode; + + html = '
    • Report thread
    • ' + + '
    • ' + + (pinned ? 'Unpin' : 'Pin') + ' thread
    • ' + + '
    • ' + + (hidden ? 'Unhide' : 'Hide') + ' thread
    • '; + + if (hasThreadWatcher) { + html += '
    • ' + + (ThreadWatcher.watched[pid + '-' + catalog.slug] ? 'Remove from' : 'Add to') + + ' watch list
    • '; + } + + div = document.createElement('div'); + div.id = 'post-menu'; + div.className = 'dd-menu'; + div.innerHTML = html + '
    '; + + btnPos = btn.getBoundingClientRect(); + + div.style.top = btnPos.bottom + 3 + window.pageYOffset + 'px'; + + document.addEventListener('click', PostMenu.close, false); + + $.addClass(btn, 'menuOpen'); + PostMenu.activeBtn = btn; + + UA.dispatchEvent('4chanPostMenuReady', { postId: pid, isOP: true, node: div.firstElementChild }); + + document.body.appendChild(div); + + left = btnPos.left + window.pageXOffset; + limit = document.documentElement.clientWidth - div.offsetWidth; + + if (left > (limit - 75)) { + div.className += ' dd-menu-left'; + } + + if (left > limit) { + left = limit; + } + + div.style.left = left + 'px'; +}; + +PostMenu.close = function() { + var el; + + if (el = $.id('post-menu')) { + el.parentNode.removeChild(el); + document.removeEventListener('click', PostMenu.close, false); + $.removeClass(PostMenu.activeBtn, 'menuOpen'); + PostMenu.activeBtn = null; + } +}; \ No newline at end of file diff --git a/js/catalog.min.js b/js/catalog.min.js new file mode 100644 index 0000000..0ceffff --- /dev/null +++ b/js/catalog.min.js @@ -0,0 +1,2 @@ +function checkMobileLayout(){var e,t;return window.matchMedia?window.matchMedia("(max-width: 480px)").matches&&"true"!=localStorage.getItem("4chan_never_show_mobile"):(e=$.id("boardNavMobile"),t=$.id("boardNavDesktop"),e&&t&&e.offsetWidth>0&&0===t.offsetWidth)}var $={};$.id=function(e){return document.getElementById(e)},$.cls=function(e,t){return(t||document).getElementsByClassName(e)},$.tag=function(e,t){return(t||document).getElementsByTagName(e)},$.extend=function(e,t){for(var a in t)e[a]=t[a]},$.on=function(e,t,a){e.addEventListener(t,a,!1)},$.off=function(e,t,a){e.removeEventListener(t,a,!1)},$.readCookie=function(e){var t,a,i,n;for(n=e+"=",i=document.cookie.split(";"),t=0;a=i[t];++t){for(;" "==a.charAt(0);)a=a.substring(1,a.length);if(0===a.indexOf(n))return decodeURIComponent(a.substring(n.length,a.length))}return null},document.documentElement.classList?($.hasClass=function(e,t){return e.classList.contains(t)},$.addClass=function(e,t){e.classList.add(t)},$.removeClass=function(e,t){e.classList.remove(t)}):($.hasClass=function(e,t){return-1!=(" "+e.className+" ").indexOf(" "+t+" ")},$.addClass=function(e,t){e.className=""===e.className?t:e.className+" "+t},$.removeClass=function(e,t){e.className=(" "+e.className+" ").replace(" "+t+" ","")}),$.toggleClass=function(e,t){$.hasClass(e,t)?$.removeClass(e,t):$.addClass(e,t)};var UA={};UA.init=function(){document.head=document.head||$.tag("head")[0],this.hasContextMenu="HTMLMenuItemElement"in window,this.hasWebStorage=function(){var e="catalog";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch(t){return!1}}(),this.hasSessionStorage=function(){var e="catalog";try{return sessionStorage.setItem(e,e),sessionStorage.removeItem(e),!0}catch(t){return!1}}(),this.hasCORS="withCredentials"in new XMLHttpRequest,this.isMobileDevice=/Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent)},UA.dispatchEvent=function(e,t){var a=document.createEvent("Event");a.initEvent(e,!1,!1),t&&(a.detail=t),document.dispatchEvent(a)};var FC=function(){function e(){var e,t,a;t=$.id("boardNavDesktop"),a=$.id("boardNavDesktopFoot"),Ke?((e=document.createElement("div")).className="pageJump",e.innerHTML='SettingsHome',t.appendChild(e),$.id("settingsWindowLinkClassic").addEventListener("click",Q,!1),$.addClass(t,"persistentNav")):(t.style.display="none",$.removeClass($.id("boardNavMobile"),"mobile")),Ge&&StickyNav.init(Ke),a.style.display="none",$.addClass(document.body,"hasDropDownNav")}function t(){var e,t,a;t=$.id("boardNavDesktop"),a=$.id("boardNavDesktopFoot"),Ke?((e=$.cls("pageJump",t)[0])&&($.id("settingsWindowLinkClassic").removeEventListener("click",Q,!1),t.removeChild(e)),$.removeClass(t,"persistentNav")):(t.style.display="",$.addClass($.id("boardNavMobile"),"mobile")),Ge&&StickyNav.destroy(Ke),a.style.display="",$.removeClass(document.body,"hasDropDownNav")}function a(){var e,t,a,n;UA.hasWebStorage&&!FC.hasMobileLayout&&(e=$.id("globalMessage"))&&e.textContent&&(e.nextElementSibling.style.clear="both",(t=document.createElement("span")).id="toggleMsgBtn",t.setAttribute("data-cmd","toggleMsg"),t.title="Toggle announcement",n=localStorage.getItem("4chan-global-msg"),a=e.getAttribute("data-utc"),n&&a<=n?(e.style.display="none",t.style.opacity="0.5",t.className="expandIcon"):t.className="collapseIcon",$.on(t,"click",i),e.parentNode.insertBefore(t,e))}function i(){var e,t;e=$.id("globalMessage"),t=$.id("toggleMsgBtn"),"none"==e.style.display?(e.style.display="",t.className="collapseIcon",t.style.opacity="1",localStorage.removeItem("4chan-global-msg")):(e.style.display="none",t.className="expandIcon",t.innerHTML='View Important Announcement',t.style.opacity="0.5",localStorage.setItem("4chan-global-msg",e.getAttribute("data-utc")))}function n(){var e=document.getElementById("postForm");"table"==e.style.display?(e.style.display="",this.textContent="Start a New Thread"):(e.style.display="table",this.textContent="Close Post Form",window.initRecaptcha(),window.initTCaptcha())}function o(){return new RegExp("(\\"+["/",".","*","+","?","(",")","[","]","{","}","\\"].join("|\\")+")","g")}function s(e){return 1+(0|Re.threads[e].b/Re.pagesize)}function l(){var e,t,a,i;for(a=(t=$.id("styleSelector")).children,e=0;i=a[e];++e)i.value==xe&&(t.selectedIndex=e);$.on(t,"change",r)}function r(){var e;"_special"!==this.value?((e=new Date).setTime(e.getTime()+31536e6),document.cookie=Te+"="+this.value+"; expires="+e.toGMTString()+"; path=/; domain="+$L.d(Re.slug),window.css_event&&(fn=window["fc_"+window.css_event+"_cleanup"],localStorage.setItem("4chan_stop_css_event",`${window.css_event}-${window.css_event_v}`))):window.css_event&&(fn=window["fc_"+window.css_event+"_init"],localStorage.removeItem("4chan_stop_css_event")),d()}function d(e){e&&e.shiftKey||(location.href=location.href)}function c(e,t){var a;return function(){var i=arguments,n=this;clearTimeout(a),a=setTimeout(function(){t.apply(n,i)},e)}}function h(){$.hasClass(Me,"active")?m(!0):u()}function u(){var e,t=$.id("qf-cnt");$.hasClass(Me,"active")?(m(),t.style.display="none",$.removeClass(Me,"active")):(t.style.display="inline",e=$.id("qf-box"),t.hasAttribute("data-built")||(t.setAttribute("data-built","1"),$.on(e,"keyup",c(250,p)),$.on(e,"keydown",function(e){"27"==e.keyCode&&u()})),e.focus(),e.value="",$.addClass(Me,"active"))}function p(){var e,t;""!==(t=$.id("qf-box").value)?(UA.hasSessionStorage&&(sessionStorage.setItem("4chan-catalog-search",t),sessionStorage.setItem("4chan-catalog-search-board",Re.slug)),e=o(),$.id("search-term").textContent=$.id("search-term-bottom").textContent=t,$.id("search-label").style.display=$.id("search-label-bottom").style.display="inline",t=t.replace(e,"\\$1"),Ze=new RegExp(t,"i"),ye()):m()}function m(e){var t=$.id("qf-box");$.id("search-label").style.display=$.id("search-label-bottom").style.display="none",e?(t.value="",t.focus()):(UA.hasSessionStorage&&sessionStorage.removeItem("4chan-catalog-search"),Ze=!1,ye())}function f(){Ie={pin:b,hide:v,report:N},$.id("ctxmenu-main").innerHTML='',$.id("ctxmenu-thread").innerHTML='',$.on($.id("ctxmenu-main"),"click",M),$.on($.id("ctxmenu-thread"),"click",T)}function g(){var e,t;UA.hasWebStorage&&$.on(Se,"mousedown",function(a){if(-1!=(e=a.target).className.indexOf("thumb"))if(t=e.getAttribute("data-id"),3==a.which)Se.setAttribute("contextmenu","ctxmenu-thread"),$.id("ctxmenu-thread").target=t;else{if(1==a.which&&a.altKey)return b(t),!1;if(1==a.which&&a.shiftKey)return v(t),!1}else 3==a.which&&Se.setAttribute("contextmenu","ctxmenu-main")}),_e.nobinds||$.on(document,"keyup",x)}function b(e){Ye[e]>=0?delete Ye[e]:Ye[e]=Re.threads[e].r||0,localStorage.setItem("4chan-pin-"+Re.slug,JSON.stringify(Ye)),ye()}function v(e){et?(delete qe[e],--Xe):(qe[e]=!0,++Xe),localStorage.setItem("4chan-hide-t-"+Re.slug,JSON.stringify(qe)),$.id("thread-"+e).style.display="none",C(Xe),0===Xe&&y(!1)}function y(e){et=e,$.id("filters-clear-hidden").textContent=$.id("filters-clear-hidden-bottom").textContent=e?"Back":"Show",ye()}function w(e,t){var a=e+"-label",i=e+"-count";t>0?($.id(i).textContent=$.id(i+"-bottom").textContent=t,$.id(a).style.display=$.id(a+"-bottom").style.display="inline"):$.id(a).style.display=$.id(a+"-bottom").style.display="none"}function C(e){w("hidden",e)}function k(e){w("filtered",e)}function N(e){var t,a;window.passEnabled||!window.grecaptcha?t=175:Qe?(t=320,a="&altc=1"):(t=510,a=""),window.open("https://sys."+$L.d(Re.slug)+"/"+Re.slug+"/imgboard.php?mode=report&no="+e+a,Date.now(),"toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height="+t)}function T(e){var t=e.target.getAttribute("data-cmd");Ie[t]($.id("ctxmenu-thread").target)}function x(e){var t=e.target;"TEXTAREA"!=t.nodeName&&"INPUT"!=t.nodeName&&Oe[e.keyCode]&&Oe[e.keyCode](e)}function S(e){e.preventDefault(),Xe>0&&("Show"==$.id("filters-clear-hidden").textContent?y(!0):y(!1))}function M(){return Ye={},localStorage.removeItem("4chan-pin-"+Re.slug),ye(),!1}function L(e){var t,a=e.target;(a=e.target)!=document&&((t=a.getAttribute("data-watch"))?ThreadWatcher.toggle(t,Re.slug,Re.threads[t].sub,Re.threads[t].teaser,Re.threads[t].lr.id):(t=a.getAttribute("data-hide"))?(e.preventDefault(),v(t)):(t=a.getAttribute("data-pin"))?(e.preventDefault(),b(t)):(t=a.getAttribute("data-report"))?(e.preventDefault(),N(t)):(t=a.getAttribute("data-post-menu"))?(e.preventDefault(),PostMenu.open(a,t,ze,qe[t],Ye[t])):a.hasAttribute("data-cm-edit")?(e.preventDefault(),CustomMenu.showEditor(!0)):"backdrop"==a.id?G($.id("filters"))?G($.id("theme"))||Z():G($.id("filters-protip"))?R():I():"filter-palette"==e.target.id&&B())}function E(){var e,t,a,i,n,o,s,l,r;if(Ae=$.id("filter-palette"),a=$.id("filter-color-table"),i=$.tag("tbody",a)[0],(n=Fe.filterColors.length)>0)for(o=Fe.filterColors[0].length,e=(r=$.tag("tfoot",a)[0]).children.length-1;e>=0;e--)r.children[e].firstElementChild.setAttribute("colspan",o);for(e=0;e
    ',$.on(l.firstElementChild,"click",Y),s.appendChild(l);i.appendChild(s)}}function W(e){var t,a=e.getBoundingClientRect();Ae||E(),$.removeClass(Ae,"hidden"),Ae.setAttribute("data-target",e.id.split("-")[2]),(t=Ae.firstElementChild).style.cssText="top:"+a.top+"px;left:"+(a.left-t.clientWidth-10)+"px;"}function A(){var e=$.id("filters-protip");e.style.top=window.pageYOffset+50+"px",$.removeClass(e,"hidden")}function I(){$.addClass($.id("filters-protip"),"hidden")}function D(e){var t=e.target;"filters-close"==t.id?R():"filters-add"==t.id?X():"filters-save"==t.id?(P(),R()):t.hasAttribute("data-active")?V(t,"active"):t.hasAttribute("data-hide")?V(t,"hide","top"):t.hasAttribute("data-top")?V(t,"top","hide"):$.hasClass(t,"filter-color")?W(t):t.hasAttribute("data-target")?z(t):t.hasAttribute("data-up")&&F(t)}function F(e){var t,a;(a=(t=e.parentNode.parentNode).previousElementSibling)&&t.parentNode.insertBefore(t,a)}function H(e){var t,a,i,n,o;for(e&&27==e.keyCode&&(this.value=""),o=this.value.toLowerCase(),i=document.getElementsByClassName("filter-pattern"),(n=document.getElementById("filter-list")).style.display="none",t=0;a=i[t];++t)-1===a.value.toLowerCase().indexOf(o)?a.parentNode.parentNode.style.display="none":a.parentNode.parentNode.style.display="";n.style.display=""}function O(){var e,t,a,i,n,o;if((t=$.id("filters"))||(t=FC.panelHTML.build("filters","panel hidden"),FC.panelHTML.build("filters-protip","panel hidden"),FC.panelHTML.build("filter-palette","hidden")),t.hasAttribute("data-built")?$.id("filters-search").value="":($.on(t,"click",D),$.on($.id("filter-palette-close"),"click",B),$.on($.id("filter-palette-clear"),"click",q),$.on($.id("filters-help-open"),"click",A),$.on($.id("filters-help-close"),"click",I),$.on($.id("filter-rgb"),"keyup",J),$.on($.id("filter-rgb-ok"),"click",Y),$.on($.id("filters-search"),"keyup",H),t.setAttribute("data-built","1")),n=0,a=localStorage.getItem("catalog-filters")){i=$.id("filter-list"),a=JSON.parse(a);for(e in a)i.appendChild(U(a[e],n)),++n;K()}t.style.top=window.pageYOffset+60+"px",$.removeClass(t,"hidden"),(o=$.cls("filter-active",t)[0])&&o.focus(),ee()}function R(){var e,t,a;for($.id("filters-msg").style.display="none",$.addClass($.id("filters"),"hidden"),t=$.id("filter-list"),e=(a=$.tag("tr",t)).length-1;e>=0;e--)t.removeChild(a[e]);B(),ee()}function B(){Ae&&!$.hasClass(Ae,"hidden")&&$.addClass(Ae,"hidden")}function _(){if(UA.hasWebStorage){Pe={};var e=localStorage.getItem("catalog-filters");if(e){e=JSON.parse(e);var t,a,i,n,s,l,r,d,c,h,u,p,m,f,g=/^\/(.*)\/(i?)$/,b=/\s*\|+\s*/g,v=/\\\*/g,y="[^\\s]*",w=o();l="(?=.*\\b",r="\\b)";try{for(a in e)if((t=e[a]).active&&""!==t.pattern){if(t.boards&&-1==t.boards.split(" ").indexOf(Re.slug))continue;if("#"==(h=t.pattern).charAt(0))f="#"==h.charAt(1)?2:1,u=new RegExp(h.slice(f).replace(w,"\\$1"));else if(f=0,d=h.match(g))u=new RegExp(d[1],d[2]);else if('"'==h.charAt(0)&&'"'==h.charAt(h.length-1))u=new RegExp(h.slice(1,-1).replace(w,"\\$1"));else{for(u="",s=(c=h.replace(b,"|").split(" ")).length,n=0;n=0;i--)""!==p[i]&&m.push(p[i].replace(w,"\\$1"));u+=l+"("+m.join("|").replace(v,y)+")"+r}else u+=l+c[n].replace(w,"\\$1").replace(v,y)+r;u=new RegExp("^"+u,"i")}Pe[a]={type:f,pattern:u,boards:t.boards,fid:a,hidden:t.hidden,color:t.color,top:t.top,hits:0}}}catch($){alert("There was an error processing one of the filters: "+$+" in: "+t.pattern)}}}}function P(){var e,t,a,i,n,o,s;for(i={},o=$.id("filter-list").children,e=0;t=o[e];++e)a={active:$.cls("filter-active",t)[0].checked?1:0,pattern:$.cls("filter-pattern",t)[0].value,boards:$.cls("filter-boards",t)[0].value,hidden:$.cls("filter-hide",t)[0].checked?1:0,top:$.cls("filter-top",t)[0].checked?1:0},(s=$.cls("filter-color",t)[0]).hasAttribute("data-nocolor")||(a.color=s.style.backgroundColor),i[e]=a;i[0]?localStorage.setItem("catalog-filters",JSON.stringify(i)):localStorage.removeItem("catalog-filters"),(n=$.id("filters-msg")).innerHTML="Done",n.className="msg-ok",n.style.display="inline",setTimeout(function(){n.style.display="none"},2e3),_(),ye(),K()}function J(){$.id("filter-rgb-ok").style.backgroundColor=this.value}function U(e,t){var a,i,n,o;return(i=document.createElement("tr")).id="filter-"+t,a=document.createElement("td"),(n=document.createElement("span")).setAttribute("data-up",t),n.className="pointer",n.innerHTML="↑",a.appendChild(n),i.appendChild(a),a=document.createElement("td"),(o=document.createElement("input")).type="checkbox",o.checked=!!e.active,o.className="filter-active",a.appendChild(o),i.appendChild(a),a=document.createElement("td"),(o=document.createElement("input")).type="text",o.value=e.pattern,o.className="filter-pattern",a.appendChild(o),i.appendChild(a),a=document.createElement("td"),(o=document.createElement("input")).type="text",o.value=e.boards,o.className="filter-boards",a.appendChild(o),i.appendChild(a),a=document.createElement("td"),(n=document.createElement("span")).id="filter-color-"+t,n.title="Change Color",n.className="button clickbox filter-color",e.color?n.style.background=e.color:(n.setAttribute("data-nocolor","1"),n.innerHTML="∕"),a.appendChild(n),i.appendChild(a),a=document.createElement("td"),(o=document.createElement("input")).type="checkbox",o.checked=!!e.hidden,o.className="filter-hide",a.appendChild(o),i.appendChild(a),a=document.createElement("td"),(o=document.createElement("input")).type="checkbox",o.checked=!!e.top,o.className="filter-top",a.appendChild(o),i.appendChild(a),a=document.createElement("td"),(n=document.createElement("span")).setAttribute("data-target",t),n.className="pointer",n.innerHTML="×",a.appendChild(n),i.appendChild(a),(a=document.createElement("td")).id="fhc-"+t,a.className="filter-hits",i.appendChild(a),i}function Y(e){var t=$.id("filter-color-"+Ae.getAttribute("data-target"));!0===e?(t.setAttribute("data-nocolor","1"),t.innerHTML="∕",t.style.background=""):(t.removeAttribute("data-nocolor"),t.innerHTML="",t.style.background=this.style.backgroundColor),B()}function q(){Y(!0)}function X(){var e={active:1,pattern:"",boards:"",color:"",hidden:0,top:0,hits:0};$.id("filter-list").appendChild(U(e,j()))}function j(){var e,t,a,i=$.id("filter-list").children;if(i.length){for(a=0,e=0;t=i[e];++e)(t=+t.id.slice(7))>a&&(a=t);return a+1}return 0}function z(e){var t=$.id("filter-"+e.getAttribute("data-target"));t.parentNode.removeChild(t)}function V(e,t,a){var i,n="data-"+t;"0"==e.getAttribute(n)?(e.setAttribute(n,"1"),$.addClass(e,"active"),e.innerHTML="✔",a&&((i=$.cls("filter-"+a,e.parentNode.parentNode)[0]).setAttribute("data-"+a,"0"),$.removeClass(i,"active"),i.innerHTML="")):(e.setAttribute(n,"0"),$.removeClass(e,"active"),e.innerHTML="")}function K(){var e,t,a=$.id("filter-list").children;for(e=0;t=a[e];++e)$.id("fhc-"+t.id.slice(7)).innerHTML=Pe[e]?"x"+Pe[e].hits:""}function G(e){return e&&$.hasClass(e,"hidden")}function Q(){var e,t,a;UA.hasWebStorage?((e=$.id("theme"))||(e=FC.panelHTML.build("theme","panel hidden")),a=(a=localStorage.getItem("catalog-theme"))?JSON.parse(a):{},$.id("theme-nobinds").checked=!!a.nobinds,$.id("theme-nospoiler").checked=!!a.nospoiler,$.id("theme-newtab").checked=!!a.newtab,$.id("theme-tw").checked=ze,$.id("theme-ddn").checked=Ve,a.css&&($.id("theme-css").value=a.css),$.on($.id("theme-save"),"click",ie),$.on($.id("theme-close"),"click",Z),$.id("theme-msg").style.display="none",e.style.top=window.pageYOffset+60+"px",$.removeClass(e,"hidden"),(t=$.tag("input",e)[0])&&t.focus(),ee(),document.dispatchEvent(new CustomEvent("4chanCatalogThemeEditorReady"))):alert("Your browser doesn't support Local Storage")}function Z(){$.off($.id("theme-save"),"click",ie),$.off($.id("theme-close"),"click",Z),$.addClass($.id("theme"),"hidden"),ee()}function ee(){$.toggleClass($.id("backdrop"),"hidden")}function te(){var e;UA.hasWebStorage&&(e=localStorage.getItem("catalog-theme"))&&(_e=JSON.parse(e))}function ae(e,t){e.nobinds?_e.nobinds!=e.nobinds&&$.off(document,"keyup",x):_e.nobinds!=e.nobinds&&$.on(document,"keyup",x),e.nospoiler?$.addClass(document.body,"reveal-img-spoilers"):$.removeClass(document.body,"reveal-img-spoilers"),t||De.applyCSS(e),document.dispatchEvent(new CustomEvent("4chanCatalogThemeApplied"))}function ie(){var a,i,n,o,s,l={};$.id("theme-nobinds").checked&&(l.nobinds=!0),$.id("theme-nospoiler").checked&&(l.nospoiler=!0),$.id("theme-newtab").checked&&(l.newtab=!0),n=$.id("theme-tw").checked,o=$.id("theme-ddn").checked,s=(s=localStorage.getItem("4chan-settings"))?JSON.parse(s):{},n!=ze&&(n?(ThreadWatcher.init(),s.disableAll=!1):ThreadWatcher.unInit()),o!=Ve&&(o?(e(),s.disableAll=!1):t()),s.threadWatcher=n,s.dropDownNav=o,localStorage.setItem("4chan-settings",JSON.stringify(s)),ze=n,Ve=o,""!==(i=$.id("theme-css").value)&&(l.css=i),ae(l),localStorage.removeItem("catalog-theme");for(a in l){localStorage.setItem("catalog-theme",JSON.stringify(l));break}_e=l,ye(),Z()}function ne(e){var t,a,i=!1,n=0;if(a=localStorage.getItem(e)){n=+Object.keys(Re.threads).pop(),a=JSON.parse(a);for(t in a)!Re.threads[t]&&t=0;e--)a[t=Be[e]]=Fe[t];localStorage.setItem("catalog-settings",JSON.stringify(a))}}function re(e,t){var a="";e?(Le.selectedIndex=1,a="extended-",Fe.extended=!0):(Le.selectedIndex=0,Fe.extended=!1),Fe.large?a+="large":a+="small",Se.className=a,t||le()}function de(e,t){var a=Fe.extended?"extended-":"";e?(Ee.selectedIndex=1,a+="large",Fe.large=!0):(Ee.selectedIndex=0,a+="small",Fe.large=!1),Se.className=a,t||(le(),ye())}function ce(e,t){var a={alt:0,absdate:1,date:2,r:3};a[e]!==undefined?(We.selectedIndex=a[e],Fe.orderby=e):(We.selectedIndex=0,Fe.orderby="date"),t||(le(),ye())}function he(){re("on"==Le.options[Le.selectedIndex].value)}function ue(){ce(We.options[We.selectedIndex].value)}function pe(){de("large"==Ee.options[Ee.selectedIndex].value)}function me(){"date"==Fe.orderby?ce("alt"):"alt"==Fe.orderby?ce("r"):"r"==Fe.orderby?ce("absdate"):ce("date")}function fe(e){var t=Fe.orderby;"date"==t?e.sort(function(e,t){return e.id>t.id?-1:e.id(t=t.entry.r||0)?-1:et.entry.b?1:0}):e.sort(function(e,t){return(e=e.entry.lr.id)>(t=t.entry.lr.id)?-1:e"+t.sub+"",t.teaser&&(o+=": "+t.teaser)):o=t.teaser,et){if(!qe[e])continue;++Xe}else if(Ze){if(!Ze.test(o)&&!Ze.test(t.file))continue}else{if(qe[e]){++Xe;continue}if(Ye[e]>=0)n=i=!0;else{s=t.capcode?(t.trip||"")+"!#"+t.capcode:t.trip;for(d in Pe)if(0==(l=Pe[d]).type&&(l.pattern.test(o)||l.pattern.test(t.file))||1==l.type&&l.pattern.test(s)||2==l.type&&l.pattern.test(t.author)){if(l.hidden){++c,l.hits+=1;continue e}a=l,i=!!l.top,l.hits+=1;break}}}Ye[e]>=0&&(n=i=!0),r.push({id:e,entry:t,pinned:n,onTop:i,hl:a})}return je=c,r}function be(e){var t,a,i,n,o,l,r,d,c,h,u,p,m,f,g,b,v,y,w,$,C,k,N,T,x,S,M,L,E;for(m="//boards."+$L.d(Re.slug)+"/"+Re.slug+"/thread/",f="i.4cdn.org/"+Re.slug+"/",x=!Fe.large,b=_e.newtab?'target="_blank" ':"",h=Re.custom_spoiler?Fe.imgspoiler+"-"+Re.slug+Re.custom_spoiler+".png":Fe.imgspoiler+".png",p="",w="",$="",t=0;o=e[t];++t){if(i=o.id,n=o.entry,r=o.hl,d=o.onTop,c=o.pinned,n.sub?(y=""+n.sub+"",n.teaser&&(y+=": "+n.teaser)):y=n.teaser,l='
    ',ze&&(v=i+"-"+Re.slug,l+='':'title="Watch" class="watchIcon">')),l+="',n.sticky||n.closed||n.capcodereps){if(l+='
    ',n.sticky&&(l+=''),n.closed&&(l+=''),n.capcodereps)for(S=n.capcodereps.split(","),a=0;M=S[a];++a)(L=He[M])&&(l+='');l+="
    "}l+='
    ',n.bumplimit?l+="R: "+n.r+"":l+="R: "+n.r+"",c&&((u=n.r-Ye[i])>0?(l+=" (+"+u+")",Ye[i]=n.r):l+="(+0)"),n.i&&(n.imagelimit?l+=" / I: "+n.i+"":l+=" / I: "+n.i+""),d&&(E=s(i))>=0&&(l+=" / P: "+E+""),l+='\u25b6',l+="
    ",y&&(l+='
    "),window.partyHats?l='
    '+l+'
    ':l+="
    ",n.sticky?$+=l:d?w+=l:p+=l}return w=$+w,Ze&&""===p&&""===w?p='
    Nothing Found
    ':w?p=w+p+'
    ':p+='
    ',p}function ve(e){var t,a,i,n,o,s,l,r,d,c,h,u,p,m;for(h="//boards."+$L.d(Re.slug)+"/"+Re.slug+"/thread/",u=_e.newtab?'target="_blank" ':"",c="",p="",t=0;n=e[t];++t)a=n.id,i=n.entry,s=n.hl,l=n.onTop,r=n.pinned,o=''+(m="')+'\xbb'+m+i.sub+'',i.bumplimit?o+=""+i.r+"":o+=i.r,r&&((d=i.r-Ye[a])>0?(o+=" (+"+d+")",Ye[a]=i.r):o+="(+0)"),o+=''+i.date+'\u25b6',l?p+=o:c+=o;return Ze&&""===c&&""===p?c='
    Nothing Found
    ':p?c=p+c+'
    ':c+='
    ',c=''+c+"
    SubjectRepliesDate
    "}function ye(){var e,t,a,i;if(0!==Re.count){Se.hasChildNodes()&&((t=document.getElementById("th-tip"))&&document.body.removeChild(t),Se.textContent=""),Xe=0,je=0;for(a in Pe)Pe[a].hits=0;fe(i=ge()),window.text_only?Se.innerHTML=ve(i):Se.innerHTML=be(i);for(e in Ye){localStorage.setItem("4chan-pin-"+Re.slug,JSON.stringify(Ye));break}k(je),C(Xe)}}function we(e){var t=e.target;($.hasClass(t,"thumb")||window.text_only&&$.hasClass(t,"txt-date"))&&(clearTimeout(Ue),Je&&ke(),Ue=setTimeout(Ce,Fe.tipdelay,t))}function $e(){clearTimeout(Ue),Je&&ke()}function Ce(e){var t,a,i,n,o,l,r,d,c,h,u,p,m;t=Date.now()/1e3,n=e.getBoundingClientRect(),o=document.documentElement.offsetWidth,(d=e.getAttribute("data-id"))&&(c=Re.threads[d],r=(r=s(d))?'Page '+r+"":"",a=c.sub&&!window.text_only?''+c.sub+"":"Posted",a+=' by '+(c.author||Re.anon),c.trip&&(a+=' '+c.trip+""),c.capcode&&(a+=" ## "+He[c.capcode]),a+=" ",Re.flags&&c.country&&(a+='
    '),a+=''+Ne(t-c.date)+" ago"+r,(!Fe.extended&&c.teaser||window.text_only)&&(a+='"),c.lr.date&&(a+='
    Last reply by '+c.lr.author,c.lr.trip&&(a+=' '+c.lr.trip+""),c.lr.capcode&&(a+=" ## "+c.lr.capcode.charAt(0).toUpperCase()+c.lr.capcode.slice(1)),c.lr.date?a+=' '+Ne(t-c.lr.date)+" ago":a+=""),(i=document.createElement("div")).id="post-preview",i.innerHTML=a,document.body.appendChild(i),m=o-n.right<(0|.3*o)?n.left-i.offsetWidth-5:n.left+n.width+5,p=document.documentElement.clientHeight,(h=(u=n.top+i.offsetHeight)>p?n.top-(u-p)-20:n.top)<0&&(h=3),(l=i.style).left=m+window.pageXOffset+"px",l.top=h+window.pageYOffset+"px",Je=!0)}function ke(){document.body.removeChild($.id("post-preview")),Je=!1}function Ne(e,t){var a,i,n;return e<2?"less than a second":t&&e<300?(0|e)+" seconds":e<60?(0|e)+" seconds":e<3600?(a=0|e/60)>1?a+" minutes":"one minute":e<86400?(i=(a=0|e/3600)>1?a+" hours":"one hour",(n=0|e/60-60*a)>1&&(i+=" and "+n+" minutes"),i):(i=(a=0|e/86400)>1?a+" days":"one day",(n=0|e/3600-24*a)>1&&(i+=" and "+n+" hours"),i)}var Te,xe,Se,Me,Le,Ee,We,Ae,Ie,De=this,Fe={orderby:"alt",large:!1,extended:!0,imgdel:"//s.4cdn.org/image/filedeleted-res.gif",imgspoiler:"//s.4cdn.org/image/spoiler",nofile:"//s.4cdn.org/image/nofile.png",smallsize:150,tipdelay:250,filterColors:[["#E0B0FF","#F2F3F4","#7DF9FF","#FFFF00"],["#FBCEB1","#FFBF00","#ADFF2F","#0047AB"],["#00A550","#007FFF","#AF0A0F","#B5BD68"]]},He={admin:"Administrator",mod:"Moderator",developer:"Developer",manager:"Manager",founder:"Founder",verified:"Verified"},Oe={83:h,82:d,88:me},Re={},Be=["orderby","large","extended"],_e={},Pe={},Je=!1,Ue=null,Ye={},qe={},Xe=0,je=0,ze=!1,Ve=!1,Ke=!1,Ge=!1,Qe=!1,Ze=!1,et=!1;window.devicePixelRatio>=2&&(Fe.imgdel.replace(".","@2x."),Fe.nofile.replace(".","@2x.")),UA.init(),te(),De.init=function(){var t,i,o,s;FC.hasMobileLayout=checkMobileLayout(),ae(_e,!0),Se=$.id("threads"),Me=$.id("qf-ctrl"),Le=$.id("teaser-ctrl"),Ee=$.id("size-ctrl"),We=$.id("order-ctrl"),$.on(Me,"click",u),$.on($.id("filters-clear-hidden"),"click",S),$.on($.id("filters-clear-hidden-bottom"),"click",S),$.on($.id("qf-clear"),"click",u),$.on($.id("settingsWindowLink"),"click",Q),$.on($.id("settingsWindowLinkBot"),"click",Q),$.on($.id("settingsWindowLinkMobile"),"click",Q),$.on($.id("filters-ctrl"),"click",O),$.on(Le,"change",he),$.on(Ee,"change",pe),$.on(We,"change",ue),$.on(Se,"mouseover",we),$.on(Se,"mouseout",$e),$.on($.id("togglePostFormLinkMobile"),"click",n),$.on(document,"click",L),se(),g(),a(),UA.hasContextMenu&&f(),window.Config={},UA.hasWebStorage&&((t=localStorage.getItem("4chan-settings"))?(t=JSON.parse(t),window.Config=t,t.disableAll||(CustomMenu.initCtrl(t.dropDownNav,t.classicNav),t.filter&&(ThreadWatcher.hasFilters=!0),t.threadWatcher&&(ze=!0,ThreadWatcher.init()),t.customMenu&&CustomMenu.apply(t.customMenuList),!1===t.dropDownNav||FC.hasMobileLayout||(Ve=!0,Ke=t.classicNav,Ge=t.autoHideNav,e()),Qe=t.altCaptcha)):UA.isMobileDevice&&!FC.hasMobileLayout?(Ve=!0,e()):CustomMenu.initCtrl(!1,!1),window.css_event&&"_special"===xe&&(s=window["fc_"+window.css_event+"_init"])&&s()),(i=document.forms.post.flag)&&(o=$.readCookie("4chan_flag"))&&(i=i.querySelector('option[value="'+o+'"]'))&&i.setAttribute("selected","selected"),ce(Fe.orderby,!0),de(Fe.large,!0),re(Fe.extended,!0),UA.dispatchEvent("4chanMainInit")},De.loadCatalog=function(e){var t;Re=e,$.addClass(document.body,xe.toLowerCase().replace(/ /g,"_")),l(),_(),oe(),UA.hasSessionStorage&&!location.hash&&(t=sessionStorage.getItem("4chan-catalog-search"))?Re.slug!=sessionStorage.getItem("4chan-catalog-search-board")&&(sessionStorage.removeItem("4chan-catalog-search"),sessionStorage.removeItem("4chan-catalog-search-board"),t=null):location.hash&&(t=location.hash.match(/#s=(.+)/))&&(t=decodeURIComponent(t[1].replace(/\+/g," "))),t?(u(),$.id("qf-box").value=t,p()):ye()},De.applyCSS=function(e,t,a){var i,n;e||(e=_e),t!==undefined&&((n=$.readCookie(t))||(n="nws_style"==t?"Yotsuba New":"Yotsuba B New"),Te=t,window.css_event&&localStorage.getItem("4chan_stop_css_event")!==`${window.css_event}-${window.css_event_v}`?(xe="_special",n=window.css_event):xe=n,(i=document.createElement("link")).type="text/css",i.id="base-css",i.rel="stylesheet",i.setAttribute("href","//s.4cdn.org/css/catalog_"+n.toLowerCase().replace(/ /g,"_")+"."+a+".css"),document.head.insertBefore(i,$.id("mobile-css"))),(i=$.id("custom-css"))&&document.head.removeChild(i),e.css&&((i=document.createElement("style")).type="text/css",i.id="custom-css",i.styleSheet?i.styleSheet.cssText=e.css:i.innerHTML=e.css,document.head.appendChild(i))}},Filter={};Filter.init=function(){this.entities=document.createElement("div"),Filter.load()},Filter.match=function(e,t){var a,i,n,o,s;for(s=!1,o=Filter.activeFilters,a=0;n=o[a];++a)if(n.boards[t])if(0==n.type){if(n.pattern===e.trip){s=!0;break}}else if(1==n.type){if(n.pattern===e.name){s=!0;break}}else if(2==n.type&&e.com){if(i===undefined&&(this.entities.innerHTML=e.com.replace(/
    /g,"\n").replace(/[<[^>]+>/g,""),i=this.entities.textContent),n.pattern.test(i)){s=!0;break}}else if(4==n.type){if(n.pattern===e.id){s=!0;break}}else if(5==n.type){if(n.pattern.test(e.sub)){s=!0;break}}else if(6==n.type&&n.pattern.test(e.filename)){s=!0;break}return s},FC.getDocTopOffset=function(){return window.Config.dropDownNav&&!window.Config.autoHideNav?$.id(window.Config.classicNav?"boardNavDesktop":"boardNavMobile").offsetHeight:0},Filter.load=function(){var e,t,a,i,n,o,s,l,r,d,c,h,u,p,m,f,g;if(this.activeFilters=[],i=localStorage.getItem("4chan-filters")){i=JSON.parse(i),s=new RegExp("(\\"+["/",".","*","+","?","(",")","[","]","{","}","\\","^","$"].join("|\\")+")","g"),l=/^\/(.*)\/(i?)$/,r="(?=.*\\b",d="\\b)",h=/\\\*/g,u="[^\\s]*";try{for(o=0;a=i[o];++o)if(a.active&&""!==a.pattern){if(a.boards)for(g=a.boards.split(/[^a-z0-9]+/i),p={},e=0;t=g[e];++e)p[t]=!0;else p=!1;if(n=a.pattern,a.type&&1!=a.type&&4!=a.type)if(f=n.match(l))m=new RegExp(f[1],f[2]);else if('"'==n[0]&&'"'==n[n.length-1])m=new RegExp(n.slice(1,-1).replace(s,"\\$1"));else{for(m="",e=0,t=(c=n.split(" ")).length;e
    ':"")+"Thread Watcher"+(UA.hasCORS?'
    ':""),this.listNode=document.createElement("ul"),this.listNode.id="watchList",this.load(),this.build(),e.appendChild(this.listNode),document.body.appendChild(e),e.addEventListener("mouseup",this.onClick,!1),Draggable.set($.id("twHeader")),window.addEventListener("storage",this.syncStorage,!1),!FC.hasMobileLayout&&this.canAutoRefresh()&&this.refresh()},ThreadWatcher.unInit=function(){var e;(e=$.id("threadWatcher"))&&(e.removeEventListener("mouseup",this.onClick,!1),Draggable.unset($.id("twHeader")),window.removeEventListener("storage",this.syncStorage,!1),document.body.removeChild(e))},ThreadWatcher.toggleList=function(e){var t=$.id("threadWatcher");e&&e.preventDefault(),ThreadWatcher.canAutoRefresh()&&ThreadWatcher.refresh(),"none"==t.style.display?(t.style.top=window.pageYOffset+30+"px",t.style.display=""):t.style.display="none"},ThreadWatcher.syncStorage=function(e){var t;e.key&&"4chan"==(t=e.key.split("-"))[0]&&"watch"==t[1]&&e.newValue!=e.oldValue&&(ThreadWatcher.load(),ThreadWatcher.build())},ThreadWatcher.load=function(){var e;(e=localStorage.getItem("4chan-watch"))&&(this.watched=JSON.parse(e)),(e=localStorage.getItem("4chan-watch-bl"))&&(this.blacklisted=JSON.parse(e))},ThreadWatcher.build=function(){var e,t,a,i;e="";for(a in this.watched)e+='
  • × ':(i=[],this.watched[a][3]&&i.push("archivelink"),this.watched[a][4]&&(i.push("hasYouReplies"),e+=' title="This thread has replies to your posts"'),this.watched[a][2]?e+=' class="'+(i[0]?i.join(" ")+" ":"")+'hasNewReplies">('+this.watched[a][2]+") ":e+=(i[0]?'class="'+i.join(" ")+'"':"")+">"),e+="/"+t[1]+"/ - "+this.watched[a][0]+"
  • ";ThreadWatcher.listNode.innerHTML=e},ThreadWatcher.onClick=function(e){var t=e.target;t.hasAttribute("data-id")?ThreadWatcher.toggle(t.getAttribute("data-id"),t.getAttribute("data-board")):"twPrune"!=t.id||ThreadWatcher.isRefreshing?"twClose"==t.id&&ThreadWatcher.toggleList():ThreadWatcher.refreshWithAutoWatch()},ThreadWatcher.generateLabel=function(e,t,a){var i;return i=(i=e)?i.slice(0,this.charLimit):(i=t)?i.replace(/(?:
    )+/g," ").replace(/<[^>]*?>/g,"").slice(0,this.charLimit):"No."+a},ThreadWatcher.toggle=function(e,t,a,i,n){var o,s,l,r;o=e+"-"+t,r=$.id("leaf-"+e),this.watched[o]?(delete this.watched[o],r&&(r.className="watchIcon",r.title="Watch")):(s=ThreadWatcher.generateLabel(a,i,e),l=n||e,this.watched[o]=[s,l,0],r.className="unwatchIcon",r.title="Unwatch"),this.save(),this.load(),this.build()},ThreadWatcher.addRaw=function(e,t){var a,i;a=e.no+"-"+t,this.watched[a]||(i=ThreadWatcher.generateLabel(e.sub,e.com,e.no),this.watched[a]=[i,0,0])},ThreadWatcher.save=function(){var e;ThreadWatcher.sortByBoard(),localStorage.setItem("4chan-watch",JSON.stringify(ThreadWatcher.watched));for(e in ThreadWatcher.blacklisted){localStorage.setItem("4chan-watch-bl",JSON.stringify(ThreadWatcher.blacklisted));break}},ThreadWatcher.sortByBoard=function(){var e,t,a,i,n;t=ThreadWatcher,i={},n=[];for(a in t.watched)n.push(a);for(n.sort(function(e,t){return(e=e.split("-")[1])<(t=t.split("-")[1])?-1:e>t?1:0}),e=0;a=n[e];++e)i[a]=t.watched[a];t.watched=i},ThreadWatcher.canAutoRefresh=function(){var e;return!!(e=localStorage.getItem("4chan-tw-timestamp"))&&Date.now()-+e>=6e4},ThreadWatcher.setRefreshTimestamp=function(){localStorage.setItem("4chan-tw-timestamp",Date.now())},ThreadWatcher.refreshWithAutoWatch=function(){var e,t,a,i,n;if(this.hasFilters){for(Filter.load(),n={},a=0,e=0;t=Filter.activeFilters[e];++e)if(t.auto&&t.boards)for(i in t.boards)n[i]||(n[i]=!0,++a);a?($.id("twPrune").className="icon rotateIcon",this.isRefreshing=!0,this.fetchCatalogs(n,a)):this.refresh()}else this.refresh()},ThreadWatcher.fetchCatalogs=function(e,t){var a,i,n,o;n={},o={count:t},a=0;for(i in e)setTimeout(ThreadWatcher.fetchCatalog,a,i,n,o),a+=200},ThreadWatcher.parseCatalogJSON=function(e){var t;try{t=JSON.parse(e)}catch(a){console.log(a),t=[]}return t},ThreadWatcher.fetchCatalog=function(e,t,a){var i;(i=new XMLHttpRequest).open("GET","//a.4cdn.org/"+e+"/catalog.json"),i.onload=function(){a.count--,t[e]=ThreadWatcher.parseCatalogJSON(this.responseText),a.count||ThreadWatcher.onCatalogsLoaded(t)},i.onerror=function(){a.count--,a.count||ThreadWatcher.onCatalogsLoaded(t)},i.send(null)},ThreadWatcher.onCatalogsLoaded=function(e){var t,a,i,n,o,s,l,r,d;$.id("twPrune").className="icon rotateIcon",this.isRefreshing=!1,d={};for(i in e)for(o=e[i],t=0;n=o[t];++t)for(s=n.threads,a=0;l=s[a];++a)r=l.no+"-"+i,this.blacklisted[r]?d[r]=1:Filter.match(l,i)&&this.addRaw(l,i);this.blacklisted=d,this.build(!0),this.refresh()},ThreadWatcher.refresh=function(){var e,t,a,i,n;if(i=$.id("watchList").children.length){e=t=0,(n=$.id("twPrune")).className="icon rotateIcon",ThreadWatcher.isRefreshing=!0,ThreadWatcher.setRefreshTimestamp();for(a in ThreadWatcher.watched)setTimeout(ThreadWatcher.fetch,t,a,++e==i?n:null),t+=200}},ThreadWatcher.onRefreshEnd=function(e){e.className="icon refreshIcon",this.isRefreshing=!1,this.save(),this.load(),this.build()},ThreadWatcher.parseThreadJSON=function(e){var t;try{t=JSON.parse(e).posts}catch(a){console.log(a),t=[]}return t},ThreadWatcher.getTrackedReplies=function(e,t){var a=null;return(a=localStorage.getItem("4chan-track-"+e+"-"+t))&&(a=JSON.parse(a)),a},ThreadWatcher.fetch=function(e,t){var a,i,n;if(n=$.id("watch-"+e),-1==ThreadWatcher.watched[e][1])return delete ThreadWatcher.watched[e],n.parentNode.removeChild(n),void(t&&ThreadWatcher.onRefreshEnd(t));a=e.split("-"),(i=new XMLHttpRequest).onload=function(){var i,n,o,s,l,r,d,c,h;if(200==this.status){for(o=ThreadWatcher.parseThreadJSON(this.responseText),s=ThreadWatcher.watched[e][1],n=0,ThreadWatcher.watched[e][4]?l=null:(l=ThreadWatcher.getTrackedReplies(a[1],a[0]))&&(r=document.createElement("div")),i=o.length-1;i>=1&&!(o[i].no<=s);i--)if(++n,l){if(r.innerHTML=o[i].com,!(d=$.cls("quotelink",r))[0])continue;for(h=0;c=d[h];++h)if(l[c.textContent]){ThreadWatcher.watched[e][4]=1,l=null;break}}n>ThreadWatcher.watched[e][2]&&(ThreadWatcher.watched[e][2]=n),o[0].archived&&(ThreadWatcher.watched[e][3]=1)}else 404==this.status&&(ThreadWatcher.watched[e][1]=-1);t&&ThreadWatcher.onRefreshEnd(t)},t&&(i.onerror=i.onload),i.open("GET","//a.4cdn.org/"+a[1]+"/thread/"+a[0]+".json"),i.send(null)},ThreadWatcher.linkToThread=function(e,t,a){return"//"+location.host+"/"+t+"/thread/"+e+(a>0?"#p"+a:"")};var Draggable={el:null,key:null,scrollX:null,scrollY:null,dx:null,dy:null,right:null,bottom:null,set:function(e){e.addEventListener("mousedown",Draggable.startDrag,!1)},unset:function(e){e.removeEventListener("mousedown",Draggable.startDrag,!1)},startDrag:function(e){var t,a,i;this.parentNode.hasAttribute("data-shiftkey")&&!e.shiftKey||(e.preventDefault(),t=Draggable,a=document.documentElement,t.el=this.parentNode,t.key=t.el.getAttribute("data-trackpos"),i=t.el.getBoundingClientRect(),t.dx=e.clientX-i.left,t.dy=e.clientY-i.top,t.right=a.clientWidth-i.width,t.bottom=a.clientHeight-i.height,"fixed"!=getComputedStyle(t.el,null).position?(t.scrollX=window.pageXOffset,t.scrollY=window.pageYOffset):t.scrollX=t.scrollY=0,t.offsetTop=FC.getDocTopOffset(),document.addEventListener("mouseup",t.endDrag,!1),document.addEventListener("mousemove",t.onDrag,!1))},endDrag:function(){document.removeEventListener("mouseup",Draggable.endDrag,!1),document.removeEventListener("mousemove",Draggable.onDrag,!1),Draggable.key&&window.Config&&(window.Config[Draggable.key]=Draggable.el.style.cssText,localStorage.setItem("4chan-settings",JSON.stringify(window.Config))),delete Draggable.el},onDrag:function(e){var t,a,i;t=e.clientX-Draggable.dx+Draggable.scrollX,a=e.clientY-Draggable.dy+Draggable.scrollY,i=Draggable.el.style,t<1?(i.left="0",i.right=""):Draggable.rightEdit]',!CustomMenu.dropDownNav||CustomMenu.classicNav||FC.hasMobileLayout?((i=$.cls("boardList"))[0]&&i[0].appendChild(a),i[1]&&i[1].appendChild(a.cloneNode(!0))):(i=$.id("boardSelectMobile").parentNode).insertBefore(a,i.lastChild)},CustomMenu.reset=function(){var e,t,a,i,n;for(a=$.cls("boardList"),i=$.cls("customBoardList"),n=$.cls("show-all-boards"),e=0;t=n[e];++e)t.removeEventListener("click",CustomMenu.reset,!1);for(e=i.length-1;t=i[e];e--)a[e].style.display=null,t.parentNode.removeChild(t)},CustomMenu.apply=function(e){var t,a,i,n,o,s,l;if(e){for(s=e.split(/[^0-9a-z]/i),(l=document.createElement("span")).className="customBoardList",t=0;n=s[t];++t)t?l.appendChild(document.createTextNode(" / ")):l.appendChild(document.createTextNode("[")),(a=document.createElement("a")).textContent=n,a.href="//boards."+$L.d(n)+"/"+n+("f"!==n?"/catalog":""),l.appendChild(a);if(l.appendChild(document.createTextNode("]")),!CustomMenu.dropDownNav||CustomMenu.classicNav||FC.hasMobileLayout){for(l.appendChild(document.createTextNode(" [")),(a=document.createElement("a")).textContent="\u2026",a.title="Show all",a.className="show-all-boards pointer",l.appendChild(a),l.appendChild(document.createTextNode("] ")),i=l.cloneNode(!0),o=$.cls("boardList"),t=0;a=o[t];++t)a.style.display="none",a.parentNode.insertBefore(t?i:l,a);for(o=$.cls("show-all-boards"),t=0;a=o[t];++t)a.addEventListener("click",CustomMenu.reset,!1)}else(a=$.cls("customBoardList")[0])&&a.parentNode.removeChild(a),(o=$.id("boardSelectMobile"))&&o.parentNode.insertBefore(l,o.nextSibling)}else!CustomMenu.dropDownNav||CustomMenu.classicNav||FC.hasMobileLayout||(a=$.cls("customBoardList")[0])&&a.parentNode.removeChild(a)},CustomMenu.onClick=function(e){var t;(t=e.target)!=document&&(t.hasAttribute("data-close")?CustomMenu.closeEditor():t.hasAttribute("data-save")&&CustomMenu.save($.id("customMenu").hasAttribute("data-standalone")))},CustomMenu.showEditor=function(e){var t,a;(t=document.createElement("div")).id="customMenu",t.className="panel",t.setAttribute("data-close","1"),!0===e&&t.setAttribute("data-standalone","1"),t.innerHTML='
    Custom Board List
    ',document.body.appendChild(t),t.style.top=window.pageYOffset+(0|document.documentElement.clientHeight/2-t.offsetHeight/2)+"px",$.removeClass($.id("backdrop"),"hidden"),(a=CustomMenu.getConfig()).customMenuList&&($.id("customMenuBox").value=a.customMenuList),t.addEventListener("click",CustomMenu.onClick,!1)},CustomMenu.closeEditor=function(){var e;(e=$.id("customMenu"))&&(e.removeEventListener("click",CustomMenu.onClick,!1),document.body.removeChild(e),$.addClass($.id("backdrop"),"hidden"))},CustomMenu.save=function(e){var t,a;(t=$.id("customMenuBox"))&&!0===e&&(CustomMenu.apply(t.value),(a=CustomMenu.getConfig()).customMenu=!0,a.customMenuList=t.value,localStorage.setItem("4chan-settings",JSON.stringify(a))),CustomMenu.closeEditor()},CustomMenu.getConfig=function(){var e;return(e=localStorage.getItem("4chan-settings"))?JSON.parse(e):{}};var StickyNav={thres:5,pos:0,timeout:null,el:null,init:function(e){this.el=e?$.id("boardNavDesktop"):$.id("boardNavMobile"),$.addClass(this.el,"autohide-nav"),window.addEventListener("scroll",this.onScroll,!1)},destroy:function(e){this.el=e?$.id("boardNavDesktop"):$.id("boardNavMobile"),$.removeClass(this.el,"autohide-nav"),window.removeEventListener("scroll",this.onScroll,!1)},onScroll:function(){clearTimeout(StickyNav.timeout),StickyNav.timeout=setTimeout(StickyNav.checkScroll,50)},checkScroll:function(){var e;e=window.pageYOffset,Math.abs(StickyNav.pos-e)<=StickyNav.thres||(eSettings

    Options

    Shortcuts

    • R — Refresh current page
    • X — Reorder threads
    • S — Open search box, Esc to close
    • Shift LMB — Hide threads
    • Alt LMB — Pin threads
    • RMB — Threads context menu (Firefox only)

    Custom CSS

    ',"filters-protip":'
    Filters & Highlights Help

    Patterns

      • Matching whole words:
      • feel — will match "feel" but not "feeling". This search is case-insensitive.
      • AND operator:
      • feel girlfriend — will match "feel" AND "girlfriend" in any order.
      • OR operator:
      • feel|girlfriend — will match "feel" OR "girlfriend".
      • Mixing both operators:
      • girlfriend|boyfriend feel — matches "feel" AND "girlfriend", or "feel" AND "boyfriend".
      • Exact match search:
      • "that feel when" — place double quotes around the pattern to search for an exact string
      • Wildcards:
      • feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc…
      • idolm*ster — this can match "idolmaster" or "idolm@ster", etc…
      • Filtering by name or tripcode:
      • Prefix the pattern with # to search by tripcode: #!Ep8pui8Vw2
      • Prefix the pattern with ## to search by name: ##Anonymous
      • To filter by capcode: #!#admin, #!#mod, #!#developer
      • It is also possible to filter by regular expression:
      • /^(?=.*detachable)(?=.*hats).*$/i — AND operator.
      • /^(?!.*touhou).*$/i — NOT operator.
      • /^&gt;/ — threads starting with a quote (">" character as an html entity).
      • /^$/ — threads with no text.

    Controls

    • On — enables or disables the filter.
    • Boards — space separated list of boards on which the filter will be active. Leave blank to apply to all boards.
    • Hide — hides matched threads.
    • Top — puts matched threads on top of the list.
    ',"filter-palette":'
    Custom
    Close Clear
    ',filters:'
    Filters & Highlights
    Order On Pattern Boards Color Hide Top Del
    '};var PostMenu={activeBtn:null};PostMenu.open=function(e,t,a,i,n){var o,s,l,r,d;PostMenu.activeBtn!=e?(PostMenu.close(),e.parentNode.parentNode,s='
    • Report thread
    • '+(n?"Unpin":"Pin")+' thread
    • '+(i?"Unhide":"Hide")+" thread
    • ",a&&(s+='
    • '+(ThreadWatcher.watched[t+"-"+catalog.slug]?"Remove from":"Add to")+" watch list
    • "),(o=document.createElement("div")).id="post-menu",o.className="dd-menu",o.innerHTML=s+"
    ",l=e.getBoundingClientRect(),o.style.top=l.bottom+3+window.pageYOffset+"px",document.addEventListener("click",PostMenu.close,!1),$.addClass(e,"menuOpen"),PostMenu.activeBtn=e,UA.dispatchEvent("4chanPostMenuReady",{postId:t,isOP:!0,node:o.firstElementChild}),document.body.appendChild(o),(r=l.left+window.pageXOffset)>(d=document.documentElement.clientWidth-o.offsetWidth)-75&&(o.className+=" dd-menu-left"),r>d&&(r=d),o.style.left=r+"px"):PostMenu.close()},PostMenu.close=function(){var e;(e=$.id("post-menu"))&&(e.parentNode.removeChild(e),document.removeEventListener("click",PostMenu.close,!1),$.removeClass(PostMenu.activeBtn,"menuOpen"),PostMenu.activeBtn=null)}; \ No newline at end of file diff --git a/js/catalog.min.map b/js/catalog.min.map new file mode 100644 index 0000000..18f8996 --- /dev/null +++ b/js/catalog.min.map @@ -0,0 +1 @@ +{"version":3,"file":"catalog.min.1004.js","sources":["catalog.1004.js"],"names":["checkMobileLayout","mobile","desktop","window","matchMedia","matches","localStorage","getItem","$","id","offsetWidth","document","getElementById","cls","klass","root","getElementsByClassName","tag","getElementsByTagName","extend","destination","source","key","on","n","e","h","addEventListener","off","removeEventListener","readCookie","name","i","c","ca","cookie","split","charAt","substring","length","indexOf","decodeURIComponent","documentElement","classList","hasClass","el","contains","addClass","add","removeClass","remove","className","replace","toggleClass","UA","init","head","this","hasContextMenu","hasWebStorage","kv","setItem","removeItem","hasSessionStorage","sessionStorage","hasCORS","XMLHttpRequest","isMobileDevice","test","navigator","userAgent","dispatchEvent","detail","createEvent","initEvent","FC","showDropDownNav","top","bottom","hasClassicNav","createElement","innerHTML","appendChild","showThemeEditor","style","display","hasAutoHideNav","StickyNav","body","hideDropDownNav","removeChild","destroy","initGlobalMessage","msg","btn","thisTs","oldTs","hasMobileLayout","textContent","nextElementSibling","clear","setAttribute","title","getAttribute","opacity","toggleGlobalCatalogMessage","parentNode","insertBefore","togglePostFormMobile","initRecaptcha","getRegexSpecials","specials","RegExp","join","getThreadPage","tid","catalog","threads","b","pagesize","initStyleSwitcher","selector","nodes","ss","children","value","activeStyleSheet","selectedIndex","onStyleSheetChange","expires","Date","setTime","getTime","activeStyleGroup","toGMTString","refreshWindow","shiftKey","location","href","debounce","delay","fn","timeout","args","arguments","context","clearTimeout","setTimeout","apply","focusQuickfilter","$qfCtrl","clearQuickfilter","toggleQuickfilter","qfBox","qfcnt","hasAttribute","applyQuickfilter","keyCode","focus","regexEscape","qfstr","slug","quickFilterPattern","buildThreads","qf","buildContextMenu","ctxCmds","pin","toggleThreadPin","hide","toggleThreadHide","report","reportThread","clearPinnedThreads","onThreadContextClick","bindGlobalShortcuts","$threads","target","which","altKey","activeTheme","nobinds","processKeybind","pinnedThreads","r","JSON","stringify","hiddenMode","hiddenThreads","hiddenThreadsCount","setHiddenCount","setHiddenMode","state","setProcessedCount","type","num","label","count","setFilteredCount","height","altc","passEnabled","grecaptcha","altCaptcha","open","now","cmd","nodeName","keybinds","toggleHiddenThreads","preventDefault","onClick","t","ThreadWatcher","toggle","sub","teaser","lr","PostMenu","hasThreadWatcher","CustomMenu","showEditor","panelHidden","closeThemeEditor","closeFilters","closeFiltersHelp","closeFilterPalette","buildFilterPalette","j","table","palette","rows","cols","tr","td","foot","$filterPalette","options","filterColors","firstElementChild","selectFilterColor","showFilterPalette","picker","pos","getBoundingClientRect","cssText","left","clientWidth","showFiltersHelp","pageYOffset","onFiltersClick","addEmptyFilter","saveFilters","toggleFilter","deleteFilter","moveFilterUp","prev","previousElementSibling","onFiltersSearch","cnt","str","toLowerCase","showFilters","filtersPanel","rawFilters","filterList","filterId","panelHTML","build","clearFilterColor","filterSetCustomColor","parse","buildFilter","updateFilterHitCount","toggleBackdrop","loadFilters","activeFilters","rf","fid","v","w","wordcount","wordSepS","wordSepE","match","inner","words","rawPattern","pattern","orOp","orCluster","regexType","regexOrNorm","regexWc","replWc","active","boards","slice","push","hidden","color","hits","err","alert","f","checked","backgroundColor","filterRgbOk","filter","span","input","background","removeAttribute","getNextFilterId","max","xor","xorEle","attr","themePanel","theme","nospoiler","newtab","hasDropDownNav","css","saveTheme","loadTheme","customTheme","applyTheme","nocss","self","applyCSS","tw","ddn","extConfig","disableAll","unInit","threadWatcher","dropDownNav","loadThreadList","mod","ft","Object","keys","pop","loadStorage","loadSettings","settings","saveSettings","basicSettings","setExtended","mode","$teaserCtrl","extended","large","setLarge","$sizeCtrl","setOrder","order","o","alt","absdate","date","undefined","$orderCtrl","orderby","onTeaserChange","onOrderChange","onSizeChange","cycleOrder","sortThreadList","threadList","sort","a","entry","getFilteredThreads","hl","onTop","pinned","tripcode","af","filtered","threadloop","file","capcode","trip","author","filteredThreadsCount","formatImageThreads","k","item","thread","spoiler","rDiff","html","provider","contentUrl","pinhl","watchKey","topHtml","stickyHtml","ratio","maxSize","imgWidth","imgHeight","calcSize","capcodeReplies","capcodeReply","capcodeTitle","page","custom_spoiler","imgspoiler","watched","imgurl","tn_w","tn_h","smallsize","imgdel","nofile","sticky","closed","capcodereps","capcodeMap","bumplimit","imagelimit","partyHats","formatTextThreads","aTag","tip","hasChildNodes","text_only","onThreadMouseOver","tooltipTimeout","hasTooltip","hideTooltip","showTooltip","tipdelay","onThreadMouseOut","rect","docWidth","docHeight","anon","flags","country","getDuration","toUpperCase","right","width","clientHeight","offsetHeight","pageXOffset","delta","precise","tail","admin","developer","manager","founder","verified",83,82,88,"devicePixelRatio","val","showPostForm","initCtrl","classicNav","hasFilters","customMenu","customMenuList","autoHideNav","forms","post","flag","querySelector","loadCatalog","query","hash","style_group","css_version","rel","styleSheet","Filter","entities","load","board","com","filters","hit","filename","getDocTopOffset","regexWildcard","replaceWildcard","tmp","auto","listNode","charLimit","blacklisted","isRefreshing","toggleList","createTextNode","Draggable","set","syncStorage","canAutoRefresh","refresh","unset","newValue","oldValue","storage","tuid","linkToThread","refreshWithAutoWatch","generateLabel","lastReply","icon","save","addRaw","no","sortByBoard","sorted","time","setRefreshTimestamp","img","fetchCatalogs","to","catalogs","meta","fetchCatalog","parseCatalogJSON","data","console","log","xhr","onload","responseText","onCatalogsLoaded","onerror","send","pages","total","fetch","onRefreshEnd","parseThreadJSON","posts","getTrackedReplies","tracked","li","newReplies","trackedReplies","dummy","quotelinks","q","status","archived","host","scrollX","scrollY","dx","dy","handle","startDrag","doc","offs","clientX","clientY","getComputedStyle","position","offsetTop","endDrag","onDrag","cloneNode","lastChild","reset","full","custom","navs","cntBottom","boardList","nextSibling","closeEditor","standalone","getConfig","thres","onScroll","checkScroll","thisPos","Math","abs","filters-protip","filter-palette","activeBtn","pid","div","btnPos","limit","close","postId","isOP","node"],"mappings":"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"} \ No newline at end of file diff --git a/js/core-test.js b/js/core-test.js new file mode 100644 index 0000000..614cb25 --- /dev/null +++ b/js/core-test.js @@ -0,0 +1,2549 @@ +var $L = { + nws: {"aco":1,"b":1,"bant":1,"d":1,"e":1,"f":1,"gif":1,"h":1,"hc":1,"hm":1,"hr":1,"i":1,"ic":1,"pol":1,"r":1,"r9k":1,"s":1,"s4s":1,"soc":1,"t":1,"trash":1,"u":1,"wg":1,"y":1}, + blue: '4chan.org', red: '4chan.org', + d: function(b) { + return $L.nws[b] ? $L.red : $L.blue; + } +}; + +/** + * Captcha +*/ +var TCaptcha = { + node: null, + + frameNode: null, + imgCntNode: null, + bgNode: null, + fgNode: null, + msgNode: null, + sliderNode: null, + respNode: null, + reloadNode: null, + nextNode: null, + helpNode: null, + challengeNode: null, + + ticketCaptchaNode: null, + + challenge: null, + + reloadTs: null, + reloadTimeout: null, + expireTimeout: null, + frameTimeout: null, + + pcdBypassable: false, + + errorCb: null, + + path: '/captcha', + + ticketKey: '4chan-tc-ticket', + + domain: '4chan.org', + + failCd: 60, + + tabindex: null, + + hCaptchaSiteKey: '49d294fa-f15c-41fc-80ba-c2544c52ec2a', + + init: function(el, board, thread_id, tabindex) { + if (this.node) { + this.destroy(); + } + + if (tabindex) { + this.tabindex = tabindex; + } + + this.node = el; + + el.style.position = 'relative'; + el.style.width = '300px'; + + this.frameNode = null; + this.imgCntNode = this.buildImgCntNode(); + this.bgNode = this.buildImgNode('bg'); + this.fgNode = this.buildImgNode('fg'); + this.sliderNode = this.buildSliderNode(); + + this.respNode = this.buildRespField(); + this.reloadNode = this.buildReloadNode(board, thread_id); + this.nextNode = this.buildNextNode(); + this.helpNode = this.buildHelpNode(); + this.msgNode = this.buildMsgNode(); + this.challengeNode = this.buildChallengeNode(); + + el.appendChild(this.reloadNode); + el.appendChild(this.respNode); + el.appendChild(this.nextNode); + el.appendChild(this.helpNode); + + this.imgCntNode.appendChild(this.bgNode); + this.imgCntNode.appendChild(this.fgNode); + el.appendChild(this.imgCntNode); + + el.appendChild(this.sliderNode); + el.appendChild(this.msgNode); + el.appendChild(this.challengeNode); + + window.addEventListener('message', this.onFrameMessage); + }, + + destroy: function() { + let self = TCaptcha; + + if (!self.node) { + return; + } + + window.removeEventListener('message', self.onFrameMessage); + + clearTimeout(self.frameTimeout); + clearTimeout(self.reloadTimeout); + clearTimeout(self.expireTimeout); + + self.node.textContent = ''; + + self.node = null; + self.frameNode = null; + self.imgCntNode = null; + self.bgNode = null; + self.fgNode = null; + self.msgNode = null; + self.sliderNode = null; + self.respNode = null; + self.reloadNode = null; + self.nextNode = null; + self.helpNode = null; + self.challengeNode = null; + + self.ticketCaptchaNode = null; + + self.challenge = null; + + self.pcdBypassable = false; + + self.errorCb = null; + + self.reloadTs = null; + + self.onReloadCdDone = null; + }, + + setErrorCb: function(func) { + TCaptcha.errorCb = func; + }, + + toggleImgCntNode: function(flag) { + TCaptcha.imgCntNode.style.display = flag ? 'block' : 'none'; + }, + + getTicket: function() { + return localStorage.getItem(TCaptcha.ticketKey); + }, + + setTicket: function(val) { + if (val) { + localStorage.setItem(TCaptcha.ticketKey, val); + } + else if (val === false) { + localStorage.removeItem(TCaptcha.ticketKey); + } + }, + + buildFrameNode: function() { + let el = document.createElement('iframe'); + el.id = 't-frame'; + el.style.border = '0'; + el.style.width = '100%'; + el.style.height = '80px'; + el.style.marginTop = '2px'; + el.style.position = 'relative'; + el.style.display = 'block'; + el.onerror = TCaptcha.onFrameError; + return el; + }, + + buildImgCntNode: function() { + let el = document.createElement('div'); + el.id = 't-cnt'; + el.style.height = '80px'; + el.style.marginTop = '2px'; + el.style.position = 'relative'; + return el; + }, + + buildImgNode: function(id) { + let el = document.createElement('div'); + el.id = 't-' + id; + el.style.width = '100%'; + el.style.height = '100%'; + el.style.position = 'absolute'; + el.style.backgroundRepeat = 'no-repeat'; + el.style.backgroundPosition = 'top left'; + el.style.pointerEvents = 'none'; + return el; + }, + + buildMsgNode: function() { + let el = document.createElement('div'); + el.id = 't-msg'; + el.style.width = '100%'; + el.style.height = 'calc(100% - 20px)'; + el.style.position = 'absolute'; + el.style.top = '20px'; + el.style.textAlign = 'center'; + el.style.fontSize = '12px'; + el.style.filter = 'inherit'; + el.style.display = 'none'; + el.style.alignContent = 'center'; + return el; + }, + + buildRespField: function() { + let el = document.createElement('input'); + el.id = 't-resp'; + el.name = 't-response'; + el.placeholder = 'Type CAPTCHA here'; + el.setAttribute('autocomplete', 'off'); + el.type = 'text'; + el.style.width = '120px'; + el.style.boxSizing = 'border-box'; + el.style.textTransform = 'uppercase'; + el.style.fontSize = '11px'; + el.style.height = '18px'; + el.style.margin = '0'; + el.style.padding = '0 2px'; + el.style.fontFamily = 'monospace'; + el.style.verticalAlign = 'middle'; + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex + 2); + } + return el; + }, + + buildSliderNode: function() { + let el = document.createElement('input'); + el.id = 't-slider'; + el.setAttribute('autocomplete', 'off'); + el.type = 'range'; + el.style.width = '100%'; + el.style.boxSizing = 'border-box'; + el.style.visibility = 'hidden'; + el.style.margin = '0'; + el.style.transition = 'box-shadow 15s linear'; + el.style.boxShadow = '0 0 6px 4px #1d8dc4'; + el.style.position = 'relative'; + el.value = 0; + el.min = 0; + el.max = 100; + el.addEventListener('input', this.onSliderInput, false); + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex + 1); + } + return el; + }, + + buildChallengeNode: function() { + let el = document.createElement('input'); + el.name = 't-challenge'; + el.type = 'hidden'; + return el; + }, + + buildReloadNode: function(board, thread_id) { + let el = document.createElement('button'); + el.id = 't-load'; + el.type = 'button'; + el.style.fontSize = '11px'; + el.style.padding = '0'; + el.style.width = '90px'; + el.style.boxSizing = 'border-box'; + el.style.margin = '0 6px 0 0'; + el.style.verticalAlign = 'middle'; + el.style.height = '18px'; + el.textContent = 'Get Captcha'; + el.setAttribute('data-board', board); + el.setAttribute('data-tid', thread_id); + el.addEventListener('click', this.onReloadClick, false); + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex); + } + return el; + }, + + buildNextNode: function() { + let el = document.createElement('button'); + el.id = 't-next'; + el.type = 'button'; + el.style.fontSize = '11px'; + el.style.padding = '0'; + el.style.width = '40px'; + el.style.boxSizing = 'border-box'; + el.style.margin = '0 0 0 6px'; + el.style.verticalAlign = 'middle'; + el.style.height = '18px'; + el.style.display = 'none'; + el.textContent = 'Next'; + el.addEventListener('click', this.onNextClick, false); + return el; + }, + + buildHelpNode: function() { + let el = document.createElement('button'); + el.id = 't-help'; + el.type = 'button'; + el.style.fontSize = '11px'; + el.style.padding = '0'; + el.style.width = '20px'; + el.style.boxSizing = 'border-box'; + el.style.margin = '0 0 0 6px'; + el.style.verticalAlign = 'middle'; + el.style.height = '18px'; + el.textContent = '?'; + el.setAttribute('data-tip', 'Help'); + el.tabIndex = -1; + el.addEventListener('click', this.onHelpClick, false); + return el; + }, + + onHelpClick: function() { + let str = `- Only type letters and numbers displayed in the image. +- If needed, use the slider to align the image to make it readable. +- Make sure to not block any cookies set by 4chan.`; + alert(str); + }, + + toggleNextBtn: function(flag) { + TCaptcha.nextNode.style.display = flag ? 'inline-block' : 'none'; + }, + + onNextClick: function() { + let self = TCaptcha; + + let btn = self.reloadNode; + let board = btn.getAttribute('data-board'); + let thread_id = btn.getAttribute('data-tid'); + + let challenge = self.challengeNode.value; + let response = self.respNode.value; + + self.toggleNextBtn(false); + self.toggleReloadBtn(false, 'Loading'); + self.load(board, thread_id, challenge, response); + }, + + onReloadClick: function() { + let btn = TCaptcha.reloadNode; + let board = btn.getAttribute('data-board'); + let thread_id = btn.getAttribute('data-tid'); + TCaptcha.toggleNextBtn(false); + TCaptcha.toggleReloadBtn(false, 'Loading'); + TCaptcha.load(board, thread_id); + }, + + onFrameMessage: function(e) { + if (e.origin !== `https://sys.${TCaptcha.domain}`) { + return; + } + + if (e.data && e.data.twister) { + TCaptcha.destroyFrame(); + TCaptcha.buildFromJson(e.data.twister); + } + }, + + onFrameError: function(e) { + TCaptcha.unlockReloadBtn(); + + console.log(e); + + if (TCaptcha.errorCb) { + TCaptcha.errorCb.call(null, + "Couldn't load the captcha frame. Check your content blocker settings." + ); + } + }, + + load: function(board, thread_id, t_challenge, t_response) { + let self = TCaptcha; + + clearTimeout(self.frameTimeout); + clearTimeout(self.reloadTimeout); + clearTimeout(self.expireTimeout); + + let params = ['framed=1']; + + if (board) { + params.push('board=' + board); + } + + if (thread_id > 0) { + params.push('thread_id=' + thread_id); + } + + if (t_challenge && t_response) { + params.push('t-challenge=' + encodeURIComponent(t_challenge)); + params.push('t-response=' + t_response); + } + + let ticket = self.getTicket(); + + if (ticket) { + params.push('ticket=' + ticket); + } + + if (params.length > 0) { + params = '?' + params.join('&'); + } + + let src = 'https://sys.' + self.domain + self.path + params; + + self.frameNode = self.buildFrameNode(); + self.toggleImgCntNode(false); + self.node.insertBefore(self.frameNode, self.imgCntNode); + self.frameTimeout = setTimeout(self.onFrameTimeout, 60000, src); + self.frameNode.src = src; + }, + + onFrameTimeout: function(src) { + let self = TCaptcha; + + self.destroyFrame(); + + console.log('Captcha frame timeout'); + + if (self.errorCb) { + self.errorCb.call(null, `Couldn't get the captcha. +Make sure your browser doesn't block content on 4chan then click +here.`); + } + }, + + destroyFrame: function() { + let self = TCaptcha; + + clearTimeout(self.frameTimeout); + self.frameTimeout = null; + if (self.frameNode) { + self.frameNode.remove(); + self.frameNode = null; + } + self.toggleImgCntNode(true); + self.unlockReloadBtn(); + }, + + unlockReloadBtn: function() { + TCaptcha.reloadTs = null; + TCaptcha.toggleReloadBtn(true, 'Get Captcha'); + }, + + toggleReloadBtn: function(flag, label) { + let self = TCaptcha; + + if (self.reloadNode) { + self.reloadNode.disabled = !flag; + + if (label !== undefined) { + self.reloadNode.textContent = label; + } + } + }, + + onCaptchaFailed: function() { + let self = TCaptcha; + + let cd = self.failCd * 1000; + + if (self.reloadTs && self.reloadTs < cd) { + self.setReloadCd(cd, true); + } + }, + + setReloadCd: function(cd, visible, onDone) { + let self = TCaptcha; + + if (!self.node) { + return; + } + + clearTimeout(self.reloadTimeout); + + self.onReloadCdDone = onDone; + + self.pcdBypassable = visible === -1; + + if (cd) { + self.toggleReloadBtn(false); + if (visible) { + self.reloadTs = Date.now() + cd; + self.onReloadCdTick(); + } + else { + self.reloadTimeout = setTimeout(self.stopReloadCd, cd); + } + } + else { + self.stopReloadCd(); + } + }, + + stopReloadCd: function() { + let self = TCaptcha; + self.unlockReloadBtn(); + if (self.onReloadCdDone) { + self.onReloadCdDone.call(self); + } + }, + + onReloadCdTick: function() { + let self = TCaptcha; + + if (!self.reloadNode || !self.reloadTs) { + return; + } + + let cd = self.reloadTs - Date.now(); + + if (self.pcdBypassable) { + if (document.cookie.indexOf('_ev1=') !== -1) { + cd = 0; + } + } + + if (cd > 0) { + self.reloadNode.textContent = Math.ceil(cd / 1000); + self.reloadTimeout = setTimeout(self.onReloadCdTick, Math.min(cd, 1000)); + } + else { + self.stopReloadCd(); + } + }, + + clearChallenge: function() { + let self = TCaptcha; + + if (self.node) { + self.challengeNode.value = ''; + self.respNode.value = ''; + self.fgNode.style.backgroundImage = ''; + self.bgNode.style.backgroundImage = ''; + self.toggleSlider(false); + self.toggleMsgOverlay(false); + self.toggleNextBtn(false); + } + }, + + toggleSlider: function(flag) { + TCaptcha.sliderNode.style.visibility = flag ? '' : 'hidden'; + TCaptcha.sliderNode.style.boxShadow = flag ? '' : '0 0 4px 2px #1d8dc4'; + }, + + toggleMsgOverlay: function(flag, txt) { + if (txt !== undefined) { + TCaptcha.msgNode.innerHTML = `
    ${txt}
    `; + } + TCaptcha.msgNode.style.display = flag ? 'grid' : 'none'; + }, + + onSliderInput: function() { + var m = -Math.floor((+this.value) / 100 * this.twisterDelta); + TCaptcha.bgNode.style.backgroundPositionX = m + 'px'; + }, + + onTicketPcdTick: function() { + let self = TCaptcha; + + let el = document.getElementById('t-pcd'); + + if (!el) { + return; + } + + let pcd = +el.getAttribute('data-pcd'); + + pcd = pcd - (0 | (Date.now() / 1000)); + + if (pcd <= 0) { + self.onTicketPcdEnd(); + return; + } + + el.textContent = pcd; + + setTimeout(self.updateTicketPcd, 1000); + }, + + clearTicketOverlay: function() { + TCaptcha.toggleMsgOverlay(false); + }, + + buildFromJson: function(data) { + let self = TCaptcha; + + if (!self.node) { + return; + } + + self.unlockReloadBtn(); + self.toggleSlider(false); + self.toggleMsgOverlay(false); + + self.setTicket(data.ticket); + + if (self.errorCb) { + self.errorCb.call(null, ''); + } + + if (data.cd) { + self.setReloadCd(data.cd * 1000, !data.challenge); + } + + if (data.pcd) { + self.buildTicket(data); + return; + } + + if (data.error) { + console.log(data.error); + + self.clearChallenge(); + + if (self.errorCb) { + self.errorCb.call(null, data.error); + } + + return; + } + + if (data.mpcd) { + self.toggleNextBtn(true); + } + + self.imgCntNode.style.width = data.img_width + 'px'; + self.imgCntNode.style.height = data.img_height + 'px'; + + self.challengeNode.value = data.challenge; + + self.expireTimeout = setTimeout(self.clearChallenge, data.ttl * 1000 - 3000); + + if (data.bg_width) { + self.buildTwister(data); + } + else if (data.img) { + self.buildStatic(data); + } + else { + self.buildNoop(data); + } + }, + + buildTwister: function(data) { + let self = TCaptcha; + + self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')'; + self.bgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.bg + ')'; + + self.bgNode.style.backgroundPositionX = '0px'; + + self.toggleSlider(true); + self.sliderNode.value = 0; + self.sliderNode.twisterDelta = data.bg_width - data.img_width; + self.sliderNode.focus(); + }, + + buildStatic: function(data) { + let self = TCaptcha; + self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')'; + self.bgNode.style.backgroundImage = ''; + }, + + buildTicket: function(data) { + let self = TCaptcha; + self.clearChallenge(); + self.toggleMsgOverlay(true, data.pcd_msg || 'Please wait a while.'); + self.setReloadCd(data.pcd * 1000, data.bpcd ? -1 : true, self.clearTicketOverlay); + }, + + buildNoop: function(data) { + let self = TCaptcha; + self.toggleMsgOverlay(true, 'Verification not required.'); + self.fgNode.style.backgroundImage = ''; + self.bgNode.style.backgroundImage = ''; + } +}; + +/** + * Tooltips + */ +var Tip = { + node: null, + timeout: null, + delay: 300, + + init: function() { + document.addEventListener('mouseover', this.onMouseOver, false); + document.addEventListener('mouseout', this.onMouseOut, false); + }, + + onMouseOver: function(e) { + var cb, data, t; + + t = e.target; + + if (Tip.timeout) { + clearTimeout(Tip.timeout); + Tip.timeout = null; + } + + if (t.hasAttribute('data-tip')) { + data = null; + + if (t.hasAttribute('data-tip-cb')) { + cb = t.getAttribute('data-tip-cb'); + if (window[cb]) { + data = window[cb](t); + } + } + Tip.timeout = setTimeout(Tip.show, Tip.delay, e.target, data); + } + }, + + onMouseOut: function(e) { + if (Tip.timeout) { + clearTimeout(Tip.timeout); + Tip.timeout = null; + } + + Tip.hide(); + }, + + show: function(t, data, pos) { + var el, rect, style, left, top; + + rect = t.getBoundingClientRect(); + + el = document.createElement('div'); + el.id = 'tooltip'; + + if (data) { + el.innerHTML = data; + } + else { + el.textContent = t.getAttribute('data-tip'); + } + + if (!pos) { + pos = 'top'; + } + + el.className = 'tip-' + pos; + + document.body.appendChild(el); + + left = rect.left - (el.offsetWidth - t.offsetWidth) / 2; + + if (left < 0) { + left = rect.left + 2; + el.className += '-right'; + } + else if (left + el.offsetWidth > document.documentElement.clientWidth) { + left = rect.left - el.offsetWidth + t.offsetWidth + 2; + el.className += '-left'; + } + + top = rect.top - el.offsetHeight - 5; + + style = el.style; + style.top = (top + window.pageYOffset) + 'px'; + style.left = left + window.pageXOffset + 'px'; + + Tip.node = el; + }, + + hide: function() { + if (Tip.node) { + document.body.removeChild(Tip.node); + Tip.node = null; + } + } +}; + +/** + * Settings Syncher + */ +/* +var StorageSync = { + queue: [], + + init: function() { + var el, self = StorageSync; + + if (self.inited || !document.body) { + return; + } + + self.remoteFrame = null; + + self.remoteOrigin = location.protocol + '//boards.' + + (location.host === 'boards.4channel.org' ? '4chan' : '4channel') + + '.org'; + + window.addEventListener('message', self.onFrameMessage, false); + + el = document.createElement('iframe'); + el.width = 0; + el.height = 0; + el.style.display = 'none'; + el.style.visibility = 'hidden'; + + el.src = self.remoteOrigin + '/syncframe.html'; + + document.body.appendChild(el); + + self.inited = true; + }, + + onFrameMessage: function(e) { + var self = StorageSync; + + if (e.origin !== self.remoteOrigin) { + return; + } + + if (e.data === 'ready') { + self.remoteFrame = e.source; + + if (self.queue.length) { + self.send(); + } + + return; + } + }, + + sync: function(key) { + var self = StorageSync; + + self.queue.push(key); + self.send(); + }, + + send: function() { + var i, key, data, self = StorageSync; + + if (!self.inited) { + return self.init(); + } + + if (!self.remoteFrame) { + return; + } + + data = {}; + + for (i = 0; key = self.queue[i]; ++i) { + data[key] = localStorage.getItem(key); + } + + self.queue = []; + + self.remoteFrame.postMessage({ storage: data }, self.remoteOrigin); + } +}; +*/ +function mShowFull(t) { + var el, data; + + if (t.className === 'name') { + if (el = t.parentNode.parentNode.parentNode + .getElementsByClassName('name')[1]) { + data = el.innerHTML; + } + } + else if (t.parentNode.className === 'subject') { + if (el = t.parentNode.parentNode.parentNode.parentNode + .getElementsByClassName('subject')[1]) { + data = el.innerHTML; + } + } + else if (/fileThumb/.test(t.parentNode.className)) { + if (el = t.parentNode.parentNode.getElementsByClassName('fileText')[0]) { + el = el.firstElementChild; + data = el.getAttribute('title') || el.innerHTML; + } + } + + return data; +} + +function loadBannerImage() { + var cnt = document.getElementById('bannerCnt'); + + if (!cnt || cnt.offsetWidth <= 0) { + return; + } + + cnt.innerHTML = '4chan'; +} + +function onMobileSelectChange() { + var board, page; + + board = this.options[this.selectedIndex].value; + page = (board !== 'f' && /\/catalog$/.test(location.pathname)) ? 'catalog' : ''; + + window.location = '//boards.' + $L.d(board) + '/' + board + '/' + page; +} + +function buildMobileNav() { + var el, boards, i, b, html, order; + + if (el = document.getElementById('boardSelectMobile')) { + html = ''; + order = []; + + boards = document.querySelectorAll('#boardNavDesktop .boardList a'); + + for (i = 0; b = boards[i]; ++i) { + order.push(b); + } + + order.sort(function(a, b) { + if (a.textContent < b.textContent) { + return -1; + } + if (a.textContent > b.textContent) { + return 1; + } + return 0; + }); + + for (i = 0; b = order[i]; ++i) { + html += ''; + } + + el.innerHTML = html; + } +} + +function cloneTopNav() { + var navT, navB, ref, el; + + navT = document.getElementById('boardNavDesktop'); + + if (!navT) { + return; + } + + ref = document.getElementById('absbot'); + + navB = navT.cloneNode(true); + navB.id = navB.id + 'Foot'; + + if (el = navB.querySelector('#navtopright')) { + el.id = 'navbotright'; + } + + if (el = navB.querySelector('#settingsWindowLink')) { + el.id = el.id + 'Bot'; + } + + document.body.insertBefore(navB, ref); +} + +function initPass() { + if (get_cookie("pass_enabled") == '1' || get_cookie('extra_path')) { + window.passEnabled = true; + } + else { + window.passEnabled = false; + } +} + +function initBlotter() { + var mTime, seenTime, el; + + el = document.getElementById('toggleBlotter'); + + if (!el) { + return; + } + + el.addEventListener('click', toggleBlotter, false); + + seenTime = localStorage.getItem('4chan-blotter'); + + if (!seenTime) { + return; + } + + mTime = +el.getAttribute('data-utc'); + + if (mTime <= +seenTime) { + toggleBlotter(); + } +} + +function toggleBlotter(e) { + var el, btn; + + e && e.preventDefault(); + + el = document.getElementById('blotter-msgs'); + + if (!el) { + return; + } + + btn = document.getElementById('toggleBlotter'); + + if (el.style.display == 'none') { + el.style.display = ''; + localStorage.removeItem('4chan-blotter'); + btn.textContent = 'Hide'; + + el = btn.nextElementSibling; + + if (el.style.display) { + el.style.display = ''; + } + } + else { + el.style.display = 'none'; + localStorage.setItem('4chan-blotter', btn.getAttribute('data-utc')); + btn.textContent = 'Show Blotter'; + btn.nextElementSibling.style.display = 'none'; + } +} + +function onRecaptchaLoaded() { + if (document.getElementById('postForm').style.display == 'table') { + initRecaptcha(); + } +} + +function initRecaptcha() { + var el; + + el = document.getElementById('g-recaptcha'); + + if (!el || el.firstElementChild) { + return; + } + + if (!window.passEnabled && window.grecaptcha) { + grecaptcha.render(el, { + sitekey: window.recaptchaKey, + theme: (activeStyleSheet === 'Tomorrow' || window.dark_captcha) ? 'dark' : 'light' + }); + } +} + +function initTCaptcha() { + let el = document.getElementById('t-root'); + + if (el) { + let board = location.pathname.split(/\//)[1]; + + let thread_id; + + if (document.forms.post && document.forms.post.resto) { + thread_id = +document.forms.post.resto.value; + } + else { + thread_id = 0; + } + + TCaptcha.init(el, board, thread_id, 5); + TCaptcha.setErrorCb(window.showPostFormError); + } +} + +function initAnalytics() { + var tid = location.host.indexOf('.4channel.org') !== -1 ? 'UA-166538-5' : 'UA-166538-1'; + + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + + ga('create', tid, {'sampleRate': 1}); + ga('set', 'anonymizeIp', true); + ga('send','pageview'); +} + +function initAdsPF(cnt, slot_id) { + let sid, nid; + + if (slot_id == 1) { + sid = '657b2d8958f9186175770b1f'; + nid = 'pf-6892-1'; + } + else if (slot_id == 2) { + sid = '657b2d9d58f9186175770b37'; + nid = 'pf-6893-1'; + } + else if (slot_id == 3) { + sid = '657b2d56256794003cd16fe4'; + nid = 'pf-6890-1'; + } + else if (slot_id == 4) { + sid = '657b2d74256794003cd17019'; + nid = 'pf-6891-1'; + } + else { + return; + } + + cnt.innerHTML = ''; + + let d = document.createElement('div'); + d.id = nid; + cnt.appendChild(d); + + window.pubfuturetag = window.pubfuturetag || []; + window.pubfuturetag.push({unit: sid, id: nid}); +} + +function initAdsADT(scope) { + var el, nodes, i, cls, s; + + if (window.matchMedia && window.matchMedia('(max-width: 480px)').matches && localStorage.getItem('4chan_never_show_mobile') != 'true') { + cls = 'adg-m'; + } + else { + cls = 'adg'; + } + + nodes = (scope || document).getElementsByClassName(cls); + + for (i = 0; el = nodes[i]; ++i) { + if (el.hasAttribute('data-abc')) { + s = document.createElement('iframe'); + s.setAttribute('scrolling', 'no'); + s.setAttribute('frameborder', '0'); + s.setAttribute('allowtransparency', 'true'); + s.setAttribute('marginheight', '0'); + s.setAttribute('marginwidth', '0'); + + if (cls === 'adg') { + s.setAttribute('width', '728'); + s.setAttribute('height', '90'); + } + else { + s.setAttribute('width', '300'); + s.setAttribute('height', '250'); + } + + s.setAttribute('name', 'spot_id_' + el.getAttribute('data-abc')); + s.src = 'https://a.adtng.com/get/' + el.getAttribute('data-abc') + '?time=' + Date.now(); + el.appendChild(s); + } + } +} + +function danboAddSlot(n, b, m, s) { + let pubid = 27; + + let el = document.createElement('div'); + el.className = 'danbo_dta'; + + if (m) { + if (s) { + s = '3'; + } + else { + s = '4'; + el.id = 'js-danbo-rld'; + } + el.setAttribute('data-danbo', `${pubid}-${b}-${s}-300-250`); + el.classList.add('danbo-m'); + } + else { + if (s) { + s = '1'; + } + else { + s = '2'; + el.id = 'js-danbo-rld'; + } + el.setAttribute('data-danbo', `${pubid}-${b}-${s}-728-90`); + el.classList.add('danbo-d'); + } + + n.appendChild(el); + + return el; +} + +function initAdsDanbo() { + if (!window.Danbo) { + return; + } + + let b = location.pathname.split(/\//)[1] || '_'; + + let m = window.matchMedia && window.matchMedia('(max-width: 480px)').matches; + + let nodes = document.getElementsByClassName('danbo-slot'); + + for (let cnt of nodes) { + let s = cnt.id === 'danbo-s-t'; + danboAddSlot(cnt, b, m, s); + } + + window.addEventListener('message', function(e) { + if (e.origin === 'https://hakurei.cdnbo.org' && e.data && e.data.origin === 'danbo') { + window.initAdsFallback(e.data.unit_id); + } + }); + + window.Danbo.initialize(); +} + +function reloadAdsDanbo() { + let cnt = document.getElementById('danbo-s-b'); + + if (!cnt) { + return; + } + + cnt.innerHTML = ''; + + let b = 'a';//location.pathname.split(/\//)[1] || '_'; + + let m = window.matchMedia && window.matchMedia('(max-width: 480px)').matches; + + danboAddSlot(cnt, b, m, false); + + window.Danbo.reload('js-danbo-rld'); +} + +function initAdsFallback(slot_id) { + let fb = window.danbo_fb; + + let cnt_id; + + if (slot_id == 1 || slot_id == 3) { + cnt_id = 'danbo-s-t'; + } + else { + cnt_id = 'danbo-s-b'; + } + + let cnt = document.getElementById(cnt_id); + + if (!cnt) { + return; + } + + let is_burichan = document.body.classList.contains('ws'); + + let hr = is_burichan ? 0.1 : 0.01; + + if (Math.random() < hr) { + return initAdsHome(cnt); + } + + if (cnt_id === 'danbo-s-t') { + if (is_burichan) { + initAdsPF(cnt, slot_id); + } + else if (fb) { + if (slot_id == 1 && fb.t_abc_d) { + cnt.innerHTML = `
    `; + initAdsADT(cnt); + } + else if (slot_id == 3 && fb.t_abc_m) { + cnt.innerHTML = `
    `; + initAdsADT(cnt); + } + else { + initAdsHome(cnt); + } + } + else { + initAdsHome(cnt); + } + } + else if (cnt_id === 'danbo-s-b') { + if (is_burichan) { + initAdsPF(cnt, slot_id); + } + else if (fb) { + if (slot_id == 4 && fb.b_abc_m) { + cnt.innerHTML = `
    `; + initAdsADT(cnt); + } + else { + initAdsHome(cnt); + } + } + else { + initAdsHome(cnt); + } + } + else { + console.log('Fallback', slot_id); + } +} + +function initAdsHome(cnt) { + let banners = [ + ['advertise', '1.png', '2.png', '3.png'], + ['pass', '4.png'], + ]; + + let banners_m = [ + ['advertise', '1m.png'], + ]; + + let d; + + if (location.host.indexOf('4channel')) { + d = '4channel'; + } + else { + d = '4chan'; + } + + let b; + + if (window.matchMedia && window.matchMedia('(max-width: 480px)').matches) { + b = banners_m; + } + else { + b = banners; + } + + b = b[Math.floor(Math.random() * b.length)]; + + let href = b[0]; + let src = b[1 + Math.floor(Math.random() * (b.length - 1))]; + + let a = document.createElement('a'); + a.href = `https://www.${d}.org/${href}`; + a.target = '_blank'; + + let img = document.createElement('img'); + img.src = '//s.4cdn.org/image/banners/' + src; + + a.appendChild(img); + + if (cnt.children.length) { + cnt.innerHTML = ''; + } + + cnt.appendChild(a); +} + +function applySearch(e) { + var str; + + e && e.preventDefault(); + + str = document.getElementById('search-box').value; + + if (str !== '') { + window.location.href = 'catalog#s=' + str; + } +} + +function onKeyDownSearch(e) { + if (e.keyCode == 13) { + applySearch(); + } +} + +function onReportClick(e) { + var i, input, nodes, board; + + nodes = document.getElementsByTagName('input'); + + board = location.pathname.split(/\//)[1]; + + for (i = 0; input = nodes[i]; ++i) { + if (input.type == 'checkbox' && input.checked && input.value == 'delete') { + return reppop('https://sys.' + $L.d(board) + '/' + board + '/imgboard.php?mode=report&no=' + + input.name.replace(/[a-z]+/, '') + ); + } + } +} + +function onStyleSheetChange(e) { + setActiveStyleSheet(this.value); +} + +function onPageSwitch(e) { + e.preventDefault(); + window.location = this.action; +} + +function onMobileFormClick(e) { + var index = location.pathname.split(/\//).length < 4; + + e.preventDefault(); + + if (window.QR && Main.tid && QR.enabled) { + QR.show(Main.tid); + } + else if (this.parentNode.id == 'mpostform') { + toggleMobilePostForm(index); + } + else { + toggleMobilePostForm(index, 1); + } +} + +function onMobileRefreshClick(e) { + locationHashChanged(this); +} + +function toggle(name) { + var a = document.getElementById(name); + a.style.display = ((a.style.display != 'block') ? 'block' : 'none'); +} + +function quote(text) { + if (document.selection) { + document.post.com.focus(); + var sel = document.selection.createRange(); + sel.text = ">>" + text + "\n"; + } else if (document.post.com.selectionStart || document.post.com.selectionStart == "0") { + var startPos = document.post.com.selectionStart; + var endPos = document.post.com.selectionEnd; + document.post.com.value = document.post.com.value.substring(0, startPos) + ">>" + text + "\n" + document.post.com.value.substring(endPos, document.post.com.value.length); + } else { + document.post.com.value += ">>" + text + "\n"; + } +} + +function repquote(rep) { + if (document.post.com.value == "") { + quote(rep); + } +} + +function reppop(url) { + var height; + + if (window.passEnabled || !window.grecaptcha) { + height = 205; + } + else { + height = 510; + } + + window.open(url, Date.now(), + 'toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height=' + height + ); + + return false; +} + +function recaptcha_load() { + var d = document.getElementById("recaptcha_div"); + if (!d) return; + + Recaptcha.create("6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc", "recaptcha_div",{theme: "clean"}); +} + +function onParsingDone(e) { + var i, nodes, n, p, tid, offset, limit; + + tid = e.detail.threadId; + offset = e.detail.offset; + + if (!offset) { + return; + } + + nodes = document.getElementById('t' + tid).getElementsByClassName('nameBlock'); + limit = e.detail.limit ? (e.detail.limit * 2) : nodes.length; + for (i = offset * 2 + 1; i < limit; i+=2) { + if (n = nodes[i].children[1]) { + if (currentHighlighted + && n.className.indexOf('id_' + currentHighlighted) != -1) { + p = n.parentNode.parentNode.parentNode; + p.className = 'highlight ' + p.className; + } + n.addEventListener('click', idClick, false); + } + } +} + +function loadExtraScripts() { + var el, path; + + path = readCookie('extra_path'); + + if (!path || !/^[a-z0-9]+$/.test(path)) { + return false; + } + + if (window.FC) { + el = document.createElement('script'); + el.type = 'text/javascript'; + el.src = 'https://s.4cdn.org/js/' + path + '.' + jsVersion + '.js'; + document.head.appendChild(el); + } + else { + document.write(''); + } + + return true; +} + + +function toggleMobilePostForm(index, scrolltotop) { + var elem = document.getElementById('mpostform').firstElementChild; + var postForm = document.getElementById('postForm'); + + if (elem.className.match('hidden')) { + elem.className = elem.className.replace('hidden', 'shown'); + postForm.className = postForm.className.replace(' hideMobile', ''); + elem.innerHTML = 'Close Post Form'; + initRecaptcha(); + initTCaptcha(); + checkIncognito(); + } + else { + elem.className = elem.className.replace('shown', 'hidden'); + postForm.className += ' hideMobile'; + elem.innerHTML = (index) ? 'Start New Thread' : 'Post Reply'; + } + + if (scrolltotop) { + elem.scrollIntoView(); + } +} + +function toggleGlobalMessage(e) { + var elem, postForm; + + if (e) { + e.preventDefault(); + } + + elem = document.getElementById('globalToggle'); + postForm = document.getElementById('globalMessage'); + + if( elem.className.match('hidden') ) { + elem.className = elem.className.replace('hidden', 'shown'); + postForm.className = postForm.className.replace(' hideMobile', ''); + + elem.innerHTML = 'Close Announcement'; + } else { + elem.className = elem.className.replace('shown', 'hidden'); + postForm.className += ' hideMobile'; + + elem.innerHTML = 'View Announcement'; + } +} + +function checkRecaptcha() +{ + if( typeof RecaptchaState.timeout != 'undefined' ) { + if( RecaptchaState.timeout == 1800 ) { + RecaptchaState.timeout = 570; + Recaptcha._reset_timer(); + clearInterval(captchainterval); + } + } +} + +function setPassMsg() { + var el, msg; + + el = document.getElementById('captchaFormPart'); + + if (!el) { + return; + } + + msg = 'You are using a 4chan Pass. [Logout]'; + el.children[1].innerHTML = '
    ' + msg + '
    '; +} + +function confirmPassLogout(event) +{ + var conf = confirm('Are you sure you want to logout?'); + if( !conf ) { + event.preventDefault(); + return false; + } +} + +var activeStyleSheet; + +function initStyleSheet() { + var i, rem, link, len; + + // fix me + if (window.FC) { + return; + } + + if (window.style_group) { + var cookie = readCookie(style_group); + activeStyleSheet = cookie ? cookie : getPreferredStyleSheet(); + } + + if (window.css_event && localStorage.getItem('4chan_stop_css_event') !== `${window.css_event}-${window.css_event_v}`) { + activeStyleSheet = '_special'; + } + + switch(activeStyleSheet) { + case "Yotsuba B": + setActiveStyleSheet("Yotsuba B New", true); + break; + + case "Yotsuba": + setActiveStyleSheet("Yotsuba New", true); + break; + + case "Burichan": + setActiveStyleSheet("Burichan New", true); + break; + + case "Futaba": + setActiveStyleSheet("Futaba New", true); + break; + + default: + setActiveStyleSheet(activeStyleSheet, true); + break; + } + + if (localStorage.getItem('4chan_never_show_mobile') == 'true') { + link = document.querySelectorAll('link'); + len = link.length; + for (i = 0; i < len; i++) { + if (link[i].getAttribute('href').match('mobile')) { + (rem = link[i]).parentNode.removeChild(rem); + } + } + } +} + +function pageHasMath() { + var i, el, nodes; + + nodes = document.getElementsByClassName('postMessage'); + + for (i = 0; el = nodes[i]; ++i) { + if (/\[(?:eqn|math)\]|"math">/.test(el.innerHTML)) { + return true; + } + } + + return false; +} + +function cleanWbr(el) { + var i, nodes, n; + + nodes = el.getElementsByTagName('wbr'); + + for (i = nodes.length - 1; n = nodes[i]; i--) { + n.parentNode.removeChild(n); + } +} + +function parseMath() { + var i, el, nodes; + + nodes = document.getElementsByClassName('postMessage'); + + for (i = 0; el = nodes[i]; ++i) { + if (/\[(?:eqn|math)\]/.test(el.innerHTML)) { + cleanWbr(el); + } + } + + MathJax.Hub.Queue(['Typeset', MathJax.Hub, nodes]); +} + +function loadMathJax() { + var head, script; + + head = document.getElementsByTagName('head')[0]; + + script = document.createElement('script'); + script.type = 'text/x-mathjax-config'; + script.text = "MathJax.Hub.Config({\ +extensions: ['Safe.js'],\ +tex2jax: { processRefs: false, processEnvironments: false, preview: 'none', inlineMath: [['[math]','[/math]']], displayMath: [['[eqn]','[/eqn]']] },\ +Safe: { allow: { URLs: 'none', classes: 'none', cssIDs: 'none', styles: 'none', fontsize: 'none', require: 'none' } },\ +displayAlign: 'left', messageStyle: 'none', skipStartupTypeset: true,\ +'CHTML-preview': { disabled: true }, MathMenu: { showRenderer: false, showLocale: false },\ +TeX: { Macros: { color: '{}', newcommand: '{}', renewcommand: '{}', newenvironment: '{}', renewenvironment: '{}', def: '{}', let: '{}'}}});"; + head.appendChild(script); + + script = document.createElement('script'); + script.src = '//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-AMS_HTML-full'; + script.onload = parseMath; + head.appendChild(script); +} + +captchainterval = null; +function init() { + var el, i; + var error = typeof is_error != "undefined"; + var board = location.href.match(/(?:4chan|4channel)\.org\/(\w+)/)[1]; + var arr = location.href.split(/#/); + if( arr[1] && arr[1].match(/q[0-9]+$/) ) { + repquote( arr[1].match(/q([0-9]+)$/)[1] ); + } + + + if (window.math_tags && pageHasMath()) { + loadMathJax(); + } + + if(navigator.userAgent) { + if( navigator.userAgent.match( /iP(hone|ad|od)/i ) ) { + links = document.querySelectorAll('s'); + len = links.length; + + for(i = 0; i < len; i++ ) { + links[i].onclick = function() { + if (this.hasAttribute('style')) { + this.removeAttribute('style'); + } + else { + this.setAttribute('style', 'color: #fff!important;'); + } + }; + } + } + } + + if( document.getElementById('styleSelector') ) { + styleSelect = document.getElementById('styleSelector'); + len = styleSelect.options.length; + for (i = 0; i < len; i++) { + if (styleSelect.options[i].value == activeStyleSheet) { + styleSelect.selectedIndex = i; + continue; + } + } + } + + if (!error && document.forms.post) { + if (board != 'i' && board != 'ic' && board != 'f') { + if (window.File && window.FileReader && window.FileList && window.Blob) { + el = document.getElementById('postFile'); + el && el.addEventListener('change', handleFileSelect, false); + } + } + } + + //window.addEventListener('onhashchange', locationHashChanged, false); + + if( typeof extra != "undefined" && extra && !error ) extra.init(); +} + +var coreLenCheckTimeout = null; +function onComKeyDown() { + clearTimeout(coreLenCheckTimeout); + coreLenCheckTimeout = setTimeout(coreCheckComLength, 500); +} + +function coreCheckComLength() { + var byteLength, comField, error; + + if (comlen) { + comField = document.getElementsByName('com')[0]; + byteLength = encodeURIComponent(comField.value).split(/%..|./).length - 1; + + if (byteLength > comlen) { + if (!(error = document.getElementById('comlenError'))) { + error = document.createElement('div'); + error.id = 'comlenError'; + error.style.cssText = 'font-weight:bold;padding:5px;color:red;'; + comField.parentNode.appendChild(error); + } + error.textContent = 'Error: Comment too long (' + byteLength + '/' + comlen + ').'; + } + else if (error = document.getElementById('comlenError')) { + error.parentNode.removeChild(error); + } + } +} + +function disableMobile() { + localStorage.setItem('4chan_never_show_mobile', 'true'); + location.reload(true); +} + +function enableMobile() { + localStorage.removeItem('4chan_never_show_mobile'); + location.reload(true); +} + +var currentHighlighted = null; +function enableClickableIds() +{ + var i = 0, len = 0; + var elems = document.getElementsByClassName('posteruid'); + var capcode = document.getElementsByClassName('capcode'); + + if( capcode != null ) { + for( i = 0, len = capcode.length; i < len; i++ ) { + capcode[i].addEventListener("click", idClick, false); + } + } + + if( elems == null ) return; + for( i = 0, len = elems.length; i < len; i++ ) { + elems[i].addEventListener("click", idClick, false); + } +} + +function idClick(evt) +{ + var i = 0, len = 0, node; + var uid = evt.target.className == 'hand' ? evt.target.parentNode.className.match(/id_([^ $]+)/)[1] : evt.target.className.match(/id_([^ $]+)/)[1]; + + // remove all .highlight classes + var hl = document.getElementsByClassName('highlight'); + len = hl.length; + for( i = 0; i < len; i++ ) { + var cn = hl[0].className.toString(); + hl[0].className = cn.replace(/highlight /g, ''); + } + + if( currentHighlighted == uid ) { + currentHighlighted = null; + return; + } + currentHighlighted = uid; + + var nhl = document.getElementsByClassName('id_' + uid); + len = nhl.length; + for( i = 0; i < len; i++ ) { + node = nhl[i].parentNode.parentNode.parentNode; + if( !node.className.match(/highlight /) ) node.className = "highlight " + node.className; + } +} + +function showPostFormError(msg) { + var el = document.getElementById('postFormError'); + + if (msg) { + el.innerHTML = msg; + el.style.display = 'block'; + } + else { + el.textContent = ''; + el.style.display = ''; + } +} + +function handleFileSelect() { + var fsize, ftype, maxFilesize; + + if (this.files) { + maxFilesize = window.maxFilesize; + + fsize = this.files[0].size; + ftype = this.files[0].type; + + if (ftype.indexOf('video/') !== -1 && window.maxWebmFilesize) { + maxFilesize = window.maxWebmFilesize; + } + + if (fsize > maxFilesize) { + showPostFormError('Error: Maximum file size allowed is ' + + Math.floor(maxFilesize / 1048576) + ' MB'); + } + else { + showPostFormError(); + } + } +} + +function locationHashChanged(e) +{ + var css = document.getElementById('id_css'); + + switch( e.id ) + { + case 'refresh_top': + url = window.location.href.replace(/#.+/, '#top'); + if( !/top$/.test(url) ) url += '#top'; + css.innerHTML = ''; + document.location.reload(true); + break; + + case 'refresh_bottom': + url = window.location.href.replace(/#.+/, '#bottom'); + if( !/bottom$/.test(url) ) url += '#bottom'; + css.innerHTML = ''; + document.location.reload(true); + break; + + default:break; + } + + return true; + +} + +function setActiveStyleSheet(title, init) { + var a, link, href, i, nodes, fn; + + if( document.querySelectorAll('link[title]').length == 1 ) { + return; + } + + href = ''; + + nodes = document.getElementsByTagName('link'); + + for (i = 0; a = nodes[i]; i++) { + if (a.getAttribute("title") == "switch") { + link = a; + } + + if (a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) { + if (a.getAttribute("title") == title) { + href = a.href; + } + } + } + + link && link.setAttribute("href", href); + + if (!init) { + if (title !== '_special') { + createCookie(style_group, title, 365, location.host.indexOf('4channel.org') === -1 ? '4chan.org' : '4channel.org'); + + if (window.css_event) { + fn = window['fc_' + window.css_event + '_cleanup']; + localStorage.setItem('4chan_stop_css_event', `${window.css_event}-${window.css_event_v}`); + } + } + else if (window.css_event) { + fn = window['fc_' + window.css_event + '_init']; + localStorage.removeItem('4chan_stop_css_event'); + } + + //StorageSync.sync('4chan_stop_css_event'); + + activeStyleSheet = title; + + fn && fn(); + } +} + +function getActiveStyleSheet() { + var i, a; + var link; + + if( document.querySelectorAll('link[title]').length == 1 ) { + return 'Yotsuba P'; + } + + for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { + if (a.getAttribute("title") == "switch") + link = a; + else if (a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title") && a.href==link.href) return a.getAttribute("title"); + } + return null; +} + +function getPreferredStyleSheet() { + return (style_group == "ws_style") ? "Yotsuba B New" : "Yotsuba New"; +} + +function createCookie(name, value, days, domain) { + let expires; + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); + } + else { + expires = ''; + } + + if (domain) { + domain = "; domain=" + domain; + } + else { + domain = ''; + } + + document.cookie = name + "=" + value + expires + "; path=/" + domain; +} + +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return ''; +} + +// legacy +var get_cookie = readCookie; + +function setRetinaIcons() { + var i, j, nodes; + + nodes = document.getElementsByClassName('retina'); + + for (i = 0; j = nodes[i]; ++i) { + j.src = j.src.replace(/\.(gif|png)$/, "@2x.$1"); + } +} + +function onCoreClick(e) { + if (/flag flag-/.test(e.target.className) && e.which == 1) { + window.open('//s.4cdn.org/image/country/' + + e.target.className.match(/flag-([a-z]+)/)[1] + + '.gif', ''); + } +} + +function showPostForm(e) { + var el; + + e && e.preventDefault(); + + if (el = document.getElementById('postForm')) { + $.id('togglePostFormLink').style.display = 'none'; + el.style.display = 'table'; + initRecaptcha(); + initTCaptcha(); + } +} + +function oeCanvasPreview(e) { + var t, el, sel; + + if (el = document.getElementById('oe-canvas-preview')) { + el.parentNode.removeChild(el); + } + + if (e.target.nodeName == 'OPTION' && e.target.value != '0') { + t = document.getElementById('f' + e.target.value); + + if (!t) { + return; + } + + t = t.getElementsByTagName('img')[0]; + + if (!t || !t.hasAttribute('data-md5')) { + return; + } + + el = t.cloneNode(); + el.id = 'oe-canvas-preview'; + sel = e.target.parentNode; + sel.parentNode.insertBefore(el, sel.nextSibling); + } +} + +function oeClearPreview(e) { + var el; + + if (el = document.getElementById('oe-canvas-preview')) { + el.parentNode.removeChild(el); + } +} + +var PainterCore = { + init: function() { + var cnt, btns; + + if (!document.forms.post) { + return; + } + + cnt = document.forms.post.getElementsByClassName('painter-ctrl')[0]; + + if (!cnt) { + return; + } + + btns = cnt.getElementsByTagName('button'); + + if (!btns[1]) { + return; + } + + this.data = null; + this.replayBlob = null; + + this.time = 0; + + this.btnDraw = btns[0]; + this.btnClear = btns[1]; + this.btnFile = document.getElementById('postFile'); + this.btnSubmit = document.forms.post.querySelector('input[type="submit"]'); + this.inputNodes = cnt.getElementsByTagName('input'); + this.replayCb = cnt.getElementsByClassName('oe-r-cb')[0]; + + btns[0].addEventListener('click', this.onDrawClick, false); + btns[1].addEventListener('click', this.onCancel, false); + }, + + onDrawClick: function() { + var w, h, dims = this.parentNode.getElementsByTagName('input'); + + w = +dims[0].value; + h = +dims[1].value; + + if (w < 1 || h < 1) { + return; + } + + window.Keybinds && (Keybinds.enabled = false); + + Tegaki.open({ + onDone: PainterCore.onDone, + onCancel: PainterCore.onCancel, + saveReplay: PainterCore.replayCb && PainterCore.replayCb.checked, + width: w, + height: h + }); + }, + + replay: function(id) { + id = +id; + + Tegaki.open({ + replayMode: true, + replayURL: '//i.4cdn.org/' + location.pathname.split(/\//)[1] + '/' + id + '.tgkr' + }); + }, + + // move this to tegaki.js + b64toBlob: function(data) { + var i, bytes, ary, bary, len; + + bytes = atob(data); + len = bytes.length; + + ary = new Array(len); + + for (i = 0; i < len; ++i) { + ary[i] = bytes.charCodeAt(i); + } + + bary = new Uint8Array(ary); + + return new Blob([bary]); + }, + + onDone: function() { + var self, el; + + self = PainterCore; + + window.Keybinds && (Keybinds.enabled = true); + + self.btnFile.disabled = true; + self.btnClear.disabled = false; + + self.data = Tegaki.flatten().toDataURL('image/png'); + + if (Tegaki.saveReplay) { + self.replayBlob = Tegaki.replayRecorder.toBlob(); + } + + if (!Tegaki.hasCustomCanvas && Tegaki.startTimeStamp) { + self.time = Math.round((Date.now() - Tegaki.startTimeStamp) / 1000); + } + else { + self.time = 0; + } + + self.btnFile.style.visibility = 'hidden'; + + self.btnDraw.textContent = 'Edit'; + + for (el of self.inputNodes) { + el.disabled = true; + } + + document.forms.post.addEventListener('submit', self.onSubmit, false); + }, + + onCancel: function() { + var self = PainterCore; + + window.Keybinds && (Keybinds.enabled = true); + + self.data = null; + self.replayBlob = null; + self.time = 0; + + self.btnFile.disabled = false; + self.btnClear.disabled = true; + + self.btnFile.style.visibility = ''; + + self.btnDraw.textContent = 'Draw'; + + for (var el of self.inputNodes) { + el.disabled = false; + } + + document.forms.post.removeEventListener('submit', self.onSubmit, false); + }, + + onSubmit: function(e) { + var formdata, blob, xhr; + + e.preventDefault(); + + formdata = new FormData(this); + + blob = PainterCore.b64toBlob(PainterCore.data.slice(PainterCore.data.indexOf(',') + 1)); + + if (blob) { + formdata.append('upfile', blob, 'tegaki.png'); + + if (PainterCore.replayBlob) { + formdata.append('oe_replay', PainterCore.replayBlob, 'tegaki.tgkr'); + } + } + + formdata.append('oe_time', PainterCore.time); + + xhr = new XMLHttpRequest(); + xhr.open('POST', this.action, true); + xhr.withCredentials = true; + xhr.onerror = PainterCore.onSubmitError; + xhr.onload = PainterCore.onSubmitDone; + + xhr.send(formdata); + + PainterCore.btnSubmit.disabled = true; + }, + + onSubmitError: function() { + PainterCore.btnSubmit.disabled = false; + showPostFormError('Connection Error.'); + }, + + onSubmitDone: function() { + var resp, ids, tid, pid, board; + + PainterCore.btnSubmit.disabled = false; + + if (ids = this.responseText.match(//)) { + tid = +ids[1]; + pid = +ids[2]; + + if (!tid) { + tid = pid; + } + + board = location.pathname.split(/\//)[1]; + + window.location.href = '/' + board + '/thread/' + tid + '#p' + pid; + + PainterCore.onCancel(); + + if (tid != pid) { + PainterCore.btnClear.disabled = true; + window.location.reload(); + } + + return; + } + + if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) { + showPostFormError(resp[1]); + } + } +}; + +function oeReplay(id) { + PainterCore.replay(id); +} + +/*! https://github.com/Joe12387/detectIncognito */ +function checkIncognito() { + if (window.isIncognito !== undefined) { + return; + } + + if (!navigator.maxTouchPoints || navigator.vendor === undefined) { + window.isIncognito = false; + return; + } + + (new Promise(function(resolve, reject) { + let eh = eval.toString().length; + + if (navigator.vendor.indexOf('Apple') === 0 && eh === 37) { + if (navigator.maxTouchPoints === undefined) { + resolve(false); + } + + let db_name = Math.random().toString(); + + try { + let db = window.indexedDB.open(db_name, 1); + db.onupgradeneeded = function (e) { + let res = e.target.result; + try { + res.createObjectStore('test', { autoIncrement: true }).put(new Blob); + resolve(false); + } + catch(err) { + let msg; + if (err instanceof Error) { + msg = err.message; + } + if (typeof msg !== 'string') { + resolve(false); + } + resolve(/BlobURLs are not yet supported/.test(msg)); + } + finally { + res.close(); + window.indexedDB.deleteDatabase(db_name); + } + }; + } + catch(err) { + resolve(false); + } + } + else if (navigator.vendor.indexOf('Google') === 0 && eh === 33) { + let hsl; + + try { + hsl = performance.memory.jsHeapSizeLimit; + } + catch(err) { + hsl = 1073741824; + } + + navigator.webkitTemporaryStorage.queryUsageAndQuota(function (_, quota) { + let q = Math.round(quota / (1024 * 1024)); + let q_lim = Math.round(hsl / (1024 * 1024)) * 2; + resolve(q < q_lim); + }, function (err) { + resolve(false); + }); + } + else if (document.body.style.MozAppearance !== undefined && eh === 37) { + resolve(navigator.serviceWorker === undefined); + } + else { + resolve(false); + } + })).then((v) => window.isIncognito = v); +} + +function onPostFormSubmit(e) { + let el = $.id('postFile'); + if (el && el.value && window.isIncognito) { + e.stopPropagation() + e.preventDefault(); + el.value = ''; + showPostFormError('Uploading files in incognito mode is not allowed.' + + '
    The File field has been cleared.'); + return false; + } +} + +function contentLoaded() { + var i, el, el2, nodes, len, mobileSelect, params, board, val, fn; + + document.removeEventListener('DOMContentLoaded', contentLoaded, true); + + initAdsADT(); + + initAdsDanbo(); + + if (document.post) { + document.post.name.value = get_cookie("4chan_name"); + document.post.email.value = get_cookie("options"); + document.post.addEventListener('submit', onPostFormSubmit, false); + } + + cloneTopNav(); + + initAnalytics(); + + params = location.pathname.split(/\//); + + board = params[1]; + + if (window.passEnabled) { + setPassMsg(); + } + + if (window.Tegaki) { + PainterCore.init(); + } + + if (el = document.getElementById('bottomReportBtn')) { + el.addEventListener('click', onReportClick, false); + } + + if (el = document.getElementById('styleSelector')) { + el.addEventListener('change', onStyleSheetChange, false); + } + + // Post form toggle + if (el = document.getElementById('togglePostFormLink')) { + if (el = el.firstElementChild) { + el.addEventListener('click', showPostForm, false); + } + if (location.hash === '#reply') { + showPostForm(); + } + } + + // Selectable flags + if ((el = document.forms.post) && el.flag) { + el.flag.addEventListener('change', onBoardFlagChanged, false); + + if ((val = localStorage.getItem('4chan_flag_' + board)) && (el2 = el.querySelector('option[value="' + val + '"]'))) { + el2.setAttribute('selected', 'selected'); + } + } + + // Mobile nav menu + buildMobileNav(); + + // Mobile global message toggle + if (el = document.getElementById('globalToggle')) { + el.addEventListener('click', toggleGlobalMessage, false); + } + + if (localStorage.getItem('4chan_never_show_mobile') == 'true') { + if (el = document.getElementById('disable-mobile')) { + el.style.display = 'none'; + el = document.getElementById('enable-mobile'); + el.parentNode.style.cssText = 'display: inline !important;'; + } + } + + if (mobileSelect = document.getElementById('boardSelectMobile')) { + len = mobileSelect.options.length; + for ( i = 0; i < len; i++) { + if (mobileSelect.options[i].value == board) { + mobileSelect.selectedIndex = i; + continue; + } + } + + mobileSelect.addEventListener('change', onMobileSelectChange, false); + } + + if (document.forms.oeform && (el = document.forms.oeform.oe_src)) { + el.addEventListener('mouseover', oeCanvasPreview, false); + el.addEventListener('mouseout', oeClearPreview, false); + } + + if (params[2] != 'catalog') { + // Mobile post form toggle + nodes = document.getElementsByClassName('mobilePostFormToggle'); + + for (i = 0; el = nodes[i]; ++i) { + el.addEventListener('click', onMobileFormClick, false); + } + + if (el = document.getElementsByName('com')[0]) { + el.addEventListener('keydown', onComKeyDown, false); + el.addEventListener('paste', onComKeyDown, false); + el.addEventListener('cut', onComKeyDown, false); + } + + // Mobile refresh buttons + if (el = document.getElementById('refresh_top')) { + el.addEventListener('mouseup', onMobileRefreshClick, false); + } + + if (el = document.getElementById('refresh_bottom')) { + el.addEventListener('mouseup', onMobileRefreshClick, false); + } + + // Clickable flags + if (board == 'int' || board == 'sp' || board == 'pol') { + el = document.getElementById('delform'); + el.addEventListener('click', onCoreClick, false); + } + + // Page switcher + Search field + if (!params[3]) { + nodes = document.getElementsByClassName('pageSwitcherForm'); + + for (i = 0; el = nodes[i]; ++i) { + el.addEventListener('submit', onPageSwitch, false); + } + + if (el = document.getElementById('search-box')) { + el.addEventListener('keydown', onKeyDownSearch, false); + } + } + + if (window.clickable_ids) { + enableClickableIds(); + } + + Tip.init(); + } + + if (window.devicePixelRatio >= 2) { + setRetinaIcons(); + } + + initBlotter(); + + loadBannerImage(); + + if (window.css_event && activeStyleSheet === '_special') { + fn = window['fc_' + window.css_event + '_init']; + fn && fn(); + } +} + +function onBoardFlagChanged() { + var key = '4chan_flag_' + location.pathname.split(/\//)[1]; + + if (this.value === '0') { + localStorage.removeItem(key); + } + else { + localStorage.setItem(key, this.value); + } +} + +initPass(); + +window.onload = init; + +if (window.clickable_ids) { + document.addEventListener('4chanParsingDone', onParsingDone, false); +} + +document.addEventListener('4chanMainInit', loadExtraScripts, false); +document.addEventListener('DOMContentLoaded', contentLoaded, true); + +initStyleSheet(); diff --git a/js/core.js b/js/core.js new file mode 100644 index 0000000..0fcbb3c --- /dev/null +++ b/js/core.js @@ -0,0 +1,2567 @@ +var $L = { + nws: {"aco":1,"b":1,"bant":1,"d":1,"e":1,"f":1,"gif":1,"h":1,"hc":1,"hm":1,"hr":1,"i":1,"ic":1,"pol":1,"r":1,"r9k":1,"s":1,"s4s":1,"soc":1,"t":1,"trash":1,"u":1,"wg":1,"y":1}, + blue: '4chan.org', red: '4chan.org', + d: function(b) { + return $L.nws[b] ? $L.red : $L.blue; + } +}; + +/** + * Captcha +*/ +var TCaptcha = { + node: null, + + frameNode: null, + imgCntNode: null, + bgNode: null, + fgNode: null, + msgNode: null, + sliderNode: null, + respNode: null, + reloadNode: null, + helpNode: null, + challengeNode: null, + + ticketCaptchaNode: null, + + challenge: null, + + reloadTs: null, + reloadTimeout: null, + expireTimeout: null, + frameTimeout: null, + + pcdBypassable: false, + + errorCb: null, + + path: '/captcha', + + ticketKey: '4chan-tc-ticket', + + domain: '4chan.org', + + failCd: 60, + + tabindex: null, + + hCaptchaSiteKey: '49d294fa-f15c-41fc-80ba-c2544c52ec2a', + + init: function(el, board, thread_id, tabindex) { + if (this.node) { + this.destroy(); + } + + if (tabindex) { + this.tabindex = tabindex; + } + + this.node = el; + + el.style.position = 'relative'; + el.style.width = '300px'; + + this.frameNode = null; + this.imgCntNode = this.buildImgCntNode(); + this.bgNode = this.buildImgNode('bg'); + this.fgNode = this.buildImgNode('fg'); + this.sliderNode = this.buildSliderNode(); + + this.respNode = this.buildRespField(); + this.reloadNode = this.buildReloadNode(board, thread_id); + this.helpNode = this.buildHelpNode(); + this.msgNode = this.buildMsgNode(); + this.challengeNode = this.buildChallengeNode(); + + el.appendChild(this.reloadNode); + el.appendChild(this.respNode); + el.appendChild(this.helpNode); + + this.imgCntNode.appendChild(this.bgNode); + this.imgCntNode.appendChild(this.fgNode); + el.appendChild(this.imgCntNode); + + el.appendChild(this.sliderNode); + el.appendChild(this.msgNode); + el.appendChild(this.challengeNode); + + window.addEventListener('message', this.onFrameMessage); + }, + + destroy: function() { + let self = TCaptcha; + + if (!self.node) { + return; + } + + window.removeEventListener('message', self.onFrameMessage); + + clearTimeout(self.frameTimeout); + clearTimeout(self.reloadTimeout); + clearTimeout(self.expireTimeout); + + self.node.textContent = ''; + + self.node = null; + self.frameNode = null; + self.imgCntNode = null; + self.bgNode = null; + self.fgNode = null; + self.msgNode = null; + self.sliderNode = null; + self.respNode = null; + self.reloadNode = null; + self.helpNode = null; + self.challengeNode = null; + + self.ticketCaptchaNode = null; + + self.challenge = null; + + self.pcdBypassable = false; + + self.errorCb = null; + + self.reloadTs = null; + + self.onReloadCdDone = null; + }, + + setErrorCb: function(func) { + TCaptcha.errorCb = func; + }, + + toggleImgCntNode: function(flag) { + TCaptcha.imgCntNode.style.display = flag ? 'block' : 'none'; + }, + + getTicket: function() { + return localStorage.getItem(TCaptcha.ticketKey); + }, + + setTicket: function(val) { + if (val) { + localStorage.setItem(TCaptcha.ticketKey, val); + } + else if (val === false) { + localStorage.removeItem(TCaptcha.ticketKey); + } + }, + + buildFrameNode: function() { + let el = document.createElement('iframe'); + el.id = 't-frame'; + el.style.border = '0'; + el.style.width = '100%'; + el.style.height = '80px'; + el.style.marginTop = '2px'; + el.style.position = 'relative'; + el.style.display = 'block'; + el.onerror = TCaptcha.onFrameError; + return el; + }, + + buildImgCntNode: function() { + let el = document.createElement('div'); + el.id = 't-cnt'; + el.style.height = '80px'; + el.style.marginTop = '2px'; + el.style.position = 'relative'; + return el; + }, + + buildImgNode: function(id) { + let el = document.createElement('div'); + el.id = 't-' + id; + el.style.width = '100%'; + el.style.height = '100%'; + el.style.position = 'absolute'; + el.style.backgroundRepeat = 'no-repeat'; + el.style.backgroundPosition = 'top left'; + el.style.pointerEvents = 'none'; + return el; + }, + + buildMsgNode: function() { + let el = document.createElement('div'); + el.id = 't-msg'; + el.style.width = '100%'; + el.style.height = 'calc(100% - 20px)'; + el.style.position = 'absolute'; + el.style.top = '20px'; + el.style.textAlign = 'center'; + el.style.fontSize = '12px'; + el.style.filter = 'inherit'; + el.style.display = 'none'; + el.style.alignContent = 'center'; + return el; + }, + + buildRespField: function() { + let el = document.createElement('input'); + el.id = 't-resp'; + el.name = 't-response'; + el.placeholder = 'Type the CAPTCHA here'; + el.setAttribute('autocomplete', 'off'); + el.type = 'text'; + el.style.width = '160px'; + el.style.boxSizing = 'border-box'; + el.style.textTransform = 'uppercase'; + el.style.fontSize = '11px'; + el.style.height = '18px'; + el.style.margin = '0'; + el.style.padding = '0 2px'; + el.style.fontFamily = 'monospace'; + el.style.verticalAlign = 'middle'; + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex + 2); + } + return el; + }, + + buildSliderNode: function() { + let el = document.createElement('input'); + el.id = 't-slider'; + el.setAttribute('autocomplete', 'off'); + el.type = 'range'; + el.style.width = '100%'; + el.style.boxSizing = 'border-box'; + el.style.visibility = 'hidden'; + el.style.margin = '0'; + el.style.transition = 'box-shadow 15s linear'; + el.style.boxShadow = '0 0 6px 4px #1d8dc4'; + el.style.position = 'relative'; + el.value = 0; + el.min = 0; + el.max = 100; + el.addEventListener('input', this.onSliderInput, false); + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex + 1); + } + return el; + }, + + buildChallengeNode: function() { + let el = document.createElement('input'); + el.name = 't-challenge'; + el.type = 'hidden'; + return el; + }, + + buildReloadNode: function(board, thread_id) { + let el = document.createElement('button'); + el.id = 't-load'; + el.type = 'button'; + el.style.fontSize = '11px'; + el.style.padding = '0'; + el.style.width = '90px'; + el.style.boxSizing = 'border-box'; + el.style.margin = '0 6px 0 0'; + el.style.verticalAlign = 'middle'; + el.style.height = '18px'; + el.textContent = 'Get Captcha'; + el.setAttribute('data-board', board); + el.setAttribute('data-tid', thread_id); + el.addEventListener('click', this.onReloadClick, false); + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex); + } + return el; + }, + + buildHelpNode: function() { + let el = document.createElement('button'); + el.id = 't-help'; + el.type = 'button'; + el.style.fontSize = '11px'; + el.style.padding = '0'; + el.style.width = '20px'; + el.style.boxSizing = 'border-box'; + el.style.margin = '0 0 0 6px'; + el.style.verticalAlign = 'middle'; + el.style.height = '18px'; + el.textContent = '?'; + el.setAttribute('data-tip', 'Help'); + el.tabIndex = -1; + el.addEventListener('click', this.onHelpClick, false); + return el; + }, + + onHelpClick: function() { + let str = `- Only type letters and numbers displayed in the image. +- If needed, use the slider to align the image to make it readable. +- Make sure to not block any cookies set by 4chan.`; + alert(str); + }, + + onTicketCaptchaError: function() { + TCaptcha.toggleMsgOverlay(true, "Couldn't load the captcha.

    Please check your browser's content blocker."); + }, + + onTicketCaptchaDone: function(resp) { + TCaptcha.reloadNode.setAttribute('data-ticket-resp', resp); + TCaptcha.destroyTicketCaptcha(); + TCaptcha.onReloadClick(); + }, + + loadTicketCaptcha: function() { + window.pcd_c_loaded = TCaptcha.buildTicketCaptcha; + window.pcd_c_done = TCaptcha.onTicketCaptchaDone; + TCaptcha.toggleMsgOverlay(true, 'Loading…'); + let s = document.createElement('script'); + s.src = 'https://js.hcaptcha.com/1/api.js?onload=pcd_c_loaded&render=explicit&recaptchacompat=off'; + s.onerror = TCaptcha.onTicketCaptchaError; + document.head.appendChild(s); + }, + + buildTicketCaptcha: function() { + let self = TCaptcha; + + self.toggleMsgOverlay(false); + + if (!window.hcaptcha) { + self.loadTicketCaptcha(); + return; + } + + let el = document.createElement('div'); + el.id = 't-tc-cnt'; + self.imgCntNode.appendChild(el); + + let wid = window.hcaptcha.render('t-tc-cnt', { + sitekey: self.hCaptchaSiteKey, + callback: 'pcd_c_done' + }); + + el.setAttribute('data-wid', wid); + + self.ticketCaptchaNode = el; + }, + + destroyTicketCaptcha: function() { + let self = TCaptcha; + + if (!window.hcaptcha || !self.ticketCaptchaNode) { + return; + } + + let wid = self.ticketCaptchaNode.getAttribute('data-wid'); + window.hcaptcha.reset(wid); + self.imgCntNode.removeChild(self.ticketCaptchaNode); + self.ticketCaptchaNode = null; + }, + + onReloadClick: function() { + let btn = TCaptcha.reloadNode; + let board = btn.getAttribute('data-board'); + let thread_id = btn.getAttribute('data-tid'); + let ticket_resp = btn.getAttribute('data-ticket-resp'); + btn.removeAttribute('data-ticket-resp'); + TCaptcha.toggleReloadBtn(false, 'Loading'); + TCaptcha.load(board, thread_id, ticket_resp); + }, + + onFrameMessage: function(e) { + if (e.origin !== `https://sys.${TCaptcha.domain}`) { + return; + } + + if (e.data && e.data.twister) { + TCaptcha.destroyFrame(); + TCaptcha.buildFromJson(e.data.twister); + } + }, + + onFrameError: function(e) { + TCaptcha.unlockReloadBtn(); + + console.log(e); + + if (TCaptcha.errorCb) { + TCaptcha.errorCb.call(null, + "Couldn't load the captcha frame. Check your content blocker settings." + ); + } + }, + + load: function(board, thread_id, ticket_resp) { + let self = TCaptcha; + + clearTimeout(self.frameTimeout); + clearTimeout(self.reloadTimeout); + clearTimeout(self.expireTimeout); + + let params = ['framed=1']; + + if (board) { + params.push('board=' + board); + } + + if (thread_id > 0) { + params.push('thread_id=' + thread_id); + } + + if (ticket_resp) { + params.push('ticket_resp=' + encodeURIComponent(ticket_resp)); + } + + let ticket = self.getTicket(); + + if (ticket) { + params.push('ticket=' + ticket); + } + + if (params.length > 0) { + params = '?' + params.join('&'); + } + + let src = 'https://sys.' + self.domain + self.path + params; + + self.frameNode = self.buildFrameNode(); + self.toggleImgCntNode(false); + self.node.insertBefore(self.frameNode, self.imgCntNode); + self.frameTimeout = setTimeout(self.onFrameTimeout, 60000, src); + self.frameNode.src = src; + }, + + onFrameTimeout: function(src) { + let self = TCaptcha; + + self.destroyFrame(); + + console.log('Captcha frame timeout'); + + if (self.errorCb) { + self.errorCb.call(null, `Couldn't get the captcha. +Make sure your browser doesn't block content on 4chan then click +here.`); + } + }, + + destroyFrame: function() { + let self = TCaptcha; + + clearTimeout(self.frameTimeout); + self.frameTimeout = null; + if (self.frameNode) { + self.frameNode.remove(); + self.frameNode = null; + } + self.toggleImgCntNode(true); + self.unlockReloadBtn(); + }, + + unlockReloadBtn: function() { + TCaptcha.reloadTs = null; + TCaptcha.toggleReloadBtn(true, 'Get Captcha'); + }, + + toggleReloadBtn: function(flag, label) { + let self = TCaptcha; + + if (self.reloadNode) { + self.reloadNode.disabled = !flag; + + if (label !== undefined) { + self.reloadNode.textContent = label; + } + } + }, + + onCaptchaFailed: function() { + let self = TCaptcha; + + let cd = self.failCd * 1000; + + if (self.reloadTs && self.reloadTs < cd) { + self.setReloadCd(cd, true); + } + }, + + setReloadCd: function(cd, visible, onDone) { + let self = TCaptcha; + + if (!self.node) { + return; + } + + clearTimeout(self.reloadTimeout); + + self.onReloadCdDone = onDone; + + self.pcdBypassable = visible === -1; + + if (cd) { + self.toggleReloadBtn(false); + if (visible) { + self.reloadTs = Date.now() + cd; + self.onReloadCdTick(); + } + else { + self.reloadTimeout = setTimeout(self.stopReloadCd, cd); + } + } + else { + self.stopReloadCd(); + } + }, + + stopReloadCd: function() { + let self = TCaptcha; + self.unlockReloadBtn(); + if (self.onReloadCdDone) { + self.onReloadCdDone.call(self); + } + }, + + onReloadCdTick: function() { + let self = TCaptcha; + + if (!self.reloadNode || !self.reloadTs) { + return; + } + + let cd = self.reloadTs - Date.now(); + + if (self.pcdBypassable) { + if (document.cookie.indexOf('_ev1=') !== -1) { + cd = 0; + } + } + + if (cd > 0) { + self.reloadNode.textContent = Math.ceil(cd / 1000); + self.reloadTimeout = setTimeout(self.onReloadCdTick, Math.min(cd, 1000)); + } + else { + self.stopReloadCd(); + } + }, + + clearChallenge: function() { + let self = TCaptcha; + + if (self.node) { + self.challengeNode.value = ''; + self.respNode.value = ''; + self.fgNode.style.backgroundImage = ''; + self.bgNode.style.backgroundImage = ''; + self.toggleSlider(false); + self.toggleMsgOverlay(false); + } + }, + + toggleSlider: function(flag) { + TCaptcha.sliderNode.style.visibility = flag ? '' : 'hidden'; + TCaptcha.sliderNode.style.boxShadow = flag ? '' : '0 0 4px 2px #1d8dc4'; + }, + + toggleMsgOverlay: function(flag, txt) { + if (txt !== undefined) { + TCaptcha.msgNode.innerHTML = `
    ${txt}
    `; + } + TCaptcha.msgNode.style.display = flag ? 'grid' : 'none'; + }, + + onSliderInput: function() { + var m = -Math.floor((+this.value) / 100 * this.twisterDelta); + TCaptcha.bgNode.style.backgroundPositionX = m + 'px'; + }, + + onTicketPcdTick: function() { + let self = TCaptcha; + + let el = document.getElementById('t-pcd'); + + if (!el) { + return; + } + + let pcd = +el.getAttribute('data-pcd'); + + pcd = pcd - (0 | (Date.now() / 1000)); + + if (pcd <= 0) { + self.onTicketPcdEnd(); + return; + } + + el.textContent = pcd; + + setTimeout(self.updateTicketPcd, 1000); + }, + + clearTicketOverlay: function() { + TCaptcha.toggleMsgOverlay(false); + }, + + buildFromJson: function(data) { + let self = TCaptcha; + + if (!self.node) { + return; + } + + self.unlockReloadBtn(); + self.toggleSlider(false); + self.toggleMsgOverlay(false); + + self.setTicket(data.ticket); + + if (TCaptcha.errorCb) { + TCaptcha.errorCb.call(null, ''); + } + + if (data.cd) { + self.setReloadCd(data.cd * 1000, !data.challenge); + } + + if (data.mpcd) { + self.clearChallenge(); + self.destroyTicketCaptcha(); + self.buildTicketCaptcha(); + return; + } + + if (data.pcd) { + self.buildTicket(data); + return; + } + + if (data.error) { + console.log(data.error); + + if (TCaptcha.errorCb) { + TCaptcha.errorCb.call(null, data.error); + } + + return; + } + + self.imgCntNode.style.width = data.img_width + 'px'; + self.imgCntNode.style.height = data.img_height + 'px'; + + self.challengeNode.value = data.challenge; + + self.expireTimeout = setTimeout(self.clearChallenge, data.ttl * 1000 - 3000); + + if (data.bg_width) { + self.buildTwister(data); + } + else if (data.img) { + self.buildStatic(data); + } + else { + self.buildNoop(data); + } + }, + + buildTwister: function(data) { + let self = TCaptcha; + + self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')'; + self.bgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.bg + ')'; + + self.bgNode.style.backgroundPositionX = '0px'; + + self.toggleSlider(true); + self.sliderNode.value = 0; + self.sliderNode.twisterDelta = data.bg_width - data.img_width; + self.sliderNode.focus(); + }, + + buildStatic: function(data) { + let self = TCaptcha; + self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')'; + self.bgNode.style.backgroundImage = ''; + }, + + buildTicket: function(data) { + let self = TCaptcha; + self.toggleMsgOverlay(true, data.pcd_msg || 'Please wait a while.'); + self.fgNode.style.backgroundImage = ''; + self.bgNode.style.backgroundImage = ''; + self.setReloadCd(data.pcd * 1000, data.bpcd ? -1 : true, self.clearTicketOverlay); + }, + + buildNoop: function(data) { + let self = TCaptcha; + self.toggleMsgOverlay(true, 'Verification not required.'); + self.fgNode.style.backgroundImage = ''; + self.bgNode.style.backgroundImage = ''; + } +}; + +/** + * Tooltips + */ +var Tip = { + node: null, + timeout: null, + delay: 300, + + init: function() { + document.addEventListener('mouseover', this.onMouseOver, false); + document.addEventListener('mouseout', this.onMouseOut, false); + }, + + onMouseOver: function(e) { + var cb, data, t; + + t = e.target; + + if (Tip.timeout) { + clearTimeout(Tip.timeout); + Tip.timeout = null; + } + + if (t.hasAttribute('data-tip')) { + data = null; + + if (t.hasAttribute('data-tip-cb')) { + cb = t.getAttribute('data-tip-cb'); + if (window[cb]) { + data = window[cb](t); + } + } + Tip.timeout = setTimeout(Tip.show, Tip.delay, e.target, data); + } + }, + + onMouseOut: function(e) { + if (Tip.timeout) { + clearTimeout(Tip.timeout); + Tip.timeout = null; + } + + Tip.hide(); + }, + + show: function(t, data, pos) { + var el, rect, style, left, top; + + rect = t.getBoundingClientRect(); + + el = document.createElement('div'); + el.id = 'tooltip'; + + if (data) { + el.innerHTML = data; + } + else { + el.textContent = t.getAttribute('data-tip'); + } + + if (!pos) { + pos = 'top'; + } + + el.className = 'tip-' + pos; + + document.body.appendChild(el); + + left = rect.left - (el.offsetWidth - t.offsetWidth) / 2; + + if (left < 0) { + left = rect.left + 2; + el.className += '-right'; + } + else if (left + el.offsetWidth > document.documentElement.clientWidth) { + left = rect.left - el.offsetWidth + t.offsetWidth + 2; + el.className += '-left'; + } + + top = rect.top - el.offsetHeight - 5; + + style = el.style; + style.top = (top + window.pageYOffset) + 'px'; + style.left = left + window.pageXOffset + 'px'; + + Tip.node = el; + }, + + hide: function() { + if (Tip.node) { + document.body.removeChild(Tip.node); + Tip.node = null; + } + } +}; + +/** + * Settings Syncher + */ +/* +var StorageSync = { + queue: [], + + init: function() { + var el, self = StorageSync; + + if (self.inited || !document.body) { + return; + } + + self.remoteFrame = null; + + self.remoteOrigin = location.protocol + '//boards.' + + (location.host === 'boards.4channel.org' ? '4chan' : '4channel') + + '.org'; + + window.addEventListener('message', self.onFrameMessage, false); + + el = document.createElement('iframe'); + el.width = 0; + el.height = 0; + el.style.display = 'none'; + el.style.visibility = 'hidden'; + + el.src = self.remoteOrigin + '/syncframe.html'; + + document.body.appendChild(el); + + self.inited = true; + }, + + onFrameMessage: function(e) { + var self = StorageSync; + + if (e.origin !== self.remoteOrigin) { + return; + } + + if (e.data === 'ready') { + self.remoteFrame = e.source; + + if (self.queue.length) { + self.send(); + } + + return; + } + }, + + sync: function(key) { + var self = StorageSync; + + self.queue.push(key); + self.send(); + }, + + send: function() { + var i, key, data, self = StorageSync; + + if (!self.inited) { + return self.init(); + } + + if (!self.remoteFrame) { + return; + } + + data = {}; + + for (i = 0; key = self.queue[i]; ++i) { + data[key] = localStorage.getItem(key); + } + + self.queue = []; + + self.remoteFrame.postMessage({ storage: data }, self.remoteOrigin); + } +}; +*/ +function mShowFull(t) { + var el, data; + + if (t.className === 'name') { + if (el = t.parentNode.parentNode.parentNode + .getElementsByClassName('name')[1]) { + data = el.innerHTML; + } + } + else if (t.parentNode.className === 'subject') { + if (el = t.parentNode.parentNode.parentNode.parentNode + .getElementsByClassName('subject')[1]) { + data = el.innerHTML; + } + } + else if (/fileThumb/.test(t.parentNode.className)) { + if (el = t.parentNode.parentNode.getElementsByClassName('fileText')[0]) { + el = el.firstElementChild; + data = el.getAttribute('title') || el.innerHTML; + } + } + + return data; +} + +function loadBannerImage() { + var cnt = document.getElementById('bannerCnt'); + + if (!cnt || cnt.offsetWidth <= 0) { + return; + } + + cnt.innerHTML = '4chan'; +} + +function onMobileSelectChange() { + var board, page; + + board = this.options[this.selectedIndex].value; + page = (board !== 'f' && /\/catalog$/.test(location.pathname)) ? 'catalog' : ''; + + window.location = '//boards.' + $L.d(board) + '/' + board + '/' + page; +} + +function buildMobileNav() { + var el, boards, i, b, html, order; + + if (el = document.getElementById('boardSelectMobile')) { + html = ''; + order = []; + + boards = document.querySelectorAll('#boardNavDesktop .boardList a'); + + for (i = 0; b = boards[i]; ++i) { + order.push(b); + } + + order.sort(function(a, b) { + if (a.textContent < b.textContent) { + return -1; + } + if (a.textContent > b.textContent) { + return 1; + } + return 0; + }); + + for (i = 0; b = order[i]; ++i) { + html += ''; + } + + el.innerHTML = html; + } +} + +function cloneTopNav() { + var navT, navB, ref, el; + + navT = document.getElementById('boardNavDesktop'); + + if (!navT) { + return; + } + + ref = document.getElementById('absbot'); + + navB = navT.cloneNode(true); + navB.id = navB.id + 'Foot'; + + if (el = navB.querySelector('#navtopright')) { + el.id = 'navbotright'; + } + + if (el = navB.querySelector('#settingsWindowLink')) { + el.id = el.id + 'Bot'; + } + + document.body.insertBefore(navB, ref); +} + +function initPass() { + if (get_cookie("pass_enabled") == '1' || get_cookie('extra_path')) { + window.passEnabled = true; + } + else { + window.passEnabled = false; + } +} + +function initBlotter() { + var mTime, seenTime, el; + + el = document.getElementById('toggleBlotter'); + + if (!el) { + return; + } + + el.addEventListener('click', toggleBlotter, false); + + seenTime = localStorage.getItem('4chan-blotter'); + + if (!seenTime) { + return; + } + + mTime = +el.getAttribute('data-utc'); + + if (mTime <= +seenTime) { + toggleBlotter(); + } +} + +function toggleBlotter(e) { + var el, btn; + + e && e.preventDefault(); + + el = document.getElementById('blotter-msgs'); + + if (!el) { + return; + } + + btn = document.getElementById('toggleBlotter'); + + if (el.style.display == 'none') { + el.style.display = ''; + localStorage.removeItem('4chan-blotter'); + btn.textContent = 'Hide'; + + el = btn.nextElementSibling; + + if (el.style.display) { + el.style.display = ''; + } + } + else { + el.style.display = 'none'; + localStorage.setItem('4chan-blotter', btn.getAttribute('data-utc')); + btn.textContent = 'Show Blotter'; + btn.nextElementSibling.style.display = 'none'; + } +} + +function onRecaptchaLoaded() { + if (document.getElementById('postForm').style.display == 'table') { + initRecaptcha(); + } +} + +function initRecaptcha() { + var el; + + el = document.getElementById('g-recaptcha'); + + if (!el || el.firstElementChild) { + return; + } + + if (!window.passEnabled && window.grecaptcha) { + grecaptcha.render(el, { + sitekey: window.recaptchaKey, + theme: (activeStyleSheet === 'Tomorrow' || window.dark_captcha) ? 'dark' : 'light' + }); + } +} + +function initTCaptcha() { + let el = document.getElementById('t-root'); + + if (el) { + let board = location.pathname.split(/\//)[1]; + + let thread_id; + + if (document.forms.post && document.forms.post.resto) { + thread_id = +document.forms.post.resto.value; + } + else { + thread_id = 0; + } + + TCaptcha.init(el, board, thread_id, 5); + TCaptcha.setErrorCb(window.showPostFormError); + } +} + +function initAnalytics() { + var tid = location.host.indexOf('.4channel.org') !== -1 ? 'UA-166538-5' : 'UA-166538-1'; + + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + + ga('create', tid, {'sampleRate': 1}); + ga('set', 'anonymizeIp', true); + ga('send','pageview'); +} + +function initAdsPF(cnt, slot_id) { + let sid, nid; + + if (slot_id == 1) { + sid = '657b2d8958f9186175770b1f'; + nid = 'pf-6892-1'; + } + else if (slot_id == 2) { + sid = '657b2d9d58f9186175770b37'; + nid = 'pf-6893-1'; + } + else if (slot_id == 3) { + sid = '657b2d56256794003cd16fe4'; + nid = 'pf-6890-1'; + } + else if (slot_id == 4) { + sid = '657b2d74256794003cd17019'; + nid = 'pf-6891-1'; + } + else { + return; + } + + cnt.innerHTML = ''; + + let d = document.createElement('div'); + d.id = nid; + cnt.appendChild(d); + + window.pubfuturetag = window.pubfuturetag || []; + window.pubfuturetag.push({unit: sid, id: nid}); +} + +function initAdsADT(scope) { + var el, nodes, i, cls, s; + + if (window.matchMedia && window.matchMedia('(max-width: 480px)').matches && localStorage.getItem('4chan_never_show_mobile') != 'true') { + cls = 'adg-m'; + } + else { + cls = 'adg'; + } + + nodes = (scope || document).getElementsByClassName(cls); + + for (i = 0; el = nodes[i]; ++i) { + if (el.hasAttribute('data-abc')) { + s = document.createElement('iframe'); + s.setAttribute('scrolling', 'no'); + s.setAttribute('frameborder', '0'); + s.setAttribute('allowtransparency', 'true'); + s.setAttribute('marginheight', '0'); + s.setAttribute('marginwidth', '0'); + + if (cls === 'adg') { + s.setAttribute('width', '728'); + s.setAttribute('height', '90'); + } + else { + s.setAttribute('width', '300'); + s.setAttribute('height', '250'); + } + + s.setAttribute('name', 'spot_id_' + el.getAttribute('data-abc')); + s.src = 'https://a.adtng.com/get/' + el.getAttribute('data-abc') + '?time=' + Date.now(); + el.appendChild(s); + } + } +} + +function danboAddSlot(n, b, m, s) { + let pubid = 27; + + let el = document.createElement('div'); + el.className = 'danbo_dta'; + + if (m) { + if (s) { + s = '3'; + } + else { + s = '4'; + el.id = 'js-danbo-rld'; + } + el.setAttribute('data-danbo', `${pubid}-${b}-${s}-300-250`); + el.classList.add('danbo-m'); + } + else { + if (s) { + s = '1'; + } + else { + s = '2'; + el.id = 'js-danbo-rld'; + } + el.setAttribute('data-danbo', `${pubid}-${b}-${s}-728-90`); + el.classList.add('danbo-d'); + } + + n.appendChild(el); + + return el; +} + +function initAdsDanbo() { + if (!window.Danbo) { + return; + } + + let b = location.pathname.split(/\//)[1] || '_'; + + let m = window.matchMedia && window.matchMedia('(max-width: 480px)').matches; + + let nodes = document.getElementsByClassName('danbo-slot'); + + for (let cnt of nodes) { + let s = cnt.id === 'danbo-s-t'; + danboAddSlot(cnt, b, m, s); + } + + window.addEventListener('message', function(e) { + if (e.origin === 'https://hakurei.cdnbo.org' && e.data && e.data.origin === 'danbo') { + window.initAdsFallback(e.data.unit_id); + } + }); + + window.Danbo.initialize(); +} + +function reloadAdsDanbo() { + let cnt = document.getElementById('danbo-s-b'); + + if (!cnt) { + return; + } + + cnt.innerHTML = ''; + + let b = 'a';//location.pathname.split(/\//)[1] || '_'; + + let m = window.matchMedia && window.matchMedia('(max-width: 480px)').matches; + + danboAddSlot(cnt, b, m, false); + + window.Danbo.reload('js-danbo-rld'); +} + +function initAdsFallback(slot_id) { + let fb = window.danbo_fb; + + let cnt_id; + + if (slot_id == 1 || slot_id == 3) { + cnt_id = 'danbo-s-t'; + } + else { + cnt_id = 'danbo-s-b'; + } + + let cnt = document.getElementById(cnt_id); + + if (!cnt) { + return; + } + + let is_burichan = document.body.classList.contains('ws'); + + let hr = is_burichan ? 0.1 : 0.01; + + if (Math.random() < hr) { + return initAdsHome(cnt); + } + + if (cnt_id === 'danbo-s-t') { + if (is_burichan) { + initAdsPF(cnt, slot_id); + } + else if (fb) { + if (slot_id == 1 && fb.t_abc_d) { + cnt.innerHTML = `
    `; + initAdsADT(cnt); + } + else if (slot_id == 3 && fb.t_abc_m) { + cnt.innerHTML = `
    `; + initAdsADT(cnt); + } + else { + initAdsHome(cnt); + } + } + else { + initAdsHome(cnt); + } + } + else if (cnt_id === 'danbo-s-b') { + if (is_burichan) { + initAdsPF(cnt, slot_id); + } + else if (fb) { + if (slot_id == 4 && fb.b_abc_m) { + cnt.innerHTML = `
    `; + initAdsADT(cnt); + } + else { + initAdsHome(cnt); + } + } + else { + initAdsHome(cnt); + } + } + else { + console.log('Fallback', slot_id); + } +} + +function initAdsHome(cnt) { + let banners = [ + ['advertise', '1.png', '2.png', '3.png'], + ['pass', '4.png'], + ]; + + let banners_m = [ + ['advertise', '1m.png'], + ]; + + let d; + + if (location.host.indexOf('4channel')) { + d = '4channel'; + } + else { + d = '4chan'; + } + + let b; + + if (window.matchMedia && window.matchMedia('(max-width: 480px)').matches) { + b = banners_m; + } + else { + b = banners; + } + + b = b[Math.floor(Math.random() * b.length)]; + + let href = b[0]; + let src = b[1 + Math.floor(Math.random() * (b.length - 1))]; + + let a = document.createElement('a'); + a.href = `https://www.${d}.org/${href}`; + a.target = '_blank'; + + let img = document.createElement('img'); + img.src = '//s.4cdn.org/image/banners/' + src; + + a.appendChild(img); + + if (cnt.children.length) { + cnt.innerHTML = ''; + } + + cnt.appendChild(a); +} + +function applySearch(e) { + var str; + + e && e.preventDefault(); + + str = document.getElementById('search-box').value; + + if (str !== '') { + window.location.href = 'catalog#s=' + str; + } +} + +function onKeyDownSearch(e) { + if (e.keyCode == 13) { + applySearch(); + } +} + +function onReportClick(e) { + var i, input, nodes, board; + + nodes = document.getElementsByTagName('input'); + + board = location.pathname.split(/\//)[1]; + + for (i = 0; input = nodes[i]; ++i) { + if (input.type == 'checkbox' && input.checked && input.value == 'delete') { + return reppop('https://sys.' + $L.d(board) + '/' + board + '/imgboard.php?mode=report&no=' + + input.name.replace(/[a-z]+/, '') + ); + } + } +} + +function onStyleSheetChange(e) { + setActiveStyleSheet(this.value); +} + +function onPageSwitch(e) { + e.preventDefault(); + window.location = this.action; +} + +function onMobileFormClick(e) { + var index = location.pathname.split(/\//).length < 4; + + e.preventDefault(); + + if (window.QR && Main.tid && QR.enabled) { + QR.show(Main.tid); + } + else if (this.parentNode.id == 'mpostform') { + toggleMobilePostForm(index); + } + else { + toggleMobilePostForm(index, 1); + } +} + +function onMobileRefreshClick(e) { + locationHashChanged(this); +} + +function toggle(name) { + var a = document.getElementById(name); + a.style.display = ((a.style.display != 'block') ? 'block' : 'none'); +} + +function quote(text) { + if (document.selection) { + document.post.com.focus(); + var sel = document.selection.createRange(); + sel.text = ">>" + text + "\n"; + } else if (document.post.com.selectionStart || document.post.com.selectionStart == "0") { + var startPos = document.post.com.selectionStart; + var endPos = document.post.com.selectionEnd; + document.post.com.value = document.post.com.value.substring(0, startPos) + ">>" + text + "\n" + document.post.com.value.substring(endPos, document.post.com.value.length); + } else { + document.post.com.value += ">>" + text + "\n"; + } +} + +function repquote(rep) { + if (document.post.com.value == "") { + quote(rep); + } +} + +function reppop(url) { + var height; + + if (window.passEnabled || !window.grecaptcha) { + height = 205; + } + else { + height = 510; + } + + window.open(url, Date.now(), + 'toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height=' + height + ); + + return false; +} + +function recaptcha_load() { + var d = document.getElementById("recaptcha_div"); + if (!d) return; + + Recaptcha.create("6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc", "recaptcha_div",{theme: "clean"}); +} + +function onParsingDone(e) { + var i, nodes, n, p, tid, offset, limit; + + tid = e.detail.threadId; + offset = e.detail.offset; + + if (!offset) { + return; + } + + nodes = document.getElementById('t' + tid).getElementsByClassName('nameBlock'); + limit = e.detail.limit ? (e.detail.limit * 2) : nodes.length; + for (i = offset * 2 + 1; i < limit; i+=2) { + if (n = nodes[i].children[1]) { + if (currentHighlighted + && n.className.indexOf('id_' + currentHighlighted) != -1) { + p = n.parentNode.parentNode.parentNode; + p.className = 'highlight ' + p.className; + } + n.addEventListener('click', idClick, false); + } + } +} + +function loadExtraScripts() { + var el, path; + + path = readCookie('extra_path'); + + if (!path || !/^[a-z0-9]+$/.test(path)) { + return false; + } + + if (window.FC) { + el = document.createElement('script'); + el.type = 'text/javascript'; + el.src = 'https://s.4cdn.org/js/' + path + '.' + jsVersion + '.js'; + document.head.appendChild(el); + } + else { + document.write(''); + } + + return true; +} + + +function toggleMobilePostForm(index, scrolltotop) { + var elem = document.getElementById('mpostform').firstElementChild; + var postForm = document.getElementById('postForm'); + + if (elem.className.match('hidden')) { + elem.className = elem.className.replace('hidden', 'shown'); + postForm.className = postForm.className.replace(' hideMobile', ''); + elem.innerHTML = 'Close Post Form'; + initRecaptcha(); + initTCaptcha(); + checkIncognito(); + } + else { + elem.className = elem.className.replace('shown', 'hidden'); + postForm.className += ' hideMobile'; + elem.innerHTML = (index) ? 'Start New Thread' : 'Post Reply'; + } + + if (scrolltotop) { + elem.scrollIntoView(); + } +} + +function toggleGlobalMessage(e) { + var elem, postForm; + + if (e) { + e.preventDefault(); + } + + elem = document.getElementById('globalToggle'); + postForm = document.getElementById('globalMessage'); + + if( elem.className.match('hidden') ) { + elem.className = elem.className.replace('hidden', 'shown'); + postForm.className = postForm.className.replace(' hideMobile', ''); + + elem.innerHTML = 'Close Announcement'; + } else { + elem.className = elem.className.replace('shown', 'hidden'); + postForm.className += ' hideMobile'; + + elem.innerHTML = 'View Announcement'; + } +} + +function checkRecaptcha() +{ + if( typeof RecaptchaState.timeout != 'undefined' ) { + if( RecaptchaState.timeout == 1800 ) { + RecaptchaState.timeout = 570; + Recaptcha._reset_timer(); + clearInterval(captchainterval); + } + } +} + +function setPassMsg() { + var el, msg; + + el = document.getElementById('captchaFormPart'); + + if (!el) { + return; + } + + msg = 'You are using a 4chan Pass. [Logout]'; + el.children[1].innerHTML = '
    ' + msg + '
    '; +} + +function confirmPassLogout(event) +{ + var conf = confirm('Are you sure you want to logout?'); + if( !conf ) { + event.preventDefault(); + return false; + } +} + +var activeStyleSheet; + +function initStyleSheet() { + var i, rem, link, len; + + // fix me + if (window.FC) { + return; + } + + if (window.style_group) { + var cookie = readCookie(style_group); + activeStyleSheet = cookie ? cookie : getPreferredStyleSheet(); + } + + if (window.css_event && localStorage.getItem('4chan_stop_css_event') !== `${window.css_event}-${window.css_event_v}`) { + activeStyleSheet = '_special'; + } + + switch(activeStyleSheet) { + case "Yotsuba B": + setActiveStyleSheet("Yotsuba B New", true); + break; + + case "Yotsuba": + setActiveStyleSheet("Yotsuba New", true); + break; + + case "Burichan": + setActiveStyleSheet("Burichan New", true); + break; + + case "Futaba": + setActiveStyleSheet("Futaba New", true); + break; + + default: + setActiveStyleSheet(activeStyleSheet, true); + break; + } + + if (localStorage.getItem('4chan_never_show_mobile') == 'true') { + link = document.querySelectorAll('link'); + len = link.length; + for (i = 0; i < len; i++) { + if (link[i].getAttribute('href').match('mobile')) { + (rem = link[i]).parentNode.removeChild(rem); + } + } + } +} + +function pageHasMath() { + var i, el, nodes; + + nodes = document.getElementsByClassName('postMessage'); + + for (i = 0; el = nodes[i]; ++i) { + if (/\[(?:eqn|math)\]|"math">/.test(el.innerHTML)) { + return true; + } + } + + return false; +} + +function cleanWbr(el) { + var i, nodes, n; + + nodes = el.getElementsByTagName('wbr'); + + for (i = nodes.length - 1; n = nodes[i]; i--) { + n.parentNode.removeChild(n); + } +} + +function parseMath() { + var i, el, nodes; + + nodes = document.getElementsByClassName('postMessage'); + + for (i = 0; el = nodes[i]; ++i) { + if (/\[(?:eqn|math)\]/.test(el.innerHTML)) { + cleanWbr(el); + } + } + + MathJax.Hub.Queue(['Typeset', MathJax.Hub, nodes]); +} + +function loadMathJax() { + var head, script; + + head = document.getElementsByTagName('head')[0]; + + script = document.createElement('script'); + script.type = 'text/x-mathjax-config'; + script.text = "MathJax.Hub.Config({\ +extensions: ['Safe.js'],\ +tex2jax: { processRefs: false, processEnvironments: false, preview: 'none', inlineMath: [['[math]','[/math]']], displayMath: [['[eqn]','[/eqn]']] },\ +Safe: { allow: { URLs: 'none', classes: 'none', cssIDs: 'none', styles: 'none', fontsize: 'none', require: 'none' } },\ +displayAlign: 'left', messageStyle: 'none', skipStartupTypeset: true,\ +'CHTML-preview': { disabled: true }, MathMenu: { showRenderer: false, showLocale: false },\ +TeX: { Macros: { color: '{}', newcommand: '{}', renewcommand: '{}', newenvironment: '{}', renewenvironment: '{}', def: '{}', let: '{}'}}});"; + head.appendChild(script); + + script = document.createElement('script'); + script.src = '//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-AMS_HTML-full'; + script.onload = parseMath; + head.appendChild(script); +} + +captchainterval = null; +function init() { + var el, i; + var error = typeof is_error != "undefined"; + var board = location.href.match(/(?:4chan|4channel)\.org\/(\w+)/)[1]; + var arr = location.href.split(/#/); + if( arr[1] && arr[1].match(/q[0-9]+$/) ) { + repquote( arr[1].match(/q([0-9]+)$/)[1] ); + } + + + if (window.math_tags && pageHasMath()) { + loadMathJax(); + } + + if(navigator.userAgent) { + if( navigator.userAgent.match( /iP(hone|ad|od)/i ) ) { + links = document.querySelectorAll('s'); + len = links.length; + + for(i = 0; i < len; i++ ) { + links[i].onclick = function() { + if (this.hasAttribute('style')) { + this.removeAttribute('style'); + } + else { + this.setAttribute('style', 'color: #fff!important;'); + } + }; + } + } + } + + if( document.getElementById('styleSelector') ) { + styleSelect = document.getElementById('styleSelector'); + len = styleSelect.options.length; + for (i = 0; i < len; i++) { + if (styleSelect.options[i].value == activeStyleSheet) { + styleSelect.selectedIndex = i; + continue; + } + } + } + + if (!error && document.forms.post) { + if (board != 'i' && board != 'ic' && board != 'f') { + if (window.File && window.FileReader && window.FileList && window.Blob) { + el = document.getElementById('postFile'); + el && el.addEventListener('change', handleFileSelect, false); + } + } + } + + //window.addEventListener('onhashchange', locationHashChanged, false); + + if( typeof extra != "undefined" && extra && !error ) extra.init(); +} + +var coreLenCheckTimeout = null; +function onComKeyDown() { + clearTimeout(coreLenCheckTimeout); + coreLenCheckTimeout = setTimeout(coreCheckComLength, 500); +} + +function coreCheckComLength() { + var byteLength, comField, error; + + if (comlen) { + comField = document.getElementsByName('com')[0]; + byteLength = encodeURIComponent(comField.value).split(/%..|./).length - 1; + + if (byteLength > comlen) { + if (!(error = document.getElementById('comlenError'))) { + error = document.createElement('div'); + error.id = 'comlenError'; + error.style.cssText = 'font-weight:bold;padding:5px;color:red;'; + comField.parentNode.appendChild(error); + } + error.textContent = 'Error: Comment too long (' + byteLength + '/' + comlen + ').'; + } + else if (error = document.getElementById('comlenError')) { + error.parentNode.removeChild(error); + } + } +} + +function disableMobile() { + localStorage.setItem('4chan_never_show_mobile', 'true'); + location.reload(true); +} + +function enableMobile() { + localStorage.removeItem('4chan_never_show_mobile'); + location.reload(true); +} + +var currentHighlighted = null; +function enableClickableIds() +{ + var i = 0, len = 0; + var elems = document.getElementsByClassName('posteruid'); + var capcode = document.getElementsByClassName('capcode'); + + if( capcode != null ) { + for( i = 0, len = capcode.length; i < len; i++ ) { + capcode[i].addEventListener("click", idClick, false); + } + } + + if( elems == null ) return; + for( i = 0, len = elems.length; i < len; i++ ) { + elems[i].addEventListener("click", idClick, false); + } +} + +function idClick(evt) +{ + var i = 0, len = 0, node; + var uid = evt.target.className == 'hand' ? evt.target.parentNode.className.match(/id_([^ $]+)/)[1] : evt.target.className.match(/id_([^ $]+)/)[1]; + + // remove all .highlight classes + var hl = document.getElementsByClassName('highlight'); + len = hl.length; + for( i = 0; i < len; i++ ) { + var cn = hl[0].className.toString(); + hl[0].className = cn.replace(/highlight /g, ''); + } + + if( currentHighlighted == uid ) { + currentHighlighted = null; + return; + } + currentHighlighted = uid; + + var nhl = document.getElementsByClassName('id_' + uid); + len = nhl.length; + for( i = 0; i < len; i++ ) { + node = nhl[i].parentNode.parentNode.parentNode; + if( !node.className.match(/highlight /) ) node.className = "highlight " + node.className; + } +} + +function showPostFormError(msg) { + var el = document.getElementById('postFormError'); + + if (msg) { + el.innerHTML = msg; + el.style.display = 'block'; + } + else { + el.textContent = ''; + el.style.display = ''; + } +} + +function handleFileSelect() { + var fsize, ftype, maxFilesize; + + if (this.files) { + maxFilesize = window.maxFilesize; + + fsize = this.files[0].size; + ftype = this.files[0].type; + + if (ftype.indexOf('video/') !== -1 && window.maxWebmFilesize) { + maxFilesize = window.maxWebmFilesize; + } + + if (fsize > maxFilesize) { + showPostFormError('Error: Maximum file size allowed is ' + + Math.floor(maxFilesize / 1048576) + ' MB'); + } + else { + showPostFormError(); + } + } +} + +function locationHashChanged(e) +{ + var css = document.getElementById('id_css'); + + switch( e.id ) + { + case 'refresh_top': + url = window.location.href.replace(/#.+/, '#top'); + if( !/top$/.test(url) ) url += '#top'; + css.innerHTML = ''; + document.location.reload(true); + break; + + case 'refresh_bottom': + url = window.location.href.replace(/#.+/, '#bottom'); + if( !/bottom$/.test(url) ) url += '#bottom'; + css.innerHTML = ''; + document.location.reload(true); + break; + + default:break; + } + + return true; + +} + +function setActiveStyleSheet(title, init) { + var a, link, href, i, nodes, fn; + + if( document.querySelectorAll('link[title]').length == 1 ) { + return; + } + + href = ''; + + nodes = document.getElementsByTagName('link'); + + for (i = 0; a = nodes[i]; i++) { + if (a.getAttribute("title") == "switch") { + link = a; + } + + if (a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) { + if (a.getAttribute("title") == title) { + href = a.href; + } + } + } + + link && link.setAttribute("href", href); + + if (!init) { + if (title !== '_special') { + createCookie(style_group, title, 365, location.host.indexOf('4channel.org') === -1 ? '4chan.org' : '4channel.org'); + + if (window.css_event) { + fn = window['fc_' + window.css_event + '_cleanup']; + localStorage.setItem('4chan_stop_css_event', `${window.css_event}-${window.css_event_v}`); + } + } + else if (window.css_event) { + fn = window['fc_' + window.css_event + '_init']; + localStorage.removeItem('4chan_stop_css_event'); + } + + //StorageSync.sync('4chan_stop_css_event'); + + activeStyleSheet = title; + + fn && fn(); + } +} + +function getActiveStyleSheet() { + var i, a; + var link; + + if( document.querySelectorAll('link[title]').length == 1 ) { + return 'Yotsuba P'; + } + + for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { + if (a.getAttribute("title") == "switch") + link = a; + else if (a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title") && a.href==link.href) return a.getAttribute("title"); + } + return null; +} + +function getPreferredStyleSheet() { + return (style_group == "ws_style") ? "Yotsuba B New" : "Yotsuba New"; +} + +function createCookie(name, value, days, domain) { + let expires; + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); + } + else { + expires = ''; + } + + if (domain) { + domain = "; domain=" + domain; + } + else { + domain = ''; + } + + document.cookie = name + "=" + value + expires + "; path=/" + domain; +} + +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return ''; +} + +// legacy +var get_cookie = readCookie; + +function setRetinaIcons() { + var i, j, nodes; + + nodes = document.getElementsByClassName('retina'); + + for (i = 0; j = nodes[i]; ++i) { + j.src = j.src.replace(/\.(gif|png)$/, "@2x.$1"); + } +} + +function onCoreClick(e) { + if (/flag flag-/.test(e.target.className) && e.which == 1) { + window.open('//s.4cdn.org/image/country/' + + e.target.className.match(/flag-([a-z]+)/)[1] + + '.gif', ''); + } +} + +function showPostForm(e) { + var el; + + e && e.preventDefault(); + + if (el = document.getElementById('postForm')) { + $.id('togglePostFormLink').style.display = 'none'; + el.style.display = 'table'; + initRecaptcha(); + initTCaptcha(); + } +} + +function oeCanvasPreview(e) { + var t, el, sel; + + if (el = document.getElementById('oe-canvas-preview')) { + el.parentNode.removeChild(el); + } + + if (e.target.nodeName == 'OPTION' && e.target.value != '0') { + t = document.getElementById('f' + e.target.value); + + if (!t) { + return; + } + + t = t.getElementsByTagName('img')[0]; + + if (!t || !t.hasAttribute('data-md5')) { + return; + } + + el = t.cloneNode(); + el.id = 'oe-canvas-preview'; + sel = e.target.parentNode; + sel.parentNode.insertBefore(el, sel.nextSibling); + } +} + +function oeClearPreview(e) { + var el; + + if (el = document.getElementById('oe-canvas-preview')) { + el.parentNode.removeChild(el); + } +} + +var PainterCore = { + init: function() { + var cnt, btns; + + if (!document.forms.post) { + return; + } + + cnt = document.forms.post.getElementsByClassName('painter-ctrl')[0]; + + if (!cnt) { + return; + } + + btns = cnt.getElementsByTagName('button'); + + if (!btns[1]) { + return; + } + + this.data = null; + this.replayBlob = null; + + this.time = 0; + + this.btnDraw = btns[0]; + this.btnClear = btns[1]; + this.btnFile = document.getElementById('postFile'); + this.btnSubmit = document.forms.post.querySelector('input[type="submit"]'); + this.inputNodes = cnt.getElementsByTagName('input'); + this.replayCb = cnt.getElementsByClassName('oe-r-cb')[0]; + + btns[0].addEventListener('click', this.onDrawClick, false); + btns[1].addEventListener('click', this.onCancel, false); + }, + + onDrawClick: function() { + var w, h, dims = this.parentNode.getElementsByTagName('input'); + + w = +dims[0].value; + h = +dims[1].value; + + if (w < 1 || h < 1) { + return; + } + + window.Keybinds && (Keybinds.enabled = false); + + Tegaki.open({ + onDone: PainterCore.onDone, + onCancel: PainterCore.onCancel, + saveReplay: PainterCore.replayCb && PainterCore.replayCb.checked, + width: w, + height: h + }); + }, + + replay: function(id) { + id = +id; + + Tegaki.open({ + replayMode: true, + replayURL: '//i.4cdn.org/' + location.pathname.split(/\//)[1] + '/' + id + '.tgkr' + }); + }, + + // move this to tegaki.js + b64toBlob: function(data) { + var i, bytes, ary, bary, len; + + bytes = atob(data); + len = bytes.length; + + ary = new Array(len); + + for (i = 0; i < len; ++i) { + ary[i] = bytes.charCodeAt(i); + } + + bary = new Uint8Array(ary); + + return new Blob([bary]); + }, + + onDone: function() { + var self, el; + + self = PainterCore; + + window.Keybinds && (Keybinds.enabled = true); + + self.btnFile.disabled = true; + self.btnClear.disabled = false; + + self.data = Tegaki.flatten().toDataURL('image/png'); + + if (Tegaki.saveReplay) { + self.replayBlob = Tegaki.replayRecorder.toBlob(); + } + + if (!Tegaki.hasCustomCanvas && Tegaki.startTimeStamp) { + self.time = Math.round((Date.now() - Tegaki.startTimeStamp) / 1000); + } + else { + self.time = 0; + } + + self.btnFile.style.visibility = 'hidden'; + + self.btnDraw.textContent = 'Edit'; + + for (el of self.inputNodes) { + el.disabled = true; + } + + document.forms.post.addEventListener('submit', self.onSubmit, false); + }, + + onCancel: function() { + var self = PainterCore; + + window.Keybinds && (Keybinds.enabled = true); + + self.data = null; + self.replayBlob = null; + self.time = 0; + + self.btnFile.disabled = false; + self.btnClear.disabled = true; + + self.btnFile.style.visibility = ''; + + self.btnDraw.textContent = 'Draw'; + + for (var el of self.inputNodes) { + el.disabled = false; + } + + document.forms.post.removeEventListener('submit', self.onSubmit, false); + }, + + onSubmit: function(e) { + var formdata, blob, xhr; + + e.preventDefault(); + + formdata = new FormData(this); + + blob = PainterCore.b64toBlob(PainterCore.data.slice(PainterCore.data.indexOf(',') + 1)); + + if (blob) { + formdata.append('upfile', blob, 'tegaki.png'); + + if (PainterCore.replayBlob) { + formdata.append('oe_replay', PainterCore.replayBlob, 'tegaki.tgkr'); + } + } + + formdata.append('oe_time', PainterCore.time); + + xhr = new XMLHttpRequest(); + xhr.open('POST', this.action, true); + xhr.withCredentials = true; + xhr.onerror = PainterCore.onSubmitError; + xhr.onload = PainterCore.onSubmitDone; + + xhr.send(formdata); + + PainterCore.btnSubmit.disabled = true; + }, + + onSubmitError: function() { + PainterCore.btnSubmit.disabled = false; + showPostFormError('Connection Error.'); + }, + + onSubmitDone: function() { + var resp, ids, tid, pid, board; + + PainterCore.btnSubmit.disabled = false; + + if (ids = this.responseText.match(//)) { + tid = +ids[1]; + pid = +ids[2]; + + if (!tid) { + tid = pid; + } + + board = location.pathname.split(/\//)[1]; + + window.location.href = '/' + board + '/thread/' + tid + '#p' + pid; + + PainterCore.onCancel(); + + if (tid != pid) { + PainterCore.btnClear.disabled = true; + window.location.reload(); + } + + return; + } + + if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) { + showPostFormError(resp[1]); + } + } +}; + +function oeReplay(id) { + PainterCore.replay(id); +} + +/*! https://github.com/Joe12387/detectIncognito */ +function checkIncognito() { + if (window.isIncognito !== undefined) { + return; + } + + if (!navigator.maxTouchPoints || navigator.vendor === undefined) { + window.isIncognito = false; + return; + } + + (new Promise(function(resolve, reject) { + let eh = eval.toString().length; + + if (navigator.vendor.indexOf('Apple') === 0 && eh === 37) { + if (navigator.maxTouchPoints === undefined) { + resolve(false); + } + + let db_name = Math.random().toString(); + + try { + let db = window.indexedDB.open(db_name, 1); + db.onupgradeneeded = function (e) { + let res = e.target.result; + try { + res.createObjectStore('test', { autoIncrement: true }).put(new Blob); + resolve(false); + } + catch(err) { + let msg; + if (err instanceof Error) { + msg = err.message; + } + if (typeof msg !== 'string') { + resolve(false); + } + resolve(/BlobURLs are not yet supported/.test(msg)); + } + finally { + res.close(); + window.indexedDB.deleteDatabase(db_name); + } + }; + } + catch(err) { + resolve(false); + } + } + else if (navigator.vendor.indexOf('Google') === 0 && eh === 33) { + let hsl; + + try { + hsl = performance.memory.jsHeapSizeLimit; + } + catch(err) { + hsl = 1073741824; + } + + navigator.webkitTemporaryStorage.queryUsageAndQuota(function (_, quota) { + let q = Math.round(quota / (1024 * 1024)); + let q_lim = Math.round(hsl / (1024 * 1024)) * 2; + resolve(q < q_lim); + }, function (err) { + resolve(false); + }); + } + else if (document.body.style.MozAppearance !== undefined && eh === 37) { + resolve(navigator.serviceWorker === undefined); + } + else { + resolve(false); + } + })).then((v) => window.isIncognito = v); +} + +function onPostFormSubmit(e) { + let el = $.id('postFile'); + if (el && el.value && window.isIncognito) { + e.stopPropagation() + e.preventDefault(); + el.value = ''; + showPostFormError('Uploading files in incognito mode is not allowed.' + + '
    The File field has been cleared.'); + return false; + } +} + +function contentLoaded() { + var i, el, el2, nodes, len, mobileSelect, params, board, val, fn; + + document.removeEventListener('DOMContentLoaded', contentLoaded, true); + + initAdsADT(); + + initAdsDanbo(); + + if (document.post) { + document.post.name.value = get_cookie("4chan_name"); + document.post.email.value = get_cookie("options"); + document.post.addEventListener('submit', onPostFormSubmit, false); + } + + cloneTopNav(); + + initAnalytics(); + + params = location.pathname.split(/\//); + + board = params[1]; + + if (window.passEnabled) { + setPassMsg(); + } + + if (window.Tegaki) { + PainterCore.init(); + } + + if (el = document.getElementById('bottomReportBtn')) { + el.addEventListener('click', onReportClick, false); + } + + if (el = document.getElementById('styleSelector')) { + el.addEventListener('change', onStyleSheetChange, false); + } + + // Post form toggle + if (el = document.getElementById('togglePostFormLink')) { + if (el = el.firstElementChild) { + el.addEventListener('click', showPostForm, false); + } + if (location.hash === '#reply') { + showPostForm(); + } + } + + // Selectable flags + if ((el = document.forms.post) && el.flag) { + el.flag.addEventListener('change', onBoardFlagChanged, false); + + if ((val = localStorage.getItem('4chan_flag_' + board)) && (el2 = el.querySelector('option[value="' + val + '"]'))) { + el2.setAttribute('selected', 'selected'); + } + } + + // Mobile nav menu + buildMobileNav(); + + // Mobile global message toggle + if (el = document.getElementById('globalToggle')) { + el.addEventListener('click', toggleGlobalMessage, false); + } + + if (localStorage.getItem('4chan_never_show_mobile') == 'true') { + if (el = document.getElementById('disable-mobile')) { + el.style.display = 'none'; + el = document.getElementById('enable-mobile'); + el.parentNode.style.cssText = 'display: inline !important;'; + } + } + + if (mobileSelect = document.getElementById('boardSelectMobile')) { + len = mobileSelect.options.length; + for ( i = 0; i < len; i++) { + if (mobileSelect.options[i].value == board) { + mobileSelect.selectedIndex = i; + continue; + } + } + + mobileSelect.addEventListener('change', onMobileSelectChange, false); + } + + if (document.forms.oeform && (el = document.forms.oeform.oe_src)) { + el.addEventListener('mouseover', oeCanvasPreview, false); + el.addEventListener('mouseout', oeClearPreview, false); + } + + if (params[2] != 'catalog') { + // Mobile post form toggle + nodes = document.getElementsByClassName('mobilePostFormToggle'); + + for (i = 0; el = nodes[i]; ++i) { + el.addEventListener('click', onMobileFormClick, false); + } + + if (el = document.getElementsByName('com')[0]) { + el.addEventListener('keydown', onComKeyDown, false); + el.addEventListener('paste', onComKeyDown, false); + el.addEventListener('cut', onComKeyDown, false); + } + + // Mobile refresh buttons + if (el = document.getElementById('refresh_top')) { + el.addEventListener('mouseup', onMobileRefreshClick, false); + } + + if (el = document.getElementById('refresh_bottom')) { + el.addEventListener('mouseup', onMobileRefreshClick, false); + } + + // Clickable flags + if (board == 'int' || board == 'sp' || board == 'pol') { + el = document.getElementById('delform'); + el.addEventListener('click', onCoreClick, false); + } + + // Page switcher + Search field + if (!params[3]) { + nodes = document.getElementsByClassName('pageSwitcherForm'); + + for (i = 0; el = nodes[i]; ++i) { + el.addEventListener('submit', onPageSwitch, false); + } + + if (el = document.getElementById('search-box')) { + el.addEventListener('keydown', onKeyDownSearch, false); + } + } + + if (window.clickable_ids) { + enableClickableIds(); + } + + Tip.init(); + } + + if (window.devicePixelRatio >= 2) { + setRetinaIcons(); + } + + initBlotter(); + + loadBannerImage(); + + if (window.css_event && activeStyleSheet === '_special') { + fn = window['fc_' + window.css_event + '_init']; + fn && fn(); + } +} + +function onBoardFlagChanged() { + var key = '4chan_flag_' + location.pathname.split(/\//)[1]; + + if (this.value === '0') { + localStorage.removeItem(key); + } + else { + localStorage.setItem(key, this.value); + } +} + +initPass(); + +window.onload = init; + +if (window.clickable_ids) { + document.addEventListener('4chanParsingDone', onParsingDone, false); +} + +document.addEventListener('4chanMainInit', loadExtraScripts, false); +document.addEventListener('DOMContentLoaded', contentLoaded, true); + +initStyleSheet(); diff --git a/js/core.min.js b/js/core.min.js new file mode 100644 index 0000000..6387682 --- /dev/null +++ b/js/core.min.js @@ -0,0 +1,2 @@ +function mShowFull(e){var t,n;return"name"===e.className?(t=e.parentNode.parentNode.parentNode.getElementsByClassName("name")[1])&&(n=t.innerHTML):"subject"===e.parentNode.className?(t=e.parentNode.parentNode.parentNode.parentNode.getElementsByClassName("subject")[1])&&(n=t.innerHTML):/fileThumb/.test(e.parentNode.className)&&(t=e.parentNode.parentNode.getElementsByClassName("fileText")[0])&&(n=(t=t.firstElementChild).getAttribute("title")||t.innerHTML),n}function loadBannerImage(){var e=document.getElementById("bannerCnt");!e||e.offsetWidth<=0||(e.innerHTML='4chan')}function onMobileSelectChange(){var e,t;t="f"!==(e=this.options[this.selectedIndex].value)&&/\/catalog$/.test(location.pathname)?"catalog":"",window.location="//boards."+$L.d(e)+"/"+e+"/"+t}function buildMobileNav(){var e,t,n,o,a,i;if(e=document.getElementById("boardSelectMobile")){for(a="",i=[],t=document.querySelectorAll("#boardNavDesktop .boardList a"),n=0;o=t[n];++n)i.push(o);for(i.sort(function(e,t){return e.textContentt.textContent?1:0}),n=0;o=i[n];++n)a+='";e.innerHTML=a}}function cloneTopNav(){var e,t,n,o;(e=document.getElementById("boardNavDesktop"))&&(n=document.getElementById("absbot"),(t=e.cloneNode(!0)).id=t.id+"Foot",(o=t.querySelector("#navtopright"))&&(o.id="navbotright"),(o=t.querySelector("#settingsWindowLink"))&&(o.id=o.id+"Bot"),document.body.insertBefore(t,n))}function initPass(){"1"==get_cookie("pass_enabled")||get_cookie("extra_path")?window.passEnabled=!0:window.passEnabled=!1}function initBlotter(){var e,t;(t=document.getElementById("toggleBlotter"))&&(t.addEventListener("click",toggleBlotter,!1),(e=localStorage.getItem("4chan-blotter"))&&+t.getAttribute("data-utc")<=+e&&toggleBlotter())}function toggleBlotter(e){var t,n;e&&e.preventDefault(),(t=document.getElementById("blotter-msgs"))&&(n=document.getElementById("toggleBlotter"),"none"==t.style.display?(t.style.display="",localStorage.removeItem("4chan-blotter"),n.textContent="Hide",(t=n.nextElementSibling).style.display&&(t.style.display="")):(t.style.display="none",localStorage.setItem("4chan-blotter",n.getAttribute("data-utc")),n.textContent="Show Blotter",n.nextElementSibling.style.display="none"))}function onRecaptchaLoaded(){"table"==document.getElementById("postForm").style.display&&initRecaptcha()}function initRecaptcha(){var e;(e=document.getElementById("g-recaptcha"))&&!e.firstElementChild&&!window.passEnabled&&window.grecaptcha&&grecaptcha.render(e,{sitekey:window.recaptchaKey,theme:"Tomorrow"===activeStyleSheet||window.dark_captcha?"dark":"light"})}function initTCaptcha(){let e=document.getElementById("t-root");if(e){let t,n=location.pathname.split(/\//)[1];t=document.forms.post&&document.forms.post.resto?+document.forms.post.resto.value:0,TCaptcha.init(e,n,t,5),TCaptcha.setErrorCb(window.showPostFormError)}}function initAnalytics(){var e,t,n,o,a,i,l,d=-1!==location.host.indexOf(".4channel.org")?"UA-166538-5":"UA-166538-1";e=window,t=document,n="script",o="//www.google-analytics.com/analytics.js",a="ga",e.GoogleAnalyticsObject=a,e[a]=e[a]||function(){(e[a].q=e[a].q||[]).push(arguments)},e[a].l=1*new Date,i=t.createElement(n),l=t.getElementsByTagName(n)[0],i.async=1,i.src=o,l.parentNode.insertBefore(i,l),ga("create",d,{sampleRate:1}),ga("set","anonymizeIp",!0),ga("send","pageview")}function initAdsPF(e,t){let n,o;if(1==t)n="657b2d8958f9186175770b1f",o="pf-6892-1";else if(2==t)n="657b2d9d58f9186175770b37",o="pf-6893-1";else if(3==t)n="657b2d56256794003cd16fe4",o="pf-6890-1";else{if(4!=t)return;n="657b2d74256794003cd17019",o="pf-6891-1"}e.innerHTML="";let a=document.createElement("div");a.id=o,e.appendChild(a),window.pubfuturetag=window.pubfuturetag||[],window.pubfuturetag.push({unit:n,id:o})}function initAdsADT(e){var t,n,o,a,i;for(a=window.matchMedia&&window.matchMedia("(max-width: 480px)").matches&&"true"!=localStorage.getItem("4chan_never_show_mobile")?"adg-m":"adg",n=(e||document).getElementsByClassName(a),o=0;t=n[o];++o)t.hasAttribute("data-abc")&&((i=document.createElement("iframe")).setAttribute("scrolling","no"),i.setAttribute("frameborder","0"),i.setAttribute("allowtransparency","true"),i.setAttribute("marginheight","0"),i.setAttribute("marginwidth","0"),"adg"===a?(i.setAttribute("width","728"),i.setAttribute("height","90")):(i.setAttribute("width","300"),i.setAttribute("height","250")),i.setAttribute("name","spot_id_"+t.getAttribute("data-abc")),i.src="https://a.adtng.com/get/"+t.getAttribute("data-abc")+"?time="+Date.now(),t.appendChild(i))}function danboAddSlot(e,t,n,o){let a=27,i=document.createElement("div");return i.className="danbo_dta",n?(o?o="3":(o="4",i.id="js-danbo-rld"),i.setAttribute("data-danbo",`${a}-${t}-${o}-300-250`),i.classList.add("danbo-m")):(o?o="1":(o="2",i.id="js-danbo-rld"),i.setAttribute("data-danbo",`${a}-${t}-${o}-728-90`),i.classList.add("danbo-d")),e.appendChild(i),i}function initAdsDanbo(){if(!window.Danbo)return;let e=location.pathname.split(/\//)[1]||"_",t=window.matchMedia&&window.matchMedia("(max-width: 480px)").matches,n=document.getElementsByClassName("danbo-slot");for(let o of n){danboAddSlot(o,e,t,"danbo-s-t"===o.id)}window.addEventListener("message",function(e){"https://hakurei.cdnbo.org"===e.origin&&e.data&&"danbo"===e.data.origin&&window.initAdsFallback(e.data.unit_id)}),window.Danbo.initialize()}function reloadAdsDanbo(){let e=document.getElementById("danbo-s-b");e&&(e.innerHTML="",danboAddSlot(e,"a",window.matchMedia&&window.matchMedia("(max-width: 480px)").matches,!1),window.Danbo.reload("js-danbo-rld"))}function initAdsFallback(e){let t,n=window.danbo_fb;t=1==e||3==e?"danbo-s-t":"danbo-s-b";let o=document.getElementById(t);if(!o)return;let a=document.body.classList.contains("ws"),i=a?.1:.01;if(Math.random()
    `,initAdsADT(o)):3==e&&n.t_abc_m?(o.innerHTML=`
    `,initAdsADT(o)):initAdsHome(o):initAdsHome(o):"danbo-s-b"===t?a?initAdsPF(o,e):n&&4==e&&n.b_abc_m?(o.innerHTML=`
    `,initAdsADT(o)):initAdsHome(o):console.log("Fallback",e)}function initAdsHome(e){let t,n,o=[["advertise","1.png","2.png","3.png"],["pass","4.png"]],a=[["advertise","1m.png"]];t=location.host.indexOf("4channel")?"4channel":"4chan";let i=(n=(n=window.matchMedia&&window.matchMedia("(max-width: 480px)").matches?a:o)[Math.floor(Math.random()*n.length)])[0],l=n[1+Math.floor(Math.random()*(n.length-1))],d=document.createElement("a");d.href=`https://www.${t}.org/${i}`,d.target="_blank";let r=document.createElement("img");r.src="//s.4cdn.org/image/banners/"+l,d.appendChild(r),e.children.length&&(e.innerHTML=""),e.appendChild(d)}function applySearch(e){var t;e&&e.preventDefault(),""!==(t=document.getElementById("search-box").value)&&(window.location.href="catalog#s="+t)}function onKeyDownSearch(e){13==e.keyCode&&applySearch()}function onReportClick(){var e,t,n,o;for(n=document.getElementsByTagName("input"),o=location.pathname.split(/\//)[1],e=0;t=n[e];++e)if("checkbox"==t.type&&t.checked&&"delete"==t.value)return reppop("https://sys."+$L.d(o)+"/"+o+"/imgboard.php?mode=report&no="+t.name.replace(/[a-z]+/,""))}function onStyleSheetChange(){setActiveStyleSheet(this.value)}function onPageSwitch(e){e.preventDefault(),window.location=this.action}function onMobileFormClick(e){var t=location.pathname.split(/\//).length<4;e.preventDefault(),window.QR&&Main.tid&&QR.enabled?QR.show(Main.tid):"mpostform"==this.parentNode.id?toggleMobilePostForm(t):toggleMobilePostForm(t,1)}function onMobileRefreshClick(){locationHashChanged(this)}function toggle(e){var t=document.getElementById(e);t.style.display="block"!=t.style.display?"block":"none"}function quote(e){if(document.selection)document.post.com.focus(),document.selection.createRange().text=">>"+e+"\n";else if(document.post.com.selectionStart||"0"==document.post.com.selectionStart){var t=document.post.com.selectionStart,n=document.post.com.selectionEnd;document.post.com.value=document.post.com.value.substring(0,t)+">>"+e+"\n"+document.post.com.value.substring(n,document.post.com.value.length)}else document.post.com.value+=">>"+e+"\n"}function repquote(e){""==document.post.com.value&"e(e)}function reppop(e){var t;return t=window.passEnabled||!window.grecaptcha?205:510,window.open(e,Date.now(),"toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height="+t),!1}function recaptcha_load(){document.getElementById("recaptcha_div")&&Recaptcha.create("6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc","recaptcha_div",{theme:"clean"})}function onParsingDone(e){var t,n,o,a,i,l,d;if(i=e.detail.threadId,l=e.detail.offset)for(n=document.getElementById("t"+i).getElementsByClassName("nameBlock"),d=e.detail.limit?2*e.detail.limit:n.length,t=2*l+1;t'),!0)}function toggleMobilePostForm(e,t){var n=document.getElementById("mpostform").firstElementChild,o=document.getElementById("postForm");n.className.match("hidden")?(n.className=n.className.replace("hidden","shown"),o.className=o.className.replace(" hideMobile",""),n.innerHTML="Close Post Form",initRecaptcha(),initTCaptcha(),checkIncognito()):(n.className=n.className.replace("shown","hidden"),o.className+=" hideMobile",n.innerHTML=e?"Start New Thread":"Post Reply"),t&&n.scrollIntoView()}function toggleGlobalMessage(e){var t,n;e&&e.preventDefault(),t=document.getElementById("globalToggle"),n=document.getElementById("globalMessage"),t.className.match("hidden")?(t.className=t.className.replace("hidden","shown"),n.className=n.className.replace(" hideMobile",""),t.innerHTML="Close Announcement"):(t.className=t.className.replace("shown","hidden"),n.className+=" hideMobile",t.innerHTML="View Announcement")}function checkRecaptcha(){"undefined"!=typeof RecaptchaState.timeout&&1800==RecaptchaState.timeout&&(RecaptchaState.timeout=570,Recaptcha._reset_timer(),clearInterval(captchainterval))}function setPassMsg(){var e,t;(e=document.getElementById("captchaFormPart"))&&(t='You are using a 4chan Pass. [Logout]',e.children[1].innerHTML='
    '+t+"
    ")}function confirmPassLogout(e){if(!confirm("Are you sure you want to logout?"))return e.preventDefault(),!1}function initStyleSheet(){var e,t,n,o;if(!window.FC){if(window.style_group){var a=readCookie(style_group);activeStyleSheet=a||getPreferredStyleSheet()}switch(window.css_event&&localStorage.getItem("4chan_stop_css_event")!==`${window.css_event}-${window.css_event_v}`&&(activeStyleSheet="_special"),activeStyleSheet){case"Yotsuba B":setActiveStyleSheet("Yotsuba B New",!0);break;case"Yotsuba":setActiveStyleSheet("Yotsuba New",!0);break;case"Burichan":setActiveStyleSheet("Burichan New",!0);break;case"Futaba":setActiveStyleSheet("Futaba New",!0);break;default:setActiveStyleSheet(activeStyleSheet,!0)}if("true"==localStorage.getItem("4chan_never_show_mobile"))for(o=(n=document.querySelectorAll("link")).length,e=0;e/.test(t.innerHTML))return!0;return!1}function cleanWbr(e){var t,n,o;for(t=(n=e.getElementsByTagName("wbr")).length-1;o=n[t];t--)o.parentNode.removeChild(o)}function parseMath(){var e,t,n;for(n=document.getElementsByClassName("postMessage"),e=0;t=n[e];++e)/\[(?:eqn|math)\]/.test(t.innerHTML)&&cleanWbr(t);MathJax.Hub.Queue(["Typeset",MathJax.Hub,n])}function loadMathJax(){var e,t;e=document.getElementsByTagName("head")[0],(t=document.createElement("script")).type="text/x-mathjax-config",t.text="MathJax.Hub.Config({extensions: ['Safe.js'],tex2jax: { processRefs: false, processEnvironments: false, preview: 'none', inlineMath: [['[math]','[/math]']], displayMath: [['[eqn]','[/eqn]']] },Safe: { allow: { URLs: 'none', classes: 'none', cssIDs: 'none', styles: 'none', fontsize: 'none', require: 'none' } },displayAlign: 'left', messageStyle: 'none', skipStartupTypeset: true,'CHTML-preview': { disabled: true }, MathMenu: { showRenderer: false, showLocale: false },TeX: { Macros: { color: '{}', newcommand: '{}', renewcommand: '{}', newenvironment: '{}', renewenvironment: '{}', def: '{}', let: '{}'}}});",e.appendChild(t),(t=document.createElement("script")).src="//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-AMS_HTML-full",t.onload=parseMath,e.appendChild(t)}function init(){var e,t,n="undefined"!=typeof is_error,o=location.href.match(/(?:4chan|4channel)\.org\/(\w+)/)[1],a=location.href.split(/#/);if(a[1]&&a[1].match(/q[0-9]+$/)&&repquote(a[1].match(/q([0-9]+)$/)[1]),window.math_tags&&pageHasMath()&&loadMathJax(),navigator.userAgent&&navigator.userAgent.match(/iP(hone|ad|od)/i))for(links=document.querySelectorAll("s"),len=links.length,t=0;tcomlen?((n=document.getElementById("comlenError"))||((n=document.createElement("div")).id="comlenError",n.style.cssText="font-weight:bold;padding:5px;color:red;",t.parentNode.appendChild(n)),n.textContent="Error: Comment too long ("+e+"/"+comlen+")."):(n=document.getElementById("comlenError"))&&n.parentNode.removeChild(n))}function disableMobile(){localStorage.setItem("4chan_never_show_mobile","true"),location.reload(!0)}function enableMobile(){localStorage.removeItem("4chan_never_show_mobile"),location.reload(!0)}function enableClickableIds(){var e=0,t=0,n=document.getElementsByClassName("posteruid"),o=document.getElementsByClassName("capcode");if(null!=o)for(e=0,t=o.length;et?showPostFormError("Error: Maximum file size allowed is "+Math.floor(t/1048576)+" MB"):showPostFormError())}function locationHashChanged(e){var t=document.getElementById("id_css");switch(e.id){case"refresh_top":url=window.location.href.replace(/#.+/,"#top"),/top$/.test(url)||(url+="#top"),t.innerHTML='',document.location.reload(!0);break;case"refresh_bottom":url=window.location.href.replace(/#.+/,"#bottom"),/bottom$/.test(url)||(url+="#bottom"),t.innerHTML='',document.location.reload(!0)}return!0}function setActiveStyleSheet(e,t){var n,o,a,i,l,d;if(1!=document.querySelectorAll("link[title]").length){for(a="",l=document.getElementsByTagName("link"),i=0;n=l[i];i++)"switch"==n.getAttribute("title")&&(o=n),-1!=n.getAttribute("rel").indexOf("style")&&n.getAttribute("title")&&n.getAttribute("title")==e&&(a=n.href);o&&o.setAttribute("href",a),t||("_special"!==e?(createCookie(style_group,e,365,-1===location.host.indexOf("4channel.org")?"4chan.org":"4channel.org"),window.css_event&&(d=window["fc_"+window.css_event+"_cleanup"],localStorage.setItem("4chan_stop_css_event",`${window.css_event}-${window.css_event_v}`))):window.css_event&&(d=window["fc_"+window.css_event+"_init"],localStorage.removeItem("4chan_stop_css_event")),activeStyleSheet=e,d&&d())}}function getActiveStyleSheet(){var e,t,n;if(1==document.querySelectorAll("link[title]").length)return"Yotsuba P";for(e=0;t=document.getElementsByTagName("link")[e];e++)if("switch"==t.getAttribute("title"))n=t;else if(-1!=t.getAttribute("rel").indexOf("style")&&t.getAttribute("title")&&t.href==n.href)return t.getAttribute("title");return null}function getPreferredStyleSheet(){return"ws_style"==style_group?"Yotsuba B New":"Yotsuba New"}function createCookie(e,t,n,o){let a;if(n){var i=new Date;i.setTime(i.getTime()+24*n*60*60*1e3),a="; expires="+i.toGMTString()}else a="";o=o?"; domain="+o:"",document.cookie=e+"="+t+a+"; path=/"+o}function readCookie(e){for(var t=e+"=",n=document.cookie.split(";"),o=0;owindow.isIncognito=e):window.isIncognito=!1)}function onPostFormSubmit(e){let t=$.id("postFile");if(t&&t.value&&window.isIncognito)return e.stopPropagation(),e.preventDefault(),t.value="",showPostFormError("Uploading files in incognito mode is not allowed.
    The File field has been cleared."),!1}function contentLoaded(){var e,t,n,o,a,i,l,d,r,s;if(document.removeEventListener("DOMContentLoaded",contentLoaded,!0),initAdsADT(),initAdsDanbo(),document.post&&(document.post.name.value=get_cookie("4chan_name"),document.post.email.value=get_cookie("options"),document.post.addEventListener("submit",onPostFormSubmit,!1)),cloneTopNav(),initAnalytics(),d=(l=location.pathname.split(/\//))[1],window.passEnabled&&setPassMsg(),window.Tegaki&&PainterCore.init(),(t=document.getElementById("bottomReportBtn"))&&t.addEventListener("click",onReportClick,!1),(t=document.getElementById("styleSelector"))&&t.addEventListener("change",onStyleSheetChange,!1),(t=document.getElementById("togglePostFormLink"))&&((t=t.firstElementChild)&&t.addEventListener("click",showPostForm,!1),"#reply"===location.hash&&showPostForm()),(t=document.forms.post)&&t.flag&&(t.flag.addEventListener("change",onBoardFlagChanged,!1),(r=localStorage.getItem("4chan_flag_"+d))&&(n=t.querySelector('option[value="'+r+'"]'))&&n.setAttribute("selected","selected")),buildMobileNav(),(t=document.getElementById("globalToggle"))&&t.addEventListener("click",toggleGlobalMessage,!1),"true"==localStorage.getItem("4chan_never_show_mobile")&&(t=document.getElementById("disable-mobile"))&&(t.style.display="none",(t=document.getElementById("enable-mobile")).parentNode.style.cssText="display: inline !important;"),i=document.getElementById("boardSelectMobile")){for(a=i.options.length,e=0;e=2&&setRetinaIcons(),initBlotter(),loadBannerImage(),window.css_event&&"_special"===activeStyleSheet&&(s=window["fc_"+window.css_event+"_init"])&&s()}function onBoardFlagChanged(){var e="4chan_flag_"+location.pathname.split(/\//)[1];"0"===this.value?localStorage.removeItem(e):localStorage.setItem(e,this.value)}var activeStyleSheet,$L={nws:{aco:1,b:1,bant:1,d:1,e:1,f:1,gif:1,h:1,hc:1,hm:1,hr:1,i:1,ic:1,pol:1,r:1,r9k:1,s:1,s4s:1,soc:1,t:1,trash:1,u:1,wg:1,y:1},blue:"4chan.org",red:"4chan.org",d:function(e){return $L.nws[e]?$L.red:$L.blue}},TCaptcha={node:null,frameNode:null,imgCntNode:null,bgNode:null,fgNode:null,msgNode:null,sliderNode:null,respNode:null,reloadNode:null,helpNode:null,challengeNode:null,ticketCaptchaNode:null,challenge:null,reloadTs:null,reloadTimeout:null,expireTimeout:null,frameTimeout:null,pcdBypassable:!1,errorCb:null,path:"/captcha",ticketKey:"4chan-tc-ticket",domain:"4chan.org",failCd:60,tabindex:null,hCaptchaSiteKey:"49d294fa-f15c-41fc-80ba-c2544c52ec2a",init:function(e,t,n,o){this.node&&this.destroy(),o&&(this.tabindex=o),this.node=e,e.style.position="relative",e.style.width="300px",this.frameNode=null,this.imgCntNode=this.buildImgCntNode(),this.bgNode=this.buildImgNode("bg"),this.fgNode=this.buildImgNode("fg"),this.sliderNode=this.buildSliderNode(),this.respNode=this.buildRespField(),this.reloadNode=this.buildReloadNode(t,n),this.helpNode=this.buildHelpNode(),this.msgNode=this.buildMsgNode(),this.challengeNode=this.buildChallengeNode(),e.appendChild(this.reloadNode),e.appendChild(this.respNode),e.appendChild(this.helpNode),this.imgCntNode.appendChild(this.bgNode),this.imgCntNode.appendChild(this.fgNode),e.appendChild(this.imgCntNode),e.appendChild(this.sliderNode),e.appendChild(this.msgNode),e.appendChild(this.challengeNode),window.addEventListener("message",this.onFrameMessage)},destroy:function(){let e=TCaptcha;e.node&&(window.removeEventListener("message",e.onFrameMessage),clearTimeout(e.frameTimeout),clearTimeout(e.reloadTimeout),clearTimeout(e.expireTimeout),e.node.textContent="",e.node=null,e.frameNode=null,e.imgCntNode=null,e.bgNode=null,e.fgNode=null,e.msgNode=null,e.sliderNode=null,e.respNode=null,e.reloadNode=null,e.helpNode=null,e.challengeNode=null,e.ticketCaptchaNode=null,e.challenge=null,e.pcdBypassable=!1,e.errorCb=null,e.reloadTs=null,e.onReloadCdDone=null)},setErrorCb:function(e){TCaptcha.errorCb=e},toggleImgCntNode:function(e){TCaptcha.imgCntNode.style.display=e?"block":"none"},getTicket:function(){return localStorage.getItem(TCaptcha.ticketKey)},setTicket:function(e){e?localStorage.setItem(TCaptcha.ticketKey,e):!1===e&&localStorage.removeItem(TCaptcha.ticketKey)},buildFrameNode:function(){let e=document.createElement("iframe");return e.id="t-frame",e.style.border="0",e.style.width="100%",e.style.height="80px",e.style.marginTop="2px",e.style.position="relative",e.style.display="block",e.onerror=TCaptcha.onFrameError,e},buildImgCntNode:function(){let e=document.createElement("div");return e.id="t-cnt",e.style.height="80px",e.style.marginTop="2px",e.style.position="relative",e},buildImgNode:function(e){let t=document.createElement("div");return t.id="t-"+e,t.style.width="100%",t.style.height="100%",t.style.position="absolute",t.style.backgroundRepeat="no-repeat",t.style.backgroundPosition="top left",t.style.pointerEvents="none",t},buildMsgNode:function(){let e=document.createElement("div");return e.id="t-msg",e.style.width="100%",e.style.height="calc(100% - 20px)",e.style.position="absolute",e.style.top="20px",e.style.textAlign="center",e.style.fontSize="12px",e.style.filter="inherit",e.style.display="none",e.style.alignContent="center",e},buildRespField:function(){let e=document.createElement("input");return e.id="t-resp",e.name="t-response",e.placeholder="Type the CAPTCHA here",e.setAttribute("autocomplete","off"),e.type="text",e.style.width="160px",e.style.boxSizing="border-box",e.style.textTransform="uppercase",e.style.fontSize="11px",e.style.height="18px",e.style.margin="0",e.style.padding="0 2px",e.style.fontFamily="monospace",e.style.verticalAlign="middle",this.tabindex&&e.setAttribute("tabindex",this.tabindex+2),e},buildSliderNode:function(){let e=document.createElement("input");return e.id="t-slider",e.setAttribute("autocomplete","off"),e.type="range",e.style.width="100%",e.style.boxSizing="border-box",e.style.visibility="hidden",e.style.margin="0",e.style.transition="box-shadow 15s linear",e.style.boxShadow="0 0 6px 4px #1d8dc4",e.style.position="relative",e.value=0,e.min=0,e.max=100,e.addEventListener("input",this.onSliderInput,!1),this.tabindex&&e.setAttribute("tabindex",this.tabindex+1),e},buildChallengeNode:function(){let e=document.createElement("input");return e.name="t-challenge",e.type="hidden",e},buildReloadNode:function(e,t){let n=document.createElement("button");return n.id="t-load",n.type="button",n.style.fontSize="11px",n.style.padding="0",n.style.width="90px",n.style.boxSizing="border-box",n.style.margin="0 6px 0 0",n.style.verticalAlign="middle",n.style.height="18px",n.textContent="Get Captcha",n.setAttribute("data-board",e),n.setAttribute("data-tid",t),n.addEventListener("click",this.onReloadClick,!1),this.tabindex&&n.setAttribute("tabindex",this.tabindex),n},buildHelpNode:function(){let e=document.createElement("button");return e.id="t-help",e.type="button",e.style.fontSize="11px",e.style.padding="0",e.style.width="20px",e.style.boxSizing="border-box",e.style.margin="0 0 0 6px",e.style.verticalAlign="middle",e.style.height="18px",e.textContent="?",e.setAttribute("data-tip","Help"),e.tabIndex=-1,e.addEventListener("click",this.onHelpClick,!1),e},onHelpClick:function(){alert("- Only type letters and numbers displayed in the image.\n- If needed, use the slider to align the image to make it readable.\n- Make sure to not block any cookies set by 4chan.")},onTicketCaptchaError:function(){TCaptcha.toggleMsgOverlay(!0,"Couldn't load the captcha.

    Please check your browser's content blocker.")},onTicketCaptchaDone:function(e){TCaptcha.reloadNode.setAttribute("data-ticket-resp",e),TCaptcha.destroyTicketCaptcha(),TCaptcha.onReloadClick()},loadTicketCaptcha:function(){window.pcd_c_loaded=TCaptcha.buildTicketCaptcha,window.pcd_c_done=TCaptcha.onTicketCaptchaDone,TCaptcha.toggleMsgOverlay(!0,"Loading\u2026");let e=document.createElement("script");e.src="https://js.hcaptcha.com/1/api.js?onload=pcd_c_loaded&render=explicit&recaptchacompat=off",e.onerror=TCaptcha.onTicketCaptchaError,document.head.appendChild(e)},buildTicketCaptcha:function(){let e=TCaptcha;if(e.toggleMsgOverlay(!1),!window.hcaptcha)return void e.loadTicketCaptcha();let t=document.createElement("div");t.id="t-tc-cnt",e.imgCntNode.appendChild(t);let n=window.hcaptcha.render("t-tc-cnt",{sitekey:e.hCaptchaSiteKey,callback:"pcd_c_done"});t.setAttribute("data-wid",n),e.ticketCaptchaNode=t},destroyTicketCaptcha:function(){let e=TCaptcha;if(!window.hcaptcha||!e.ticketCaptchaNode)return;let t=e.ticketCaptchaNode.getAttribute("data-wid");window.hcaptcha.reset(t),e.imgCntNode.removeChild(e.ticketCaptchaNode),e.ticketCaptchaNode=null},onReloadClick:function(){let e=TCaptcha.reloadNode,t=e.getAttribute("data-board"),n=e.getAttribute("data-tid"),o=e.getAttribute("data-ticket-resp");e.removeAttribute("data-ticket-resp"),TCaptcha.toggleReloadBtn(!1,"Loading"),TCaptcha.load(t,n,o)},onFrameMessage:function(e){e.origin===`https://sys.${TCaptcha.domain}`&&e.data&&e.data.twister&&(TCaptcha.destroyFrame(),TCaptcha.buildFromJson(e.data.twister))},onFrameError:function(e){TCaptcha.unlockReloadBtn(),console.log(e),TCaptcha.errorCb&&TCaptcha.errorCb.call(null,"Couldn't load the captcha frame. Check your content blocker settings.")},load:function(e,t,n){let o=TCaptcha;clearTimeout(o.frameTimeout),clearTimeout(o.reloadTimeout),clearTimeout(o.expireTimeout);let a=["framed=1"];e&&a.push("board="+e),t>0&&a.push("thread_id="+t),n&&a.push("ticket_resp="+encodeURIComponent(n));let i=o.getTicket();i&&a.push("ticket="+i),a.length>0&&(a="?"+a.join("&"));let l="https://sys."+o.domain+o.path+a;o.frameNode=o.buildFrameNode(),o.toggleImgCntNode(!1),o.node.insertBefore(o.frameNode,o.imgCntNode),o.frameTimeout=setTimeout(o.onFrameTimeout,6e4,l),o.frameNode.src=l},onFrameTimeout:function(e){let t=TCaptcha;t.destroyFrame(),console.log("Captcha frame timeout"),t.errorCb&&t.errorCb.call(null,`Couldn't get the captcha.\nMake sure your browser doesn't block content on 4chan then click\nhere.`)},destroyFrame:function(){let e=TCaptcha;clearTimeout(e.frameTimeout),e.frameTimeout=null,e.frameNode&&(e.frameNode.remove(),e.frameNode=null),e.toggleImgCntNode(!0),e.unlockReloadBtn()},unlockReloadBtn:function(){TCaptcha.reloadTs=null,TCaptcha.toggleReloadBtn(!0,"Get Captcha")},toggleReloadBtn:function(e,t){let n=TCaptcha;n.reloadNode&&(n.reloadNode.disabled=!e,t!==undefined&&(n.reloadNode.textContent=t))},onCaptchaFailed:function(){let e=TCaptcha,t=1e3*e.failCd;e.reloadTs&&e.reloadTs0?(e.reloadNode.textContent=Math.ceil(t/1e3),e.reloadTimeout=setTimeout(e.onReloadCdTick,Math.min(t,1e3))):e.stopReloadCd()},clearChallenge:function(){let e=TCaptcha;e.node&&(e.challengeNode.value="",e.respNode.value="",e.fgNode.style.backgroundImage="",e.bgNode.style.backgroundImage="",e.toggleSlider(!1),e.toggleMsgOverlay(!1))},toggleSlider:function(e){TCaptcha.sliderNode.style.visibility=e?"":"hidden",TCaptcha.sliderNode.style.boxShadow=e?"":"0 0 4px 2px #1d8dc4"},toggleMsgOverlay:function(e,t){t!==undefined&&(TCaptcha.msgNode.innerHTML=`
    ${t}
    `),TCaptcha.msgNode.style.display=e?"grid":"none"},onSliderInput:function(){var e=-Math.floor(+this.value/100*this.twisterDelta);TCaptcha.bgNode.style.backgroundPositionX=e+"px"},onTicketPcdTick:function(){let e=TCaptcha,t=document.getElementById("t-pcd");if(!t)return;let n=+t.getAttribute("data-pcd");(n-=0|Date.now()/1e3)<=0?e.onTicketPcdEnd():(t.textContent=n,setTimeout(e.updateTicketPcd,1e3))},clearTicketOverlay:function(){TCaptcha.toggleMsgOverlay(!1)},buildFromJson:function(e){let t=TCaptcha;if(t.node){if(t.unlockReloadBtn(),t.toggleSlider(!1),t.toggleMsgOverlay(!1),t.setTicket(e.ticket),TCaptcha.errorCb&&TCaptcha.errorCb.call(null,""),e.cd&&t.setReloadCd(1e3*e.cd,!e.challenge),e.mpcd)return t.clearChallenge(),t.destroyTicketCaptcha(),void t.buildTicketCaptcha();if(e.pcd)t.buildTicket(e);else{if(e.error)return console.log(e.error),void(TCaptcha.errorCb&&TCaptcha.errorCb.call(null,e.error));t.imgCntNode.style.width=e.img_width+"px",t.imgCntNode.style.height=e.img_height+"px",t.challengeNode.value=e.challenge,t.expireTimeout=setTimeout(t.clearChallenge,1e3*e.ttl-3e3),e.bg_width?t.buildTwister(e):e.img?t.buildStatic(e):t.buildNoop(e)}}},buildTwister:function(e){let t=TCaptcha;t.fgNode.style.backgroundImage="url(data:image/png;base64,"+e.img+")",t.bgNode.style.backgroundImage="url(data:image/png;base64,"+e.bg+")",t.bgNode.style.backgroundPositionX="0px",t.toggleSlider(!0),t.sliderNode.value=0,t.sliderNode.twisterDelta=e.bg_width-e.img_width,t.sliderNode.focus()},buildStatic:function(e){let t=TCaptcha;t.fgNode.style.backgroundImage="url(data:image/png;base64,"+e.img+")",t.bgNode.style.backgroundImage=""},buildTicket:function(e){let t=TCaptcha;t.toggleMsgOverlay(!0,e.pcd_msg||"Please wait a while."),t.fgNode.style.backgroundImage="",t.bgNode.style.backgroundImage="",t.setReloadCd(1e3*e.pcd,!e.bpcd||-1,t.clearTicketOverlay)},buildNoop:function(){let e=TCaptcha;e.toggleMsgOverlay(!0,"Verification not required."),e.fgNode.style.backgroundImage="",e.bgNode.style.backgroundImage=""}},Tip={node:null,timeout:null,delay:300,init:function(){document.addEventListener("mouseover",this.onMouseOver,!1),document.addEventListener("mouseout",this.onMouseOut,!1)},onMouseOver:function(e){var t,n,o;o=e.target,Tip.timeout&&(clearTimeout(Tip.timeout),Tip.timeout=null),o.hasAttribute("data-tip")&&(n=null,o.hasAttribute("data-tip-cb")&&(t=o.getAttribute("data-tip-cb"),window[t]&&(n=window[t](o))),Tip.timeout=setTimeout(Tip.show,Tip.delay,e.target,n))},onMouseOut:function(){Tip.timeout&&(clearTimeout(Tip.timeout),Tip.timeout=null),Tip.hide()},show:function(e,t,n){var o,a,i,l,d;a=e.getBoundingClientRect(),(o=document.createElement("div")).id="tooltip",t?o.innerHTML=t:o.textContent=e.getAttribute("data-tip"),n||(n="top"),o.className="tip-"+n,document.body.appendChild(o),(l=a.left-(o.offsetWidth-e.offsetWidth)/2)<0?(l=a.left+2,o.className+="-right"):l+o.offsetWidth>document.documentElement.clientWidth&&(l=a.left-o.offsetWidth+e.offsetWidth+2,o.className+="-left"),d=a.top-o.offsetHeight-5,(i=o.style).top=d+window.pageYOffset+"px",i.left=l+window.pageXOffset+"px",Tip.node=o},hide:function(){Tip.node&&(document.body.removeChild(Tip.node),Tip.node=null)}};captchainterval=null;var coreLenCheckTimeout=null,currentHighlighted=null,get_cookie=readCookie,PainterCore={init:function(){var e,t;document.forms.post&&(e=document.forms.post.getElementsByClassName("painter-ctrl")[0])&&(t=e.getElementsByTagName("button"))[1]&&(this.data=null,this.replayBlob=null,this.time=0,this.btnDraw=t[0],this.btnClear=t[1],this.btnFile=document.getElementById("postFile"),this.btnSubmit=document.forms.post.querySelector('input[type="submit"]'),this.inputNodes=e.getElementsByTagName("input"),this.replayCb=e.getElementsByClassName("oe-r-cb")[0],t[0].addEventListener("click",this.onDrawClick,!1),t[1].addEventListener("click",this.onCancel,!1))},onDrawClick:function(){var e,t,n=this.parentNode.getElementsByTagName("input");e=+n[0].value,t=+n[1].value,e<1||t<1||(window.Keybinds&&(Keybinds.enabled=!1),Tegaki.open({onDone:PainterCore.onDone,onCancel:PainterCore.onCancel,saveReplay:PainterCore.replayCb&&PainterCore.replayCb.checked,width:e,height:t}))},replay:function(e){e=+e,Tegaki.open({replayMode:!0,replayURL:"//i.4cdn.org/"+location.pathname.split(/\//)[1]+"/"+e+".tgkr"})},b64toBlob:function(e){var t,n,o,a,i;for(i=(n=atob(e)).length,o=new Array(i),t=0;t/))return n=+t[1],o=+t[2],n||(n=o),a=location.pathname.split(/\//)[1],window.location.href="/"+a+"/thread/"+n+"#p"+o,PainterCore.onCancel(),void(n!=o&&(PainterCore.btnClear.disabled=!0,window.location.reload()));(e=this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/))&&showPostFormError(e[1])}};initPass(),window.onload=init,window.clickable_ids&&document.addEventListener("4chanParsingDone",onParsingDone,!1),document.addEventListener("4chanMainInit",loadExtraScripts,!1),document.addEventListener("DOMContentLoaded",contentLoaded,!0),initStyleSheet(); \ No newline at end of file diff --git a/js/core.min.map b/js/core.min.map new file mode 100644 index 0000000..744783f --- /dev/null +++ b/js/core.min.map @@ -0,0 +1 @@ +{"version":3,"file":"core.min.1012.js","sources":["core.1012.js"],"names":["mShowFull","t","el","data","className","parentNode","getElementsByClassName","innerHTML","test","firstElementChild","getAttribute","loadBannerImage","cnt","document","getElementById","offsetWidth","onMobileSelectChange","board","page","this","options","selectedIndex","value","location","pathname","window","buildMobileNav","boards","i","b","html","order","querySelectorAll","push","sort","a","textContent","title","cloneTopNav","navT","navB","ref","cloneNode","id","querySelector","body","insertBefore","initPass","passEnabled","get_cookie","initBlotter","mTime","seenTime","addEventListener","toggleBlotter","localStorage","getItem","e","btn","preventDefault","style","display","removeItem","nextElementSibling","setItem","onRecaptchaLoaded","initRecaptcha","grecaptcha","render","sitekey","recaptchaKey","theme","activeStyleSheet","initAnalytics","s","o","g","r","m","q","arguments","l","Date","createElement","getElementsByTagName","async","src","ga","sampleRate","initAds","category","p","d","protocol","z","type","onload","ados","run","matchMedia","matches","_top_ad","ados_add_placement","setZone","ados_setPassbackTimeout","ados_setDomain","ados_setKeywords","thread_archived","ados_setNoTrack","ados_load","initAdsAG","nodes","cls","appendChild","replace","applySearch","str","href","onKeyDownSearch","keyCode","onReportClick","input","split","checked","reppop","name","onStyleSheetChange","setActiveStyleSheet","onPageSwitch","action","onMobileFormClick","index","length","toggleMobilePostForm","onMobileRefreshClick","locationHashChanged","get_pass","pass","chars","len","rnd","Math","floor","random","substring","toggle","quote","text","selection","post","com","focus","sel","createRange","selectionStart","startPos","endPos","selectionEnd","repquote","rep","url","height","open","now","recaptcha_load","Recaptcha","create","onParsingDone","n","tid","offset","limit","detail","threadId","children","currentHighlighted","indexOf","idClick","loadExtraScripts","path","readCookie","FC","jsVersion","head","write","scrolltotop","elem","postForm","match","scrollIntoView","toggleGlobalMessage","checkRecaptcha","RecaptchaState","timeout","_reset_timer","clearInterval","captchainterval","setPassMsg","msg","confirmPassLogout","event","conf","confirm","initStyleSheet","rem","link","style_group","cookie","getPreferredStyleSheet","removeChild","pageHasMath","cleanWbr","parseMath","MathJax","Hub","Queue","loadMathJax","script","init","error","is_error","arr","math_tags","navigator","userAgent","links","onclick","hasAttribute","removeAttribute","setAttribute","styleSelect","forms","File","FileReader","FileList","Blob","handleFileSelect","extra","check_for_block","checkForBlock","onComKeyDown","clearTimeout","coreLenCheckTimeout","setTimeout","coreCheckComLength","byteLength","comField","comlen","getElementsByName","encodeURIComponent","cssText","disableMobile","reload","enableMobile","plea","offsetHeight","blockPlea","width","border","enableClickableIds","elems","capcode","evt","node","uid","target","hl","cn","toString","nhl","showPostFormError","fsize","maxFilesize","files","size","maxWebmFilesize","css","createCookie","getActiveStyleSheet","days","domain","date","setTime","getTime","expires","toGMTString","nameEQ","ca","c","charAt","decodeURIComponent","setRetinaIcons","j","onCoreClick","which","showPostForm","$","oeCanvasPreview","nodeName","nextSibling","oeClearPreview","contentLoaded","el2","mobileSelect","params","val","removeEventListener","_adg","email","pwd","Tegaki","PainterCore","hash","flag","oeform","oe_src","clickable_ids","Tip","devicePixelRatio","delay","onMouseOver","onMouseOut","cb","show","hide","pos","rect","left","top","getBoundingClientRect","documentElement","clientWidth","pageYOffset","pageXOffset","btns","btnDraw","btnClear","btnFile","btnSubmit","onDrawClick","onCancel","w","h","dims","onDone","b64toBlob","bytes","ary","bary","atob","Array","charCodeAt","Uint8Array","disabled","flatten","toDataURL","onSubmit","formdata","blob","xhr","FormData","slice","append","XMLHttpRequest","withCredentials","onerror","onSubmitError","onSubmitDone","send","resp","ids","pid","responseText"],"mappings":"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"} \ No newline at end of file diff --git a/js/extension-test-sync2.js b/js/extension-test-sync2.js new file mode 100644 index 0000000..1f80b42 --- /dev/null +++ b/js/extension-test-sync2.js @@ -0,0 +1,8967 @@ +/******************************** + * * + * 4chan Extension * + * * + ********************************/ + +/** + * Helpers + */ +$ = {}; + +$.id = function(id) { + return document.getElementById(id); +}; + +$.cls = function(klass, root) { + return (root || document).getElementsByClassName(klass); +}; + +$.byName = function(name) { + return document.getElementsByName(name); +}; + +$.tag = function(tag, root) { + return (root || document).getElementsByTagName(tag); +}; + +$.qs = function(sel, root) { + return (root || document).querySelector(sel); +}; + +$.extend = function(destination, source) { + for (var key in source) { + destination[key] = source[key]; + } +}; + +if (!document.documentElement.classList) { + $.hasClass = function(el, klass) { + return (' ' + el.className + ' ').indexOf(' ' + klass + ' ') != -1; + }; + + $.addClass = function(el, klass) { + el.className = (el.className == '') ? klass : el.className + ' ' + klass; + }; + + $.removeClass = function(el, klass) { + el.className = (' ' + el.className + ' ').replace(' ' + klass + ' ', ''); + }; +} +else { + $.hasClass = function(el, klass) { + return el.classList.contains(klass); + }; + + $.addClass = function(el, klass) { + el.classList.add(klass); + }; + + $.removeClass = function(el, klass) { + el.classList.remove(klass); + }; +} + +$.get = function(url, callbacks, headers) { + var key, xhr; + + xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + if (callbacks) { + for (key in callbacks) { + xhr[key] = callbacks[key]; + } + } + if (headers) { + for (key in headers) { + xhr.setRequestHeader(key, headers[key]); + } + } + xhr.send(null); + return xhr; +}; + +$.hash = function(str) { + var i, j, msg = 0; + for (i = 0, j = str.length; i < j; ++i) { + msg = ((msg << 5) - msg) + str.charCodeAt(i); + } + return msg; +}; + +$.prettySeconds = function(fs) { + var m, s; + + m = Math.floor(fs / 60); + s = Math.round(fs - m * 60); + + return [ m, s ]; +}; + +$.docEl = document.documentElement; + +$.cache = {}; + +/** + * Parser + */ +var Parser = {}; + +Parser.init = function() { + var o, a, h, m, tail, staticPath, tracked, el; + + if (Config.filter || Config.embedSoundCloud || Config.embedYouTube || Config.embedVocaroo) { + this.needMsg = true; + } + + staticPath = '//s.4cdn.org/image/'; + + tail = window.devicePixelRatio >= 2 ? '@2x.gif' : '.gif'; + + this.icons = { + admin: staticPath + 'adminicon' + tail, + mod: staticPath + 'modicon' + tail, + dev: staticPath + 'developericon' + tail, + manager: staticPath + 'managericon' + tail, + del: staticPath + 'filedeleted-res' + tail + }; + + this.prettify = typeof prettyPrint == 'function'; + + this.customSpoiler = {}; + + if (Config.localTime) { + if (o = (new Date).getTimezoneOffset()) { + a = Math.abs(o); + h = (0 | (a / 60)); + + this.utcOffset = 'Timezone: UTC' + (o < 0 ? '+' : '-') + + h + ((m = a - h * 60) ? (':' + m) : ''); + } + else { + this.utcOffset = 'Timezone: UTC'; + } + + this.weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + } + + if (Main.tid) { + this.trackedReplies = this.getTrackedReplies(Main.tid) || {}; + } +}; + +Parser.getTrackedReplies = function(tid) { + var tracked = null; + + if (tracked = sessionStorage.getItem('4chan-track-' + Main.board + '-' + tid)) { + tracked = JSON.parse(tracked); + } + + return tracked; +}; + +Parser.saveTrackedReplies = function(tid, replies) { + sessionStorage.setItem( + '4chan-track-' + Main.board + '-' + tid, + JSON.stringify(replies) + ); +}; + +Parser.parseThreadJSON = function(data) { + var thread; + + try { + thread = JSON.parse(data).posts; + } + catch (e) { + console.log(e); + thread = []; + } + + return thread; +}; + +Parser.parseCatalogJSON = function(data) { + var catalog; + + try { + catalog = JSON.parse(data); + } + catch (e) { + console.log(e); + catalog = []; + } + + return catalog; +}; + +Parser.setCustomSpoiler = function(board, val) { + var s; + if (!this.customSpoiler[board] && (val = parseInt(val))) { + if (board == Main.board && (s = $.cls('imgspoiler')[0])) { + this.customSpoiler[board] = + s.firstChild.src.match(/spoiler(-[a-z0-9]+)\.png$/)[1]; + } + else { + this.customSpoiler[board] = '-' + board + + (Math.floor(Math.random() * val) + 1); + } + } +}; + +Parser.buildPost = function(thread, board, pid) { + var i, j, el = null; + + for (i = 0; j = thread[i]; ++i) { + if (j.no != pid) { + continue; + } + + if (!Config.revealSpoilers && thread[0].custom_spoiler) { + Parser.setCustomSpoiler(board, thread[0].custom_spoiler); + } + + el = Parser.buildHTMLFromJSON(j, board, false, true).lastElementChild; + + if (Config.IDColor && (uid = $.cls('posteruid', el)[Main.hasMobileLayout ? 0 : 1])) { + IDColor.applyRemote(uid.firstElementChild); + } + } + + return el; +}; + +Parser.decodeSpecialChars = function(str) { + return str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>'); +}; + +Parser.encodeSpecialChars = function(str) { + return str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +}; + +Parser.buildHTMLFromJSON = function(data, board, standalone, fromQuote) { + var + container = document.createElement('div'), + isOP = false, + + userId, + fileDims = '', + imgSrc = '', + fileInfo = '', + fileHtml = '', + fileThumb, + filePath, + fileName, + fileSpoilerTip = '"', + size = '', + fileClass = '', + shortFile = '', + longFile = '', + tripcode = '', + capcodeStart = '', + capcodeClass = '', + capcode = '', + flag, + highlight = '', + emailStart = '', + emailEnd = '', + name, + subject, + noLink, + quoteLink, + replySpan = '', + noFilename, + decodedFilename, + mobileLink = '', + postType = 'reply', + summary = '', + postCountStr, + resto, + capcode_replies = '', + threadIcons = '', + needFileTip = false, + + i, q, href, quotes, + + imgDir = '//i.4cdn.org/' + board; + + if (data.resto == 0) { + isOP = true; + + if (standalone) { + mobileLink = ''; + postType = 'op'; + replySpan = '  [Reply]' + } + + resto = data.no; + } + else { + resto = data.resto; + } + + + if (!Main.tid || board != Main.board) { + noLink = 'thread/' + resto + '#p' + data.no; + quoteLink = 'thread/' + resto + '#q' + data.no; + } + else { + noLink = '#p' + data.no; + quoteLink = 'javascript:quote(\'' + data.no + '\')'; + } + + if (!data.capcode && data.id) { + userId = ' (ID: ' + + data.id + ') '; + } + else { + userId = ''; + } + + switch (data.capcode) { + case 'admin_highlight': + highlight = ' highlightPost'; + case 'admin': + capcodeStart = ' ## Admin'; + capcodeClass = ' capcodeAdmin'; + + capcode = ' '; + break; + case 'mod': + capcodeStart = ' ## Mod'; + capcodeClass = ' capcodeMod'; + + capcode = ' '; + break; + case 'developer': + capcodeStart = ' ## Developer'; + capcodeClass = ' capcodeDeveloper'; + + capcode = ' '; + break; + case 'manager': + capcodeStart = ' ## Manager'; + capcodeClass = ' capcodeManager'; + + capcode = ' '; + break; + } + + if (data.email) { + emailStart = ''; + emailEnd = ''; + } + + if (data.country) { + if (board == 'pol') { + flag = ' '
+        + data.country + ''; + } + else { + flag = ' '; + } + } + else { + flag = ''; + } + + if (data.filedeleted) { + fileHtml = '
    File deleted.
    '; + } + else if (data.ext) { + decodedFilename = Parser.decodeSpecialChars(data.filename); + + shortFile = longFile = data.filename + data.ext; + + if (decodedFilename.length > (isOP ? 40 : 30)) { + shortFile = Parser.encodeSpecialChars( + decodedFilename.slice(0, isOP ? 35 : 25) + ) + '(...)' + data.ext; + + needFileTip = true; + } + + if (!data.tn_w && !data.tn_h && data.ext == '.gif') { + data.tn_w = data.w; + data.tn_h = data.h; + } + if (data.fsize >= 1048576) { + size = ((0 | (data.fsize / 1048576 * 100 + 0.5)) / 100) + ' M'; + } + else if (data.fsize > 1024) { + size = (0 | (data.fsize / 1024 + 0.5)) + ' K'; + } + else { + size = data.fsize + ' '; + } + + if (data.spoiler) { + if (!Config.revealSpoilers) { + fileName = 'Spoiler Image'; + fileSpoilerTip = '" title="' + longFile + '"'; + fileClass = ' imgspoiler'; + + fileThumb = '//s.4cdn.org/image/spoiler' + + (Parser.customSpoiler[board] || '') + '.png'; + data.tn_w = 100; + data.tn_h = 100; + + noFilename = true; + } + else { + fileName = shortFile; + } + } + else { + fileName = shortFile; + } + + if (!fileThumb) { + fileThumb = '//0.t.4cdn.org/' + board + '/' + data.tim + 's.jpg'; + } + + fileDims = data.ext == '.pdf' ? 'PDF' : data.w + 'x' + data.h; + + if (board != 'f') { + filePath = imgDir + '/' + data.tim + data.ext; + + imgSrc = '' + size + 'B' + + '
    ' + size + 'B ' + + data.ext.slice(1).toUpperCase() + + '
    '; + + fileInfo = '
    ' + + fileName + ' (' + size + 'B, ' + fileDims + ')
    '; + } + else { + filePath = imgDir + '/' + data.filename + data.ext; + + fileDims += ', ' + data.tag; + + fileInfo = '
    File: ' + + data.filename + '.swf (' + size + 'B, ' + fileDims + ')
    '; + } + + fileHtml = '
    ' + + fileInfo + imgSrc + '
    '; + } + + if (data.trip) { + tripcode = ' ' + data.trip + ''; + } + + name = data.name || ''; + + + if (isOP) { + if (data.capcode_replies) { + capcode_replies = Parser.buildCapcodeReplies(data.capcode_replies, board, data.no); + } + + if (fromQuote && data.replies) { + postCountStr = data.replies + ' post' + (data.replies > 1 ? 's' : ''); + + if (data.images) { + postCountStr += ' and ' + data.images + ' image repl' + + (data.images > 1 ? 'ies' : 'y'); + } + + summary = '' + postCountStr + '.'; + } + + if (data.sticky) { + threadIcons += 'Sticky '; + } + + if (data.closed) { + if (data.archived) { + threadIcons += 'Archived '; + } + else { + threadIcons += 'Closed '; + } + } + + subject = '' + (data.sub || '') + ' '; + } + else { + subject = ''; + } + + container.className = 'postContainer ' + postType + 'Container'; + container.id = 'pc' + data.no; + + container.innerHTML = + (isOP ? '' : '
    >>
    ') + + '
    ' + + '' + + (isOP ? fileHtml : '') + + '' + + (isOP ? '' : fileHtml) + + '
    ' + + (data.com || '') + capcode_replies + summary + '
    ' + + '
    ' + mobileLink; + + if (!Main.tid || board != Main.board) { + quotes = container.getElementsByClassName('quotelink'); + for (i = 0; q = quotes[i]; ++i) { + href = q.getAttribute('href'); + if (href.charAt(0) != '/') { + q.href = '/' + board + '/thread/' + resto + href; + } + } + } + + return container; +}; + +Parser.buildCapcodeReplies = function(replies, board, tid) { + var i, capcode, id, html, map, post_ids, prelink, pretext; + + map = { + admin: 'Administrator', + mod: 'Moderator', + developer: 'Developer', + manager: 'Manager' + }; + + if (board != Main.board) { + prelink = '/' + board + '/thread/'; + pretext = '>>>/' + board + '/'; + } + else { + prelink = ''; + pretext = '>>'; + } + + html = '

    '; + + for (capcode in replies) { + html += '' + map[capcode] + ' Replies: '; + + post_ids = replies[capcode]; + + for (i = 0; id = post_ids[i]; ++i) { + html += '' + pretext + id + ' '; + } + } + + return html + ''; +}; + +Parser.parseBoard = function() { + var i, threads = document.getElementsByClassName('thread'); + + for (i = 0; threads[i]; ++i) { + Parser.parseThread(threads[i].id.slice(1)); + } +}; + +Parser.parseThread = function(tid, offset, limit) { + var i, j, thread, posts, pi, el, frag, summary, omitted, key, filtered, cnt, + frag; + + thread = $.id('t' + tid); + posts = thread.getElementsByClassName('post'); + + if (!offset) { + pi = document.getElementById('pi' + tid); + + if (!Main.tid) { + if (Config.filter) { + filtered = Filter.exec( + thread, + pi, + document.getElementById('m' + tid), + tid + ); + } + + if (Config.threadHiding && !filtered) { + if (Main.hasMobileLayout) { + el = document.createElement('a'); + el.href = 'javascript:;'; + el.setAttribute('data-cmd', 'hide'); + el.setAttribute('data-id', tid); + el.className = 'mobileHideButton button'; + el.textContent = 'Hide'; + posts[0].nextElementSibling.appendChild(el); + } + else { + el = document.createElement('span'); + el.innerHTML = 'H'; + posts[0].insertBefore(el, posts[0].firstChild); + } + el.id = 'sa' + tid; + if (ThreadHiding.hidden[tid]) { + ThreadHiding.hidden[tid] = Main.now; + ThreadHiding.hide(tid); + } + } + + if (ThreadExpansion.enabled + && (summary = $.cls('summary', thread)[0])) { + frag = document.createDocumentFragment(); + + omitted = summary.cloneNode(true); + omitted.className = ''; + summary.textContent = ''; + + el = document.createElement('img'); + el.className = 'extButton expbtn'; + el.title = 'Expand thread'; + el.alt = '+'; + el.setAttribute('data-cmd', 'expand'); + el.setAttribute('data-id', tid); + el.src = Main.icons.plus; + frag.appendChild(el); + + frag.appendChild(omitted); + + el = document.createElement('span'); + el.style.display = 'none'; + el.textContent = 'Showing all replies.' + frag.appendChild(el); + + summary.appendChild(frag); + } + } + + if (Main.tid && Config.threadWatcher && (cnt = $.cls('navLinksBot')[0])) { + el = document.createElement('img'); + + if (ThreadWatcher.watched[key = tid + '-' + Main.board]) { + el.src = Main.icons.watched; + el.setAttribute('data-active', '1'); + } + else { + el.src = Main.icons.notwatched; + } + + el.className = 'extButton wbtn wbtn-' + key; + el.setAttribute('data-cmd', 'watch'); + el.setAttribute('data-id', tid); + el.alt = 'W'; + el.title = 'Add to watch list'; + + frag = document.createDocumentFragment(); + frag.appendChild(document.createTextNode('[')); + frag.appendChild(el.cloneNode(true)); + frag.appendChild(document.createTextNode('] ')); + cnt.insertBefore(frag, cnt.firstChild); + } + } + + j = offset ? offset < 0 ? posts.length + offset : offset : 0; + limit = limit ? j + limit : posts.length; + + if (Main.isMobileDevice && Config.quotePreview) { + for (i = j; i < limit; ++i) { + Parser.parseMobileQuotelinks(posts[i]); + } + } + + if (Parser.trackedReplies) { + for (i = j; i < limit; ++i) { + Parser.parseTrackedReplies(posts[i]); + } + } + + for (i = j; i < limit; ++i) { + Parser.parsePost(posts[i].id.slice(1), tid); + } + + if (offset) { + if (Parser.prettify) { + for (i = j; i < limit; ++i) { + Parser.parseMarkup(posts[i]); + } + } + if (window.jsMath) { + if (window.jsMath.loaded) { + for (i = j; i < limit; ++i) { + window.jsMath.ProcessBeforeShowing(posts[i]); + } + } + else { + Parser.loadJSMath(); + } + } + } + + UA.dispatchEvent('4chanParsingDone', { threadId: tid, offset: j, limit: limit }); +}; + +Parser.loadJSMath = function(root) { + if ($.cls('math', root)[0]) { + window.jsMath.Autoload.Script.Push('ProcessBeforeShowing', [ null ]); + window.jsMath.Autoload.LoadJsMath(); + } +}; + +Parser.parseMathOne = function(node) { + if (window.jsMath.loaded) { + window.jsMath.ProcessBeforeShowing(node); + } + else { + Parser.loadJSMath(node); + } +}; + +Parser.parseTrackedReplies = function(post) { + var i, link, quotelinks; + + quotelinks = $.cls('quotelink', post); + + for (i = 0; link = quotelinks[i]; ++i) { + if (Parser.trackedReplies[link.textContent]) { + link.textContent += ' (You)'; + Parser.hasYouMarkers = true; + } + } +}; + +Parser.parseMobileQuotelinks = function(post) { + var i, link, quotelinks, t, el; + + quotelinks = $.cls('quotelink', post); + + for (i = 0; link = quotelinks[i]; ++i) { + t = link.getAttribute('href').match(/^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/); + + if (!t) { + continue; + } + + el = document.createElement('a'); + el.href = link.href; + el.textContent = ' #'; + el.className = 'quoteLink'; + + link.parentNode.insertBefore(el, link.nextSibling); + } +}; + +Parser.parseMarkup = function(post) { + var i, pre, el; + + if ((pre = post.getElementsByClassName('prettyprint'))[0]) { + for (i = 0; el = pre[i]; ++i) { + el.innerHTML = prettyPrintOne(el.innerHTML); + } + } +}; + +Parser.parsePost = function(pid, tid) { + var hasMobileLayout, cnt, el, pi, href, img, file, msg, filtered, html, filename, txt, finfo, isOP, uid; + + hasMobileLayout = Main.hasMobileLayout; + + if (!tid) { + pi = pid.getElementsByClassName('postInfo')[0]; + pid = pi.id.slice(2); + } + else { + pi = document.getElementById('pi' + pid); + } + + if (Parser.needMsg) { + msg = document.getElementById('m' + pid); + } + + if (hasMobileLayout) { + if (Config.reportButton) { + el = document.createElement('span'); + el.className = 'mobile mobile-report'; + el.setAttribute('data-cmd', 'report'); + el.setAttribute('data-id', pid); + el.textContent = 'Report'; + pi.parentNode.appendChild(el); + } + } + else { + el = document.createElement('a'); + el.href = '#'; + el.className = 'postMenuBtn'; + el.title = 'Post menu'; + el.setAttribute('data-cmd', 'post-menu'); + el.textContent = '▶'; + pi.appendChild(el); + } + + if (tid && pid != tid) { + if (Config.filter) { + filtered = Filter.exec(pi.parentNode, pi, msg); + } + + if (!filtered && ReplyHiding.hidden[pid]) { + ReplyHiding.hidden[pid] = Main.now; + ReplyHiding.hide(pid); + } + + if (Config.backlinks) { + Parser.parseBacklinks(pid, tid); + } + } + + if (IDColor.enabled && (uid = $.cls('posteruid', pi.parentNode)[hasMobileLayout ? 0 : 1])) { + IDColor.apply(uid.firstElementChild); + } + + if (Config.embedSoundCloud) { + Media.parseSoundCloud(msg); + } + + if (Config.embedYouTube) { + Media.parseYouTube(msg); + } + + if (Config.embedVocaroo) { + Media.parseVocaroo(msg); + } + + if (Config.revealSpoilers + && (file = document.getElementById('f' + pid)) + && (file = file.children[1]) + ) { + if ($.hasClass(file, 'imgspoiler')) { + img = file.firstChild; + file.removeChild(img); + img.removeAttribute('style'); + isOP = $.hasClass(pi.parentNode, 'op'); + img.style.maxWidth = img.style.maxHeight = isOP ? '250px' : '125px'; + img.src = '//0.t.4cdn.org' + + (file.pathname.replace(/([0-9]+).+$/, '/$1s.jpg')); + + filename = file.previousElementSibling; + finfo = filename.title.split('.'); + + if (finfo[0].length > (isOP ? 40 : 30)) { + txt = finfo[0].slice(0, isOP ? 35 : 25) + '(...)' + finfo[1]; + } + else { + txt = filename.title; + filename.removeAttribute('title'); + } + + filename.firstElementChild.innerHTML = txt; + file.insertBefore(img, file.firstElementChild); + } + } + + if (Config.localTime) { + if (hasMobileLayout) { + el = pi.parentNode.getElementsByClassName('dateTime')[0]; + el.firstChild.nodeValue + = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)) + ' '; + } + else { + el = pi.getElementsByClassName('dateTime')[0]; + el.title = this.utcOffset; + el.textContent + = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)); + } + } + +}; + +Parser.getLocaleDate = function(date) { + return ('0' + (1 + date.getMonth())).slice(-2) + '/' + + ('0' + date.getDate()).slice(-2) + '/' + + ('0' + date.getFullYear()).slice(-2) + '(' + + this.weekdays[date.getDay()] + ')' + + ('0' + date.getHours()).slice(-2) + ':' + + ('0' + date.getMinutes()).slice(-2) + ':' + + ('0' + date.getSeconds()).slice(-2); +}; + +Parser.parseBacklinks = function(pid, tid) { + var i, j, msg, backlinks, linklist, ids, target, bid, html, bl, el, href; + + msg = document.getElementById('m' + pid); + + if (!(backlinks = msg.getElementsByClassName('quotelink'))) { + return; + } + + linklist = {}; + + for (i = 0; j = backlinks[i]; ++i) { + // [tid, pid] + ids = j.getAttribute('href').split('#p'); + + if (!ids[1]) { + continue; + } + + if (ids[1] == tid) { + j.textContent += ' (OP)'; + } + + if (!(target = document.getElementById('pi' + ids[1]))) { + if (Main.tid && j.textContent.charAt(2) != '>' ) { + j.textContent += ' →'; + } + continue; + } + + // Already processed? + if (linklist[ids[1]]) { + continue; + } + + linklist[ids[1]] = true; + + // Backlink node + bl = document.createElement('span'); + + if (!Main.tid) { + href = 'thread/' + tid + '#p' + pid; + } + else { + href = '#p' + pid; + } + + if (!Main.hasMobileLayout) { + bl.innerHTML = '>>' + pid + ' '; + } + else { + bl.innerHTML = '>>' + pid + + ' # '; + } + + // Backlinks container + if (!(el = document.getElementById('bl_' + ids[1]))) { + el = document.createElement('div'); + el.id = 'bl_' + ids[1]; + el.className = 'backlink'; + + if (Main.hasMobileLayout) { + el.className = 'backlink mobile'; + target = document.getElementById('p' + ids[1]); + } + + target.appendChild(el); + } + + el.appendChild(bl); + } +}; + +Parser.buildSummary = function(tid, oRep, oImg) { + if (oRep) { + oRep = oRep + ' post' + (oRep > 1 ? 's' : ''); + } + else { + return null; + } + + if (oImg) { + oImg = ' and ' + oImg + ' image repl' + (oImg > 1 ? 'ies' : 'y'); + } + else { + oImg = ''; + } + + el = document.createElement('span'); + el.className = 'summary desktop'; + el.innerHTML = oRep + oImg + + ' omitted. Click here to view.'; + + return el; +}; + +/** + * Sync + */ +var UserSync = { + url: 'https://sys.4chan.org/sync', + timeout: null, + processing: false, + maxDelay: 3600000, + queue: {} +}; + +UserSync.onEnable = function() { + var tkn = Math.random().toString(16).substring(2) + + Math.random().toString(16).substring(2); + + Main.setCookie('sync', tkn, '4chan.org'); +}; + +UserSync.onDisable = function() { + Main.removeCookie('sync', '4chan.org'); + localStorage.removeItem('4chan-sync-ts'); +}; + +UserSync.onSyncNowClick = function() { + UserSync.syncStatus(true); +}; + +UserSync.purgeSync = function() { + var xhr, tkn; + + tkn = Main.getCookie('sync'); + + if (!tkn) { + alert("Syncing doesn't seem to be enabled on this machine"); + return; + } + + if (!confirm('All data associated with this sync key will be deleted from the server.')) { + return; + } + + xhr = new XMLHttpRequest(); + xhr.open('POST', UserSync.url + '?action=purge'); + xhr.onload = UserSync.onPurgeSyncLoaded; + xhr.onerror = UserSync.onSyncError; + xhr.withCredentials = true; + xhr.withFeedback = true; + + //Feedback.notify('Processing…', false); + + xhr.send(JSON.stringify({tkn: tkn})); +}; + +UserSync.onPurgeSyncLoaded = function() { + var resp = JSON.parse(this.responseText); + + if (resp.error) { + return Feedback.error(resp.error); + } + + //Feedback.notify('Done'); + + UserSync.onDisable(); +}; + +UserSync.syncStatus = function(withFeedback) { + var xhr; + + if (UserSync.processing) { + console.log('Sync: Already syncing'); + return; + } + + UserSync.processing = true; + + if (withFeedback) { + //Feedback.notify('Syncing…', false); + } + + xhr = new XMLHttpRequest(); + xhr.open('GET', UserSync.url + '?action=status'); + xhr.withCredentials = true; + xhr.withFeedback = withFeedback; + xhr.onerror = UserSync.onSyncError; + xhr.onload = UserSync.onSyncStatusLoaded; + xhr.send(null); +}; + +UserSync.onSyncStatusLoaded = function() { + var i, key, item, items, get, set, req, xhr, local_ts, remote_ts, data, syncTs, tkn; + + UserSync.processing = false; + + items = JSON.parse(this.responseText); + + if (items.error) { + console.log('Sync: ' + items.error); + return; + } + + syncTs = UserSync.getSyncTs(); + syncTs.ts = Date.now(); + + get = []; + set = {}; + + for (key in items) { + local_ts = syncTs[key] || 0; + remote_ts = items[key] || 0; + + if (remote_ts > local_ts) { + get.push(key); + } + else if (local_ts > remote_ts) { + data = localStorage.getItem(key); + + if (data) { + set[key] = { + ts: local_ts, + data: JSON.parse(data) + }; + } + else { + delete syncTs[key]; + } + } + } + + UserSync.setSyncTs(syncTs); + + req = {}; + + if (get.length) { + req['get'] = get; + } + + for (i in set) { + req['set'] = set; + break; + } + + if (!req['get'] && !req['set']) { + if (this.withFeedback) { + //Feedback.notify('Done'); + } + if (Config.threadWatcher) { + ThreadWatcher.onUserSyncLoaded(); + } + return; + } + + tkn = Main.getCookie('sync'); + + if (!tkn) { + return; + } + + req.tkn = tkn; + + xhr = new XMLHttpRequest(); + xhr.open('POST', UserSync.url + '?action=sync'); + xhr.withCredentials = true; + xhr.withFeedback = this.withFeedback; + xhr.onload = UserSync.onSyncLoaded; + xhr.onerror = UserSync.onSyncError; + xhr.send(JSON.stringify(req)); +}; + +UserSync.onSyncError = function() { + var msg = 'Sync: Connection Error'; + + UserSync.processing = false; + + UserSync.resetSyncTs(); + + console.log(msg); +}; + +UserSync.getSyncTs = function() { + var data = localStorage.getItem('4chan-sync-ts'); + + return data ? JSON.parse(data) : {}; +}; + +UserSync.setSyncTs = function(data) { + return localStorage.setItem('4chan-sync-ts', JSON.stringify(data)); +}; + +UserSync.resetSyncTs = function() { + var tsData = UserSync.getSyncTs(); + delete tsData.ts; + UserSync.setSyncTs(tsData); +}; + +UserSync.onSyncLoaded = function() { + var items, key, value, local_ts, syncTs; + + items = JSON.parse(this.responseText); + + if (items.error) { + console.log('Sync: ' + items.error); + return; + } + + if (this.withFeedback) { + //Feedback.notify('Done'); + } + + syncTs = UserSync.getSyncTs(); + syncTs.ts = Date.now(); + + for (key in items) { + value = items[key]; + + local_ts = syncTs[key] || 0; + + if (+local_ts > +value['ts']) { + continue; + } + + localStorage.setItem(key, JSON.stringify(value['data'])); + + syncTs[key] = value['ts']; + } + + UserSync.setSyncTs(syncTs); + + if (Config.threadWatcher) { + ThreadWatcher.onUserSyncLoaded(); + } +}; + +UserSync.onQueueProcessed = function() { + var items; + + items = JSON.parse(this.responseText); + + if (items.error) { + console.log('Sync: ' + items.error); + } +} + +UserSync.syncPush = function(key) { + var ts, tsData; + + ts = Math.round(Date.now() / 1000); + + tsData = UserSync.getSyncTs(); + tsData[key] = ts; + UserSync.setSyncTs(tsData); + + UserSync.queue[key] = ts; + + if (UserSync.timeout) { + clearTimeout(UserSync.timeout); + } + + UserSync.timeout = setTimeout(UserSync.syncProcessQueue, 1000); +}; + +UserSync.syncProcessQueue = function() { + var set, xhr, key, tkn; + + tkn = Main.getCookie('sync'); + + if (!tkn) { + UserSync.queue = {}; + return; + } + + set = {}; + + for (key in UserSync.queue) { + set[key] = { + ts: UserSync.queue[key], + data: JSON.parse(localStorage.getItem(key)) + } + } + + UserSync.queue = {}; + + xhr = new XMLHttpRequest(); + xhr.open('POST', UserSync.url + '?action=sync'); + xhr.withCredentials = true; + xhr.onload = UserSync.onQueueProcessed; + xhr.onerror = UserSync.onSyncError; + xhr.send(JSON.stringify({ + tkn: tkn, + set: set + })); +}; + + +/** + * Post Menu + */ +var PostMenu = { + activeBtn: null +}; + +PostMenu.open = function(btn) { + var div, html, pid, board, btnPos, txt, el, href, left, limit, isOP; + + PostMenu.close(); + + pid = btn.parentNode.id.split('pi')[1]; + + board = btn.parentNode.getAttribute('data-board'); + + isOP = !board && !!$.id('t' + pid); + + html = '
    • Report post
    • '; + + if (isOP) { + if (!Main.tid) { + html += '
    • ' + + ($.hasClass($.id('t' + pid), 'post-hidden') ? 'Unhide' : 'Hide') + + ' thread
    • '; + } + if (Config.threadWatcher) { + html += '
    • ' + + (ThreadWatcher.watched[pid + '-' + Main.board] ? 'Remove from' : 'Add to') + + ' watch list
    • '; + } + } + else if (el = $.id('pc' + pid)) { + html += '
    • ' + + ($.hasClass(el, 'post-hidden') ? 'Unhide' : 'Hide') + + ' post
    • '; + } + + if (file = $.id('fT' + pid)) { + el = $.cls('fileThumb', file.parentNode)[0]; + + if (el) { + if (/\.(png|jpg)$/.test(el.href)) { + href = el.href; + } + else { + href = 'http://0.t.4cdn.org/' + Main.board + '/' + + el.href.match(/\/([0-9]+)\..+$/)[1] + 's.jpg'; + } + + html += '
    • Image search »
    • '; + } + } + + if (Config.filter) { + html += '
    • Filter selected text
    • '; + } + + div = document.createElement('div'); + div.id = 'post-menu'; + div.className = 'dd-menu'; + div.innerHTML = html + '
    '; + + btnPos = btn.getBoundingClientRect(); + + div.style.top = btnPos.bottom + 3 + window.pageYOffset + 'px'; + + document.addEventListener('click', PostMenu.close, false); + + $.addClass(btn, 'menuOpen'); + PostMenu.activeBtn = btn; + + UA.dispatchEvent('4chanPostMenuReady', { postId: pid, isOP: isOP, node: div.firstElementChild }); + + document.body.appendChild(div); + + left = btnPos.left + window.pageXOffset; + limit = $.docEl.clientWidth - div.offsetWidth; + + if (left > (limit - 75)) { + div.className += ' dd-menu-left'; + } + + if (left > limit) { + left = limit; + } + + div.style.left = left + 'px'; +}; + +PostMenu.close = function() { + var el; + + if (el = $.id('post-menu')) { + el.parentNode.removeChild(el); + document.removeEventListener('click', PostMenu.close, false); + $.removeClass(PostMenu.activeBtn, 'menuOpen'); + PostMenu.activeBtn = null; + } +}; + +/** + * Depager + */ +var Depager = {}; + +Depager.init = function() { + var el, el2, cnt; + + this.isLoading = false; + this.isEnabled = false; + this.isComplete = false; + this.threadsLoaded = false; + this.threadQueue = []; + this.debounce = 100; + this.threshold = 350; + + this.adId = 'azk53379'; + this.adZones = [ 16258, 16260 ]; + + this.boardHasAds = !!$.id(this.adId); + + if (this.boardHasAds) { + el = $.cls('ad-plea'); + this.adPlea = el[el.length - 1]; + } + + if (el = $.cls('prev')[0]) { + el.innerHTML = '[All]'; + el = el.firstElementChild; + } + else { + return; + } + + if (Config.alwaysDepage) { + this.isEnabled = true; + el.parentNode.parentNode.className += ' depagerEnabled'; + Depager.bindHandlers(); + + if (cnt = $.cls('board')[0]) { + el2 = document.createElement('span'); + el2.className = 'depageNumber'; + el2.textContent = 'Page 1'; + cnt.insertBefore(el2, cnt.firstElementChild); + } + } + else { + el.setAttribute('data-cmd', 'depage'); + } +}; + +Depager.onScroll = function() { + if (document.documentElement.scrollHeight + <= (window.innerHeight + window.pageYOffset + Depager.threshold)) { + if (Depager.threadsLoaded) { + Depager.renderNext(); + } + else { + Depager.depage(); + } + } +}; + +Depager.trackPageview = function(pageId) { + var url; + + try { + if (window._gat) { + url = '/' + Main.board + '/' + pageId; + window._gat._getTrackerByName()._trackPageview(url); + } + + if (window.__qc) { + window.__qc.qpixelsent = []; + window._qevents.push({ qacct: window.__qc.qopts.qacct }); + window.__qc.firepixels(); + } + } + catch(e) { + console.log(e); + } +}; + +Depager.insertAd = function(pageId, frag, zone, isLastPage) { + var wrap, cnt, nodes; + + if (!Depager.boardHasAds || !window.ados_add_placement) { + return; + } + + if (isLastPage) { + nodes = $.cls('bottomad'); + wrap = nodes[nodes.length - 1]; + cnt = document.createElement('div'); + cnt.id = 'azkDepage' + pageId; + wrap.appendChild(cnt); + window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone); + } + else { + wrap = document.createElement('div'); + wrap.className = 'bottomad center'; + + if (pageId == 2) { + cnt = $.id(Depager.adId); + } + else { + cnt = document.createElement('div'); + cnt.id = 'azkDepage' + pageId; + } + + wrap.appendChild(cnt); + frag.appendChild(wrap); + + if (Depager.adPlea) { + frag.appendChild(Depager.adPlea.cloneNode(true)); + } + + frag.appendChild(document.createElement('hr')); + + if (pageId != 2) { + window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone); + } + } +}; + +Depager.loadAds = function() { + if (!Depager.boardHasAds || !window.ados_load) { + return; + } + + window.ados_load(); +}; + +Depager.renderNext = function() { + var el, frag, i, j, k, threads, op, summary, cnt, reply, parseList, scroll, + lastReplies, pageId, data, isLastPage, html; + + parseList = []; + + scroll = window.pageYOffset; + + frag = document.createDocumentFragment(); + + data = Depager.threadQueue.shift(); + + if (!data) { + return; + } + + threads = data.threads; + pageId = data.page; + + isLastPage = !Depager.threadQueue.length; + + Depager.insertAd(pageId, frag, data.adZone, isLastPage); + + el = document.createElement('span'); + el.className = 'depageNumber'; + el.textContent = 'Page ' + pageId; + frag.appendChild(el); + + for (j = 0; op = threads[j]; ++j) { + if ($.id('t' + op.no)) { + continue; + } + + cnt = document.createElement('div'); + cnt.id = 't' + op.no; + cnt.className = 'thread'; + + cnt.appendChild(Parser.buildHTMLFromJSON(op, Main.board, true)); + + if (summary = Parser.buildSummary(op.no, op.omitted_posts, op.omitted_images)) { + cnt.appendChild(summary); + } + + if (op.replies) { + last_replies = op.last_replies; + + for (k = 0; reply = last_replies[k]; ++k) { + cnt.appendChild(Parser.buildHTMLFromJSON(reply, Main.board)); + } + } + + frag.appendChild(cnt); + + frag.appendChild(document.createElement('hr')); + + parseList.push(op.no); + } + + if (isLastPage) { + Depager.unbindHandlers(); + Depager.isComplete = true; + Depager.setStatus('disabled'); + } + + boardDiv = $.cls('board')[0]; + boardDiv.insertBefore(frag, boardDiv.lastElementChild); + + Depager.trackPageview(pageId); + + Depager.loadAds(); + + for (i = 0; op = parseList[i]; ++i) { + Parser.parseThread(op); + } + + window.scrollTo(0, scroll); +}; + +Depager.bindHandlers = function() { + window.addEventListener('scroll', Depager.onScroll, false); + window.addEventListener('resize', Depager.onScroll, false); +}; + +Depager.unbindHandlers = function() { + window.removeEventListener('scroll', Depager.onScroll, false); + window.removeEventListener('resize', Depager.onScroll, false); +}; + +Depager.setStatus = function(type) { + var i, el, links, p; + + links = $.cls('depagelink'); + + if (!links.length) { + return; + } + + if (type == 'enabled') { + for (i = 0; el = links[i]; ++i) { + el.textContent = 'All'; + p = el.parentNode.parentNode; + if (!$.hasClass(p, 'depagerEnabled')) { + $.addClass(p,'depagerEnabled'); + } + } + } + else if (type == 'loading') { + for (i = 0; el = links[i]; ++i) { + el.textContent = 'Loading…'; + } + } + else if (type == 'disabled') { + for (i = 0; el = links[i]; ++i) { + el.textContent = 'All'; + $.removeClass(el.parentNode.parentNode,'depagerEnabled'); + } + } + else if (type == 'error') { + for (i = 0; el = links[i]; ++i) { + el.textContent = 'Error'; + el.removeAttribute('title'); + el.removeAttribute('data-cmd'); + $.removeClass(el.parentNode.parentNode, 'depagerEnabled'); + } + } +}; + +Depager.toggle = function() { + if (Depager.isLoading || Depager.isComplete) { + return; + } + + if (Depager.isEnabled) { + Depager.disable(); + } + else { + Depager.enable(); + } + + Depager.isEnabled = !Depager.isEnabled; +}; + +Depager.enable = function() { + Depager.bindHandlers(); + Depager.setStatus('enabled'); + Depager.onScroll(); +}; + +Depager.disable = function() { + Depager.unbindHandlers(); + Depager.setStatus('disabled'); +}; + +Depager.depage = function() { + if (Depager.isLoading) { + return; + } + + Depager.isLoading = true; + + $.get('//a.4cdn.org/' + Main.board + '/catalog.json', { + onload: Depager.onLoad, + onerror: Depager.onError + }); + + Depager.setStatus('loading'); +}; + +Depager.onLoad = function() { + var catalog, i, page, queue, adZone; + + Depager.isLoading = false; + Depager.threadsLoaded = true; + + if (this.status == 200) { + Depager.setStatus('enabled'); + + if (!Config.alwaysDepage) { + Depager.bindHandlers(); + } + + catalog = Parser.parseCatalogJSON(this.responseText); + + queue = Depager.threadQueue; + + adZone = 0; + for (i = 1; page = catalog[i]; ++i) { + page.adZone = adZone; + queue.push(page); + adZone = adZone ? 0 : 1; + } + + Depager.renderNext(); + } + else if (this.status == 404) { + Depager.unbindHandlers(); + Depager.setStatus('error'); + } + else { + Depager.unbindHandlers(); + console.log('Error: ' + this.status); + Depager.setStatus('error'); + } +}; + +Depager.onError = function() { + Depager.isLoading = false; + Depager.unbindHandlers(); + console.log('Error: ' + this.status); + Depager.setStatus('error'); +}; + +/** + * Quote inlining + */ +var QuoteInline = {}; + +QuoteInline.isSelfQuote = function(node, pid, board) { + var cnt; + + if (board && board != Main.board) { + return false; + } + + node = node.parentNode; + + if ((node.nodeName == 'BLOCKQUOTE' && node.id.split('m')[1] == pid) + || node.parentNode.id.split('_')[1] == pid) { + return true; + } + + return false; +}; + +QuoteInline.toggle = function(link, e) { + var t, pfx, src, el, count; + + t = link.getAttribute('href').match(/^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/); + + if (!t || t[1] == 'rs' || QuoteInline.isSelfQuote(link, t[3], t[1])) { + return; + } + + e && e.preventDefault(); + + if (pfx = link.getAttribute('data-pfx')) { + link.removeAttribute('data-pfx'); + $.removeClass(link, 'linkfade'); + + el = $.id(pfx + 'p' + t[3]); + el.parentNode.removeChild(el); + + if (link.parentNode.parentNode.className == 'backlink') { + el = $.id('pc' + t[3]); + count = +el.getAttribute('data-inline-count') - 1; + if (count == 0) { + el.style.display = ''; + el.removeAttribute('data-inline-count'); + } + else { + el.setAttribute('data-inline-count', count); + } + } + + return; + } + + if (src = $.id('p' + t[3])) { + QuoteInline.inline(link, src, t[3]); + } + else { + QuoteInline.inlineRemote(link, t[1] || Main.board, t[2], t[3]); + } +}; + +QuoteInline.inlineRemote = function(link, board, tid, pid) { + var xhr, onload, onerror, cached, key, el, dummy; + + if (link.hasAttribute('data-loading')) { + return; + } + + key = board + '-' + tid; + + if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) { + Parser.parsePost(el); + QuoteInline.inline(link, el); + return; + } + + if ((dummy = link.nextElementSibling) && $.hasClass(dummy, 'spinner')) { + dummy.parentNode.removeChild(dummy); + return; + } + else { + dummy = document.createElement('div'); + } + + dummy.className = 'preview spinner inlined'; + dummy.textContent = 'Loading...'; + link.parentNode.insertBefore(dummy, link.nextSibling); + + onload = function() { + var el, thread; + + link.removeAttribute('data-loading'); + + if (this.status == 200 || this.status == 304 || this.status == 0) { + thread = Parser.parseThreadJSON(this.responseText); + + $.cache[key] = thread; + + if (el = Parser.buildPost(thread, board, pid)) { + dummy.parentNode && dummy.parentNode.removeChild(dummy); + Parser.parsePost(el); + QuoteInline.inline(link, el); + } + else { + $.addClass(link, 'deadlink'); + dummy.textContent = 'This post doesn\'t exist anymore'; + } + } + else if (this.status == 404) { + $.addClass(link, 'deadlink'); + dummy.textContent = 'This thread doesn\'t exist anymore'; + } + else { + this.onerror(); + } + }; + + onerror = function() { + dummy.textContent = 'Error: ' + this.statusText + ' (' + this.status + ')'; + link.removeAttribute('data-loading'); + }; + + link.setAttribute('data-loading', '1'); + + $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json', + { + onload: onload, + onerror: onerror + } + ); +}; + +QuoteInline.inline = function(link, src, id) { + var i, j, now, el, blcnt, isBl, inner, tblcnt, pfx, dest, count, cnt; + + now = Date.now(); + + if (id) { + if ((blcnt = link.parentNode.parentNode).className == 'backlink') { + el = blcnt.parentNode.parentNode.parentNode; + isBl = true; + } + else { + el = blcnt.parentNode; + } + + while (el.parentNode !== document) { + if (el.id.split('m')[1] == id) { + return; + } + el = el.parentNode; + } + } + + link.className += ' linkfade'; + link.setAttribute('data-pfx', now); + + el = src.cloneNode(true); + el.id = now + el.id; + el.setAttribute('data-pfx', now); + el.className += ' preview inlined'; + $.removeClass(el, 'highlight'); + $.removeClass(el, 'highlight-anti'); + + if ((inner = $.cls('inlined', el))[0]) { + while (j = inner[0]) { + j.parentNode.removeChild(j); + } + inner = $.cls('quotelink', el); + for (i = 0; j = inner[i]; ++i) { + j.removeAttribute('data-pfx'); + $.removeClass(j, 'linkfade'); + } + } + + for (i = 0; j = el.children[i]; ++i) { + j.id = now + j.id; + } + + if (tblcnt = $.cls('backlink', el)[0]) { + tblcnt.id = now + tblcnt.id; + } + + if (isBl) { + pfx = blcnt.parentNode.parentNode.getAttribute('data-pfx') || ''; + dest = $.id(pfx + 'm' + blcnt.id.split('_')[1]); + dest.insertBefore(el, dest.firstChild); + if (count = src.parentNode.getAttribute('data-inline-count')) { + count = +count + 1; + } + else { + count = 1; + src.parentNode.style.display = 'none'; + } + src.parentNode.setAttribute('data-inline-count', count); + } + else { + if ($.hasClass(link.parentNode, 'quote')) { + link = link.parentNode; + cnt = link.parentNode; + } + else { + cnt = link.parentNode; + } + cnt.insertBefore(el, link.nextSibling); + } +}; + +/** + * Quote preview + */ +var QuotePreview = {}; + +QuotePreview.init = function() { + var thread; + + this.regex = /^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/; + this.highlight = null; + this.highlightAnti = null; + this.out = true; +}; + +QuotePreview.resolve = function(link) { + var self, t, post, ids, offset, pfx; + + self = QuotePreview; + self.out = false; + + t = link.getAttribute('href').match(self.regex); + + if (!t) { + return; + } + + // Quoted post in scope + pfx = link.getAttribute('data-pfx') || ''; + + if (post = document.getElementById(pfx + 'p' + t[3])) { + // Visible and not filtered out? + offset = post.getBoundingClientRect(); + if (offset.top > 0 + && offset.bottom < document.documentElement.clientHeight + && !$.hasClass(post.parentNode, 'post-hidden')) { + if (!$.hasClass(post, 'highlight') && location.hash.slice(1) != post.id) { + self.highlight = post; + $.addClass(post, 'highlight'); + } + else if (!$.hasClass(post, 'op')) { + self.highlightAnti = post; + $.addClass(post, 'highlight-anti'); + } + return; + } + // Nope + self.show(link, post); + } + // Quoted post out of scope + else { + if (!UA.hasCORS) { + return; + } + self.showRemote(link, t[1] || Main.board, t[2], t[3]); + } +}; + +QuotePreview.showRemote = function(link, board, tid, pid) { + var xhr, onload, onerror, el, cached, key; + + key = board + '-' + tid; + + if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) { + QuotePreview.show(link, el); + return; + } + + link.style.cursor = 'wait'; + + onload = function() { + var el, thread; + + link.style.cursor = ''; + + if (this.status == 200 || this.status == 304 || this.status == 0) { + thread = Parser.parseThreadJSON(this.responseText); + + $.cache[key] = thread; + + if ($.id('quote-preview') || QuotePreview.out) { + return; + } + + if (el = Parser.buildPost(thread, board, pid)) { + el.className = 'post preview'; + el.style.display = 'none'; + el.id = 'quote-preview'; + document.body.appendChild(el); + QuotePreview.show(link, el, true); + } + else { + $.addClass(link, 'deadlink'); + } + } + else if (this.status == 404) { + $.addClass(link, 'deadlink'); + } + }; + + onerror = function() { + link.style.cursor = ''; + }; + + $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json', + { + onload: onload, + onerror: onerror + } + ); +}; + +QuotePreview.show = function(link, post, remote) { + var rect, postHeight, postWidth, doc, docWidth, style, pos, quotes, i, j, qid, + top, scrollTop, margin, img; + + if (remote) { + Parser.parsePost(post); + post.style.display = ''; + } + else { + post = post.cloneNode(true); + if (location.hash && location.hash == ('#' + post.id)) { + post.className += ' highlight'; + } + post.id = 'quote-preview'; + post.className += ' preview'; + + if (Config.imageExpansion && (img = $.cls('expanded-thumb', post)[0])) { + ImageExpansion.contract(img); + } + } + + if (!link.parentNode.className) { + quotes = post.querySelectorAll( + '#' + $.cls('postMessage', post)[0].id + ' > .quotelink' + ); + if (quotes[1]) { + qid = '>>' + link.parentNode.parentNode.id.split('_')[1]; + for (i = 0; j = quotes[i]; ++i) { + if (j.textContent == qid) { + $.addClass(j, 'dotted'); + break; + } + } + } + } + + rect = link.getBoundingClientRect(); + doc = document.documentElement; + docWidth = doc.offsetWidth; + style = post.style; + + document.body.appendChild(post); + + if (Main.isMobileDevice) { + style.top = rect.top + link.offsetHeight + window.pageYOffset + 'px'; + + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + style.right = docWidth - rect.right + 'px'; + } + else { + style.left = rect.left + 'px'; + } + } + else { + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + pos = docWidth - rect.left; + style.right = pos + 5 + 'px'; + } + else { + pos = rect.left + rect.width; + style.left = pos + 5 + 'px'; + } + + top = rect.top + link.offsetHeight + window.pageYOffset + - post.offsetHeight / 2 - rect.height / 2; + + postHeight = post.getBoundingClientRect().height; + + if (doc.scrollTop != document.body.scrollTop) { + scrollTop = doc.scrollTop + document.body.scrollTop; + } else { + scrollTop = document.body.scrollTop; + } + + if (top < scrollTop) { + style.top = scrollTop + 'px'; + } + else if (top + postHeight > scrollTop + doc.clientHeight) { + style.top = scrollTop + doc.clientHeight - postHeight + 'px'; + } + else { + style.top = top + 'px'; + } + } +}; + +QuotePreview.remove = function(el) { + var self, cnt; + + self = QuotePreview; + self.out = true; + + if (self.highlight) { + $.removeClass(self.highlight, 'highlight'); + self.highlight = null; + } + else if (self.highlightAnti) { + $.removeClass(self.highlightAnti, 'highlight-anti'); + self.highlightAnti = null + } + + if (el) { + el.style.cursor = ''; + } + + if (cnt = $.id('quote-preview')) { + document.body.removeChild(cnt); + } +}; + +/** + * Image expansion + */ +var ImageExpansion = { + activeVideos: [], + timeout: null +}; + +ImageExpansion.expand = function(thumb) { + var img, el, href, ext; + + if (Config.imageHover) { + ImageHover.hide(); + } + + href = thumb.parentNode.getAttribute('href'); + + if (ext = href.match(/\.(?:webm|pdf)$/)) { + if (!Main.hasMobileLayout && ext[0] == '.webm') { + return ImageExpansion.expandWebm(thumb); + } + return false; + } + + thumb.setAttribute('data-expanding', '1'); + + img = document.createElement('img'); + img.alt = 'Image'; + img.setAttribute('src', href); + img.className = 'expanded-thumb'; + img.style.display = 'none'; + img.onerror = this.onError; + + thumb.parentNode.insertBefore(img, thumb.nextElementSibling); + + if (UA.hasCORS) { + thumb.style.opacity = '0.75'; + this.timeout = this.checkLoadStart(img, thumb); + } + else { + this.onLoadStart(img, thumb); + } + + return true; +}; + +ImageExpansion.contract = function(img) { + var cnt, p; + + clearTimeout(this.timeout); + + p = img.parentNode; + cnt = p.parentNode.parentNode; + + $.removeClass(p.parentNode, 'image-expanded'); + + if (Config.centeredThreads) { + $.removeClass(cnt.parentNode, 'centre-exp'); + cnt.parentNode.style.marginLeft = ''; + } + + if (!Main.tid && Config.threadHiding) { + $.removeClass(p, 'image-expanded-anti'); + } + + p.firstChild.style.display = ''; + + p.removeChild(img); + + if (cnt.offsetTop < window.pageYOffset) { + cnt.scrollIntoView(); + } +}; + +ImageExpansion.toggle = function(t) { + if (t.hasAttribute('data-md5')) { + if (!t.hasAttribute('data-expanding')) { + return ImageExpansion.expand(t); + } + } + else { + ImageExpansion.contract(t); + } + + return true; +}; + +ImageExpansion.expandWebm = function(thumb) { + var el, link, fileText, left, width, href, maxWidth, self; + + self = ImageExpansion; + + if (el = document.getElementById('image-hover')) { + document.body.removeChild(el); + } + + link = thumb.parentNode; + + href = link.getAttribute('href'); + + left = link.getBoundingClientRect().left; + maxWidth = document.documentElement.clientWidth - left - 25; + + el = document.createElement('video'); + el.muted = true; + el.controls = true; + el.loop = true; + el.autoplay = true; + el.className = 'expandedWebm'; + el.onloadedmetadata = ImageExpansion.fitWebm; + el.onplay = ImageExpansion.onWebmPlay; + el.src = href; + + link.style.display = 'none'; + link.parentNode.appendChild(el); + + fileText = thumb.parentNode.previousElementSibling; + + el = document.createElement('span'); + el.className = 'collapseWebm'; + el.innerHTML = '-[Close]'; + el.firstElementChild.addEventListener('click', self.collapseWebm, false); + + fileText.appendChild(el); + + return true; +}; + +ImageExpansion.fitWebm = function() { + var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, cntEl, + centerWidth, ofs; + + if (Config.centeredThreads) { + centerWidth = $.cls('opContainer')[0].offsetWidth; + cntEl = this.parentNode.parentNode.parentNode; + $.addClass(cntEl, 'centre-exp') + } + + left = this.getBoundingClientRect().left; + + maxWidth = document.documentElement.clientWidth - left - 25; + maxHeight = document.documentElement.clientHeight; + + imgWidth = this.videoWidth; + imgHeight = this.videoHeight; + + if (imgWidth > maxWidth) { + ratio = maxWidth / imgWidth; + imgWidth = maxWidth; + imgHeight = imgHeight * ratio; + } + + if (Config.fitToScreenExpansion && imgHeight > maxHeight) { + ratio = maxHeight / imgHeight; + imgHeight = maxHeight; + imgWidth = imgWidth * ratio; + } + + this.style.maxWidth = imgWidth + 'px'; + this.style.maxHeight = imgHeight + 'px'; + + if (Config.centeredThreads) { + left = this.getBoundingClientRect().left; + ofs = this.offsetWidth + left * 2; + if (ofs > centerWidth) { + left = Math.floor(($.docEl.clientWidth - ofs) / 2); + + if (left > 0) { + cntEl.style.marginLeft = left + 'px'; + } + } + else { + $.removeClass(cntEl, 'centre-exp') + } + } +}; + +ImageExpansion.onWebmPlay = function(e) { + var self = ImageExpansion; + + if (!self.activeVideos.length) { + document.addEventListener('scroll', self.onScroll, false); + } + + self.activeVideos.push(this); +}; + +ImageExpansion.collapseWebm = function(e) { + var cnt, el, el2; + + e.preventDefault(); + + this.removeEventListener('click', ImageExpansion.collapseWebm, false); + + cnt = this.parentNode; + el = cnt.parentNode.parentNode.getElementsByClassName('expandedWebm')[0]; + + if (Config.centeredThreads) { + el2 = el.parentNode.parentNode.parentNode; + $.removeClass(el2, 'centre-exp') + el2.style.marginLeft = ''; + } + + el.previousElementSibling.style.display = ''; + el.parentNode.removeChild(el); + cnt.parentNode.removeChild(cnt); +}; + +ImageExpansion.onScroll = function(e) { + clearTimeout(ImageExpansion.timeout); + ImageExpansion.timeout = setTimeout(ImageExpansion.pauseVideos, 500); +}; + +ImageExpansion.pauseVideos = function() { + var self, i, el, pos, min, max, nodes; + + self = ImageExpansion; + + nodes = []; + min = window.pageYOffset; + max = window.pageYOffset + $.docEl.clientHeight; + + for (i = 0; el = self.activeVideos[i]; ++i) { + pos = el.getBoundingClientRect(); + if (pos.top + window.pageYOffset > max || pos.bottom + window.pageYOffset < min) { + el.pause(); + } + else if (!el.paused){ + nodes.push(el); + } + } + + if (!nodes.length) { + document.removeEventListener('scroll', self.onScroll, false); + } + + self.activeVideos = nodes; +}; + +ImageExpansion.onError = function(e) { + var thumb, img; + + img = e.target; + thumb = $.qs('img[data-expanding]', img.parentNode); + + img.parentNode.removeChild(img); + thumb.style.opacity = ''; + thumb.removeAttribute('data-expanding'); +}; + +ImageExpansion.onLoadStart = function(img, thumb) { + var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, fileEl, cntEl, + centerWidth, ofs; + + thumb.removeAttribute('data-expanding'); + + fileEl = thumb.parentNode.parentNode; + + if (Config.centeredThreads) { + cntEl = fileEl.parentNode.parentNode; + centerWidth = $.cls('opContainer')[0].offsetWidth; + $.addClass(cntEl, 'centre-exp'); + } + + left = thumb.getBoundingClientRect().left; + + maxWidth = $.docEl.clientWidth - left - 25; + maxHeight = $.docEl.clientHeight; + + imgWidth = img.naturalWidth; + imgHeight = img.naturalHeight; + + if (imgWidth > maxWidth) { + ratio = maxWidth / imgWidth; + imgWidth = maxWidth; + imgHeight = imgHeight * ratio; + } + + if (Config.fitToScreenExpansion && imgHeight > maxHeight) { + ratio = maxHeight / imgHeight; + imgHeight = maxHeight; + imgWidth = imgWidth * ratio; + } + + img.style.maxWidth = imgWidth + 'px'; + img.style.maxHeight = imgHeight + 'px'; + + $.addClass(fileEl, 'image-expanded'); + + if (!Main.tid && Config.threadHiding) { + $.addClass(thumb.parentNode, 'image-expanded-anti'); + } + + img.style.display = ''; + thumb.style.display = 'none'; + + if (Config.centeredThreads) { + left = img.getBoundingClientRect().left; + ofs = img.offsetWidth + left * 2; + if (ofs > centerWidth) { + left = Math.floor(($.docEl.clientWidth - ofs) / 2); + + if (left > 0) { + cntEl.style.marginLeft = left + 'px'; + } + } + else { + $.removeClass(cntEl, 'centre-exp'); + } + } +}; + +ImageExpansion.checkLoadStart = function(img, thumb) { + if (img.naturalWidth) { + ImageExpansion.onLoadStart(img, thumb); + thumb.style.opacity = ''; + } + else { + return setTimeout(ImageExpansion.checkLoadStart, 15, img, thumb); + } +}; + +/** + * Image hover + */ +var ImageHover = {}; + +ImageHover.show = function(thumb) { + var el, href, ext; + + href = thumb.parentNode.getAttribute('href'); + + if (ext = href.match(/\.(?:webm|pdf)$/)) { + if (ext[0] == '.webm') { + ImageHover.showWebm(thumb); + } + return; + } + + el = document.createElement('img'); + el.id = 'image-hover'; + el.alt = 'Image'; + el.setAttribute('src', href); + + document.body.appendChild(el); + + if (UA.hasCORS) { + el.style.display = 'none'; + this.timeout = ImageHover.checkLoadStart(el, thumb); + } + else { + el.style.left = thumb.getBoundingClientRect().right + 10 + 'px'; + } +}; + +ImageHover.hide = function() { + var img; + clearTimeout(this.timeout); + if (img = $.id('image-hover')) { + if (img.play) { + Tip.hide(); + } + document.body.removeChild(img); + } +}; + +ImageHover.showWebm = function(thumb) { + var dims, el, bounds, limit, width; + + dims = thumb.parentNode.previousElementSibling.textContent.match(/, ([0-9]+)x[0-9]+/); + width = +dims[1]; + + el = document.createElement('video'); + el.id = 'image-hover'; + el.src = thumb.parentNode.getAttribute('href'); + el.loop = true; + el.muted = true; + el.autoplay = true; + el.onloadedmetadata = function() { ImageHover.showWebMDuration(this, thumb); }; + + bounds = thumb.getBoundingClientRect(); + limit = window.innerWidth - bounds.right - 20; + + if (width > limit) { + el.style.maxWidth = limit + 'px'; + } + + document.body.appendChild(el); +}; + +ImageHover.showWebMDuration = function(el, thumb) { + if (!el.parentNode) { + return; + } + + var ms = $.prettySeconds(el.duration); + + Tip.show(thumb, ms[0] + ':' + ('0' + ms[1]).slice(-2)); +}; + +ImageHover.onLoadStart = function(img, thumb) { + var bounds, limit; + + bounds = thumb.getBoundingClientRect(); + limit = window.innerWidth - bounds.right - 20; + + if (img.naturalWidth > limit) { + img.style.maxWidth = limit + 'px'; + } + + img.style.display = ''; +}; + +ImageHover.checkLoadStart = function(img, thumb) { + if (img.naturalWidth) { + ImageHover.onLoadStart(img, thumb); + } + else { + return setTimeout(ImageHover.checkLoadStart, 15, img, thumb); + } +}; + +/** + * Quick reply + */ +var QR = {}; + +QR.init = function() { + var item; + + if (!UA.hasFormData) { + return; + } + + this.enabled = true; + this.currentTid = null; + this.cooldown = null; + this.timestamp = null; + this.auto = false; + + this.btn = null; + this.comField = null; + this.comLength = window.comlen; + this.lenCheckTimeout = null; + + this.preuploadSizeLimit = Main.hasMobileLayout ? 0 : 204800; + + this.cdElapsed = 0; + this.activeDelay = 0; + + this.cooldowns = {}; + + for (item in window.cooldowns) { + this.cooldowns[item] = window.cooldowns[item] * 1000; + } + + this.captchaDelay = 240500; + this.captchaInterval = null; + this.pulse = null; + this.xhr = null; + + this.fileDisabled = !!window.imagelimit; + + this.tracked = {}; + + this.lastTid = localStorage.getItem('4chan-cd-' + Main.board + '-tid'); + + if (Main.tid && !Main.hasMobileLayout && !Main.threadClosed) { + QR.addReplyLink(); + } + + window.addEventListener('storage', this.syncStorage, false); +}; + +QR.addReplyLink = function() { + var cnt, el; + + cnt = $.cls('navLinks')[2]; + + el = document.createElement('div'); + el.className = 'open-qr-wrap'; + el.innerHTML = '[Post a Reply]'; + + cnt.insertBefore(el, cnt.firstChild); +}; + +QR.lock = function() { + QR.showPostError('This thread is closed.', 'closed', true); +}; + +QR.unlock = function() { + QR.hidePostError('closed'); +}; + +QR.syncStorage = function(e) { + var key; + + if (!e.key) { + return; + } + + key = e.key.split('-'); + + if (key[0] != '4chan') { + return; + } + + if (key[1] == 'cd' && e.newValue && Main.board == key[2]) { + if (key[3] == 'tid') { + QR.lastTid = e.newValue; + } + else { + QR.startCooldown(); + } + } +}; + +QR.quotePost = function(tid, pid) { + if (!QR.noCooldown + && (Main.threadClosed || (!Main.tid && Main.isThreadClosed(tid)))) { + alert('This thread is closed'); + return; + } + QR.show(tid); + QR.addQuote(pid); +}; + +QR.addQuote = function(pid) { + var q, pos, sel, ta; + + ta = $.tag('textarea', document.forms.qrPost)[0]; + + pos = ta.selectionStart; + + sel = UA.getSelection(); + + if (pid) { + q = '>>' + pid + '\n'; + } + else { + q = ''; + } + + if (sel) { + q += '>' + sel.trim().replace(/[\r\n]+/g, '\n>') + '\n'; + } + + if (ta.value) { + ta.value = ta.value.slice(0, pos) + + q + ta.value.slice(ta.selectionEnd); + } + else { + ta.value = q; + } + if (UA.isOpera) { + pos += q.split('\n').length; + } + + ta.selectionStart = ta.selectionEnd = pos + q.length; + + if (ta.selectionStart == ta.value.length) { + ta.scrollTop = ta.scrollHeight; + } + ta.focus(); +}; + +QR.show = function(tid) { + var i, j, cnt, postForm, form, qrForm, fields, row, spoiler, file, + el, el2, placeholder, cd, qrError, cookie; + + if (QR.currentTid) { + if (!Main.tid && QR.currentTid != tid) { + $.id('qrTid').textContent = $.id('qrResto').value = QR.currentTid = tid; + $.byName('com')[1].value = ''; + + QR.startCooldown(); + } + + if (Main.hasMobileLayout) { + $.id('quickReply').style.top = window.pageYOffset + 25 + 'px'; + } + + return; + } + + QR.currentTid = tid; + + postForm = $.id('postForm'); + + cnt = document.createElement('div'); + cnt.id = 'quickReply'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-trackpos', 'QR-position'); + + if (Main.hasMobileLayout) { + cnt.style.top = window.pageYOffset + 28 + 'px'; + } + else if (Config['QR-position']) { + cnt.style.cssText = Config['QR-position']; + } + else { + cnt.style.right = '0px'; + cnt.style.top = '10%'; + } + + cnt.innerHTML = + '
    Reply to Thread No.' + + tid + 'X
    '; + + form = postForm.parentNode.cloneNode(false); + form.setAttribute('name', 'qrPost'); + form.innerHTML = + '' + + '' + + ''; + + qrForm = document.createElement('div'); + qrForm.id = 'qrForm'; + + fields = postForm.firstElementChild.children; + for (i = 0, j = fields.length - 1; i < j; ++i) { + row = document.createElement('div'); + if (fields[i].id == 'captchaFormPart') { + if (QR.noCaptcha) { + continue; + } + row.id = 'qrCaptchaContainer'; + } + else { + placeholder = fields[i].getAttribute('data-type'); + if (placeholder == 'Password' || placeholder == 'Spoilers') { + continue; + } + else if (placeholder == 'File') { + file = fields[i].children[1].firstChild.cloneNode(false); + file.tabIndex += 20; + file.id = 'qrFile'; + file.size = '19'; + file.addEventListener('change', QR.onFileChange, false); + row.appendChild(file); + + if (UA.hasDragAndDrop) { + $.addClass(file, 'qrRealFile'); + + file = document.createElement('div'); + file.id = 'qrDummyFile'; + + el = document.createElement('button'); + el.id = 'qrDummyFileButton'; + el.type = 'button'; + el.textContent = 'Browse…'; + file.appendChild(el); + + el = document.createElement('span'); + el.id = 'qrDummyFileLabel'; + el.textContent = 'No file selected.'; + file.appendChild(el); + + row.appendChild(file); + } + + file.title = 'Shift + Click to remove the file'; + } + else { + row.innerHTML = fields[i].children[1].innerHTML; + if (row.firstChild.type == 'hidden') { + el = row.lastChild.previousSibling; + } + else { + el = row.firstChild; + } + if (el.tabIndex > 0) { + el.tabIndex += 20; + } + if (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA') { + if (el.name == 'name') { + if (cookie = Main.getCookie('4chan_name')) { + el.value = cookie; + } + } + else if (el.name == 'email') { + el.id = 'qrEmail'; + } + else if (el.name == 'com') { + QR.comField = el; + el.addEventListener('keydown', QR.onKeyDown, false); + el.addEventListener('paste', QR.onKeyDown, false); + el.addEventListener('cut', QR.onKeyDown, false); + if (row.children[1]) { + row.removeChild(el.nextSibling); + } + } + else if (el.name == 'sub') { + continue; + } + if (placeholder !== null) { + el.setAttribute('placeholder', placeholder); + } + } + else if ((el.name == 'flag')) { + if (el2 = el.querySelector('option[selected]')) { + el2.removeAttribute('selected'); + } + if ((cookie = Main.getCookie('4chan_flag')) && + (el2 = el.querySelector('option[value="' + cookie + '"]'))) { + el2.setAttribute('selected', 'selected'); + } + } + } + } + qrForm.appendChild(row); + } + + this.btn = qrForm.querySelector('input[type="submit"]'); + this.btn.previousSibling.className = 'presubmit'; + this.btn.tabIndex += 20; + + if (el = postForm.querySelector('.desktop > label > input[name="spoiler"]')) { + spoiler = document.createElement('span'); + spoiler.id = 'qrSpoiler'; + spoiler.innerHTML = ''; + file.parentNode.insertBefore(spoiler, file.nextSibling); + } + + form.appendChild(qrForm); + cnt.appendChild(form); + + qrError = document.createElement('div'); + qrError.id = 'qrError'; + cnt.appendChild(qrError); + + cnt.addEventListener('click', QR.onClick, false); + + document.body.appendChild(cnt); + + QR.startCooldown(); + + if (Main.threadClosed) { + QR.lock(); + } + + if (!window.passEnabled) { + if (window.captchaReady) { + if (QR.captchaInterval === null) { + QR.onCaptchaReady(); + } + else { + QR.reloadCaptcha(); + } + } + else { + window.loadRecaptcha(); + } + } + + if (!Main.hasMobileLayout) { + Draggable.set($.id('qrHeader')); + } +}; + +QR.onCaptchaReady = function() { + if (!$.id('qrCaptchaContainer')) { + QR.captchaInterval = 1; + return; + } + + QR.pollCaptcha(); +}; + +QR.onFileChange = function(e) { + var fsize, maxFilesize; + + QR.needPreuploadCaptcha = false; + + if (this.value) { + maxFilesize = window.maxFilesize; + + if (this.files) { + fsize = this.files[0].size; + if (this.files[0].type == 'video/webm' && window.maxWebmFilesize) { + maxFilesize = window.maxWebmFilesize; + } + } + else { + fsize = 0; + } + + if (QR.fileDisabled) { + QR.showPostError('Image limit reached.', 'imagelimit', true); + } + else if (fsize > maxFilesize) { + QR.showPostError('Error: Maximum file size allowed is ' + + Math.floor(maxFilesize / 1048576) + ' MB', 'filesize', true); + } + else { + QR.hidePostError(); + } + + if (fsize >= QR.preuploadSizeLimit) { + QR.needPreuploadCaptcha = true; + } + } + else { + QR.hidePostError(); + } + + QR.startCooldown(); +}; + +QR.onKeyDown = function(e) { + if (e.ctrlKey && e.keyCode == 83) { + var ta, start, end, spoiler; + + e.stopPropagation(); + e.preventDefault(); + + ta = e.target; + start = ta.selectionStart; + end = ta.selectionEnd; + + if (ta.value) { + spoiler = '[spoiler]' + ta.value.slice(start, end) + '[/spoiler]'; + ta.value = ta.value.slice(0, start) + spoiler + ta.value.slice(end); + ta.setSelectionRange(end + 19, end + 19); + } + else { + ta.value = '[spoiler][/spoiler]'; + ta.setSelectionRange(9, 9); + } + } + else if (e.keyCode == 27 && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { + QR.close(); + return; + } + + clearTimeout(QR.lenCheckTimeout); + QR.lenCheckTimeout = setTimeout(QR.checkComLength, 500); +}; + +QR.checkComLength = function() { + var byteLength, qrError; + + if (QR.comLength) { + byteLength = encodeURIComponent(QR.comField.value).split(/%..|./).length - 1; + + if (byteLength > QR.comLength) { + QR.showPostError('Error: Comment too long (' + + byteLength + '/' + QR.comLength + ').', 'length', true); + } + else { + QR.hidePostError('length'); + } + } +}; + +QR.close = function() { + var el, cnt = $.id('quickReply'); + + QR.comField = null; + QR.currentTid = null; + + clearInterval(QR.captchaInterval); + clearInterval(QR.pulse); + + if (QR.xhr) { + QR.xhr.abort(); + QR.xhr = null; + } + + cnt.removeEventListener('click', QR.onClick, false); + + (el = $.id('qrFile')) && el.removeEventListener('change', QR.startCooldown, false); + (el = $.id('qrEmail')) && el.removeEventListener('change', QR.startCooldown, false); + $.tag('textarea', cnt)[0].removeEventListener('keydown', QR.onKeyDown, false); + + Draggable.unset($.id('qrHeader')); + + if (window.RecaptchaState) { + Recaptcha.destroy(); + window.captchaReady = false; + if (el = $.id('captchaContainer')) { + el.innerHTML = '
    ' + + el.getAttribute('data-placeholder') + '
    '; + } + } + + document.body.removeChild(cnt); +}; + +QR.cloneCaptcha = function() { + var row = $.id('qrCaptchaContainer'); + + if (!row) { + return false; + } + + row.innerHTML = 'reCAPTCHA challenge image' + + (window.preupload_captcha ? '' : '') + + '' + + ''; + + return true; +}; + +QR.reloadCaptcha = function(focus) { + var pulse, poll; + + if (QR.noCaptcha || !$.id('recaptcha_image') || !window.RecaptchaState) { + return; + } + + poll = function() { + var el; + clearTimeout(pulse); + if (el = $.id('recaptcha_challenge_image')) { + QR.captchaInterval = setInterval(QR.cloneCaptcha, QR.captchaDelay); + QR.cloneCaptcha(); + if (focus) { + $.id('qrCapField').focus(); + } + } + else { + pulse = setTimeout(poll, 100); + } + }; + clearInterval(QR.captchaInterval); + Recaptcha.destroy(); + window.loadRecaptcha(); + pulse = setTimeout(poll, 100); +}; + +QR.pollCaptcha = function() { + clearTimeout(QR.captchaPollTimeout); + + if ($.id('recaptcha_challenge_image')) { + QR.captchaInterval = setInterval(QR.cloneCaptcha, QR.captchaDelay); + QR.cloneCaptcha(); + } + else { + QR.captchaPollTimeout = setTimeout(QR.pollCaptcha, 100); + } +}; + +QR.onClick = function(e) { + var t = e.target; + + if (t.type == 'submit') { + e.preventDefault(); + QR.submit(e.shiftKey); + } + else { + switch (t.id) { + case 'qrFile': + if (e.shiftKey) { + e.preventDefault(); + QR.resetFile(); + } + break; + case 'qrDummyFile': + case 'qrDummyFileButton': + case 'qrDummyFileLabel': + e.preventDefault(); + if (e.shiftKey) { + QR.resetFile(); + } + else { + $.id('qrFile').click(); + } + break; + case 'qrCaptcha': + QR.reloadCaptcha(true); + break; + case 'qrClose': + QR.close(); + break; + } + } +}; + +QR.submit = function(force) { + if (force) { + QR.submitDirect(true); + } + else if (!QR.noCaptcha && window.preupload_captcha && QR.needPreuploadCaptcha) { + QR.submitPreupload(); + } + else { + QR.submitDirect(); + } +}; + +QR.showPostError = function(msg, type, silent) { + var qrError; + + qrError = $.id('qrError'); + + if (!qrError) { + return; + } + + qrError.innerHTML = msg; + qrError.style.display = 'block'; + + qrError.setAttribute('data-type', type || ''); + + if (!silent && (document.hidden + || document.mozHidden + || document.webkitHidden + || document.msHidden)) { + alert('Posting Error'); + } +}; + +QR.hidePostError = function(type) { + var el = $.id('qrError'); + + if (!el.hasAttribute('style')) { + return; + } + + if (!type || el.getAttribute('data-type') == type) { + el.removeAttribute('style'); + } +}; + +QR.resetFile = function() { + var file, el; + + el = document.createElement('input'); + el.id = 'qrFile'; + el.type = 'file'; + el.size = '19'; + el.name = 'upfile'; + el.addEventListener('change', QR.onFileChange, false); + + file = $.id('qrFile'); + file.removeEventListener('change', QR.onFileChange, false); + + file.parentNode.replaceChild(el, file); + + QR.hidePostError('imagelimit'); + + QR.needPreuploadCaptcha = false; + + QR.startCooldown(); +}; + +QR.submitPreupload = function() { + var token, challenge, response, data; + + if (!QR.presubmitChecks()) { + return; + } + + challenge = $.id('qrChallenge'); + response = $.id('qrCapField'); + + if (response.value == '') { + QR.showPostError('You forgot to type in the CAPTCHA.'); + response.focus(); + return; + } + + data = new FormData(); + data.append('mode', 'checkcaptcha'); + data.append('challenge', challenge.value); + data.append('response', response.value); + + QR.xhr = new XMLHttpRequest(); + + QR.xhr.open('POST', document.forms.post.action, true); + + QR.xhr.onerror = function() { + QR.xhr = null; + QR.submitDirect(); + }; + + QR.xhr.onload = function() { + var el, resp; + + QR.xhr = null; + + try { + resp = JSON.parse(this.responseText); + } + catch(e) { + console.log("Couldn't verify captcha."); + QR.submitDirect(); + return; + } + + if (resp.token) { + el = $.id('qrCapToken'); + el.value = resp.token; + el.removeAttribute('disabled'); + + QR.submitDirect(); + } + else if (resp.error) { + QR.reloadCaptcha(); + QR.btn.value = 'Post'; + QR.showPostError(resp.error); + } + else { + if (resp.fail) { + console.log(resp.fail); + } + QR.submitDirect(); + } + }; + + token = $.id('qrCapToken'); + token.value = ''; + token.setAttribute('disabled', '1'); + + QR.btn.value = 'Sending'; + + QR.xhr.send(data); +}; + +QR.submitDirect = function(force) { + var field, formdata, file; + + QR.hidePostError(); + + if (!QR.presubmitChecks(force)) { + return; + } + + QR.auto = false; + + if (!force && (field = $.id('qrCapField')) && field.value == '') { + QR.showPostError('You forgot to type in the CAPTCHA.'); + field.focus(); + return; + } + + QR.xhr = new XMLHttpRequest(); + + QR.xhr.open('POST', document.forms.qrPost.action, true); + + QR.xhr.withCredentials = true; + + QR.xhr.upload.onprogress = function(e) { + if (e.loaded >= e.total) { + QR.btn.value = '100%'; + } + else { + QR.btn.value = (0 | (e.loaded / e.total * 100)) + '%'; + } + }; + + QR.xhr.onerror = function() { + QR.xhr = null; + QR.showPostError('Connection error.'); + }; + + QR.xhr.onload = function() { + var resp, el, hasFile, ids, tid, pid, tracked; + + QR.xhr = null; + + QR.btn.value = 'Post'; + + if (this.status == 200) { + if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) { + QR.reloadCaptcha(true); + QR.showPostError(resp[1]); + return; + } + + if (ids = this.responseText.match(//)) { + tid = ids[1]; + pid = ids[2]; + + QR.lastTid = tid; + + localStorage.setItem('4chan-cd-' + Main.board + '-tid', tid); + + hasFile = (el = $.id('qrFile')) && el.value; + + QR.setPostTime(); + + if (Config.persistentQR) { + $.byName('com')[1].value = ''; + + if (el = $.byName('spoiler')[2]) { + el.checked = false; + } + + QR.reloadCaptcha(); + + if (hasFile) { + QR.resetFile(); + } + + QR.startCooldown(); + } + else { + QR.close(); + } + + if (Main.tid) { + if (Config.threadWatcher) { + ThreadWatcher.setLastRead(pid, tid); + } + QR.lastReplyId = +pid; + Parser.trackedReplies['>>' + pid] = 1; + Parser.saveTrackedReplies(tid, Parser.trackedReplies); + } + else { + tracked = Parser.getTrackedReplies(tid) || {}; + tracked['>>' + pid] = 1; + Parser.saveTrackedReplies(tid, tracked); + } + + UA.dispatchEvent('4chanQRPostSuccess', { threadId: tid, postId: pid }); + } + + if (ThreadUpdater.enabled) { + setTimeout(ThreadUpdater.forceUpdate, 500); + } + } + else { + QR.showPostError('Error: ' + this.status + ' ' + this.statusText); + } + }; + + formdata = new FormData(document.forms.qrPost); + + clearInterval(QR.pulse); + + QR.btn.value = 'Sending'; + + QR.xhr.send(formdata); +}; + +QR.presubmitChecks = function(force) { + if (QR.xhr) { + QR.xhr.abort(); + QR.xhr = null; + QR.showPostError('Aborted.'); + QR.btn.value = 'Post'; + return false; + } + + if (!force && QR.cooldown) { + if (QR.auto = !QR.auto) { + QR.btn.value = QR.cooldown + 's (auto)'; + } + else { + QR.btn.value = QR.cooldown + 's'; + } + return false; + } + + return true; +}; + +QR.getCooldown = function(type) { + if (QR.currentTid != QR.lastTid) { + return QR.cooldowns[type]; + } + else { + return QR.cooldowns[type + '_intra']; + } +}; + +QR.setPostTime = function() { + return localStorage.setItem('4chan-cd-' + Main.board, Date.now()); +}; + +QR.getPostTime = function() { + return localStorage.getItem('4chan-cd-' + Main.board); +}; + +QR.removePostTime = function() { + return localStorage.removeItem('4chan-cd-' + Main.board); +}; + +QR.startCooldown = function() { + var type, el, time; + + if (QR.noCooldown || !$.id('quickReply') || QR.xhr) { + return; + } + + clearInterval(QR.pulse); + + type = ((el = $.id('qrFile')) && el.value) ? 'image' : 'reply'; + + time = QR.getPostTime(type); + + if (!time) { + QR.btn.value = 'Post'; + return; + } + + QR.timestamp = parseInt(time, 10); + + QR.activeDelay = QR.getCooldown(type); + + QR.cdElapsed = Date.now() - QR.timestamp; + + QR.cooldown = Math.floor((QR.activeDelay - QR.cdElapsed) / 1000); + + if (QR.cooldown <= 0 || QR.cdElapsed < 0) { + QR.cooldown = false; + QR.removePostTime(type); + return; + } + + QR.btn.value = QR.cooldown + 's'; + + QR.pulse = setInterval(QR.onPulse, 1000); +}; + +QR.onPulse = function() { + QR.cdElapsed = Date.now() - QR.timestamp; + QR.cooldown = Math.floor((QR.activeDelay - QR.cdElapsed) / 1000); + if (QR.cooldown <= 0) { + clearInterval(QR.pulse); + QR.btn.value = 'Post'; + QR.cooldown = false; + if (QR.auto) { + QR.submit(); + } + } + else { + QR.btn.value = QR.cooldown + (QR.auto ? 's (auto)' : 's'); + } +}; + +/** + * Thread hiding + */ +var ThreadHiding = {}; + +ThreadHiding.init = function() { + this.threshold = 43200000; // 12 hours + + this.hidden = {}; + + this.load(); + + this.purge(); +}; + +ThreadHiding.clear = function(silent) { + var i, id, key, msg; + + this.load(); + + i = 0; + + for (id in this.hidden) { + ++i; + } + + key = '4chan-hide-t-' + Main.board; + + if (!silent) { + if (!i) { + alert("You don't have any hidden threads on /" + Main.board + '/'); + return; + } + + msg = 'This will unhide ' + i + ' thread' + (i > 1 ? 's' : '') + ' on /' + Main.board + '/'; + + if (!confirm(msg)) { + return; + } + + localStorage.removeItem(key); + } + else { + localStorage.removeItem(key); + } +}; + +ThreadHiding.isHidden = function(tid) { + var sa = $.id('sa' + tid); + + return !sa || sa.hasAttribute('data-hidden'); +}; + +ThreadHiding.toggle = function(tid) { + if (this.isHidden(tid)) { + this.show(tid); + } + else { + this.hide(tid); + } + this.save(); +}; + +ThreadHiding.show = function(tid) { + var sa, th; + + th = $.id('t' + tid); + + sa = $.id('sa' + tid); + sa.removeAttribute('data-hidden'); + + if (Main.hasMobileLayout) { + sa.textContent = 'Hide'; + $.removeClass(sa, 'mobile-tu-show'); + $.cls('postLink', th)[0].appendChild(sa); + + th.style.display = null; + $.removeClass(th.nextElementSibling, 'mobile-hr-hidden'); + } + else { + sa.firstChild.src = Main.icons.minus; + $.removeClass(th, 'post-hidden'); + } + + delete this.hidden[tid]; +}; + +ThreadHiding.hide = function(tid) { + var sa, th; + + th = $.id('t' + tid); + + if (Main.hasMobileLayout) { + th.style.display = 'none'; + $.addClass(th.nextElementSibling, 'mobile-hr-hidden'); + + sa = $.id('sa' + tid); + sa.setAttribute('data-hidden', tid); + sa.textContent = 'Show Hidden Thread'; + $.addClass(sa, 'mobile-tu-show'); + + th.parentNode.insertBefore(sa, th); + } + else { + if (Config.hideStubs && !$.cls('stickyIcon', th)[0]) { + th.style.display = th.nextElementSibling.style.display = 'none'; + } + else { + sa = $.id('sa' + tid); + sa.setAttribute('data-hidden', tid); + sa.firstChild.src = Main.icons.plus; + th.className += ' post-hidden'; + } + } + + this.hidden[tid] = Date.now(); +}; + +ThreadHiding.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-hide-t-' + Main.board)) { + this.hidden = JSON.parse(storage); + } +}; + +ThreadHiding.purge = function() { + var i, hasHidden, lastPurged, key; + + key = '4chan-purge-t-' + Main.board; + + lastPurged = localStorage.getItem(key); + + for (i in this.hidden) { + hasHidden = true; + break; + } + + if (!hasHidden) { + return; + } + + if (!lastPurged || lastPurged < Date.now() - this.threshold) { + $.get('//a.4cdn.org/' + Main.board + '/threads.json', + { + onload: function() { + var i, j, t, p, pages, threads, alive; + + if (this.status == 200) { + alive = {}; + pages = JSON.parse(this.responseText); + for (i = 0; p = pages[i]; ++i) { + threads = p.threads; + for (j = 0; t = threads[j]; ++j) { + if (ThreadHiding.hidden[t.no]) { + alive[t.no] = 1; + } + } + } + ThreadHiding.hidden = alive; + ThreadHiding.save(); + localStorage.setItem(key, Date.now()); + } + else { + console.log('Bad status code while purging threads'); + } + }, + onerror: function() { + console.log('Error while purging hidden threads'); + } + }); + } +}; + +ThreadHiding.save = function() { + for (var i in this.hidden) { + localStorage.setItem('4chan-hide-t-' + Main.board, + JSON.stringify(this.hidden) + ); + return; + } + localStorage.removeItem('4chan-hide-t-' + Main.board); +}; + +/** + * Reply hiding + */ +var ReplyHiding = {}; + +ReplyHiding.init = function() { + this.threshold = 7 * 86400000; + this.hidden = {}; + this.load(); +}; + +ReplyHiding.isHidden = function(pid) { + var sa = $.id('sa' + pid); + + return !sa || sa.hasAttribute('data-hidden'); +}; + +ReplyHiding.toggle = function(pid) { + if (this.isHidden(pid)) { + this.show(pid); + } + else { + this.hide(pid); + } + this.save(); +}; + +ReplyHiding.show = function(pid) { + var post, sa; + + post = $.id('pc' + pid); + + $.removeClass(post, 'post-hidden'); + + sa = $.id('sa' + pid); + sa.removeAttribute('data-hidden'); + sa.firstChild.src = Main.icons.minus; + + delete this.hidden[pid]; +}; + +ReplyHiding.hide = function(pid) { + var post, sa; + + post = $.id('pc' + pid); + post.className += ' post-hidden'; + + sa = $.id('sa' + pid); + sa.setAttribute('data-hidden', pid); + sa.firstChild.src = Main.icons.plus; + + this.hidden[pid] = Date.now(); +}; + +ReplyHiding.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-hide-r-' + Main.board)) { + this.hidden = JSON.parse(storage); + } +}; + +ReplyHiding.purge = function() { + var tid, now; + + now = Date.now(); + + for (tid in this.hidden) { + if (now - this.hidden[tid] > this.threshold) { + delete this.hidden[tid]; + } + } + this.save(); +}; + +ReplyHiding.save = function() { + for (var i in this.hidden) { + localStorage.setItem('4chan-hide-r-' + Main.board, + JSON.stringify(this.hidden) + ); + return; + } + localStorage.removeItem('4chan-hide-r-' + Main.board); +}; + +/** + * Thread watcher + */ +var ThreadWatcher = {}; + +ThreadWatcher.init = function() { + var cnt, jumpTo, rect, el; + + this.listNode = null; + this.charLimit = 45; + this.watched = {}; + this.isRefreshing = false; + + if (Main.hasMobileLayout) { + el = document.createElement('a'); + el.href = '#'; + el.textContent = 'TW'; + el.addEventListener('click', ThreadWatcher.toggleList, false); + cnt = $.id('settingsWindowLinkMobile'); + cnt.parentNode.insertBefore(el, cnt); + cnt.parentNode.insertBefore(document.createTextNode(' '), cnt); + } + + if (location.hash && (jumpTo = location.hash.split('lr')[1])) { + if (jumpTo = $.id('pc' + jumpTo)) { + if (jumpTo.nextElementSibling) { + jumpTo = jumpTo.nextElementSibling; + if (el = $.id('p' + jumpTo.id.slice(2))) { + $.addClass(el, 'highlight'); + } + } + + rect = jumpTo.getBoundingClientRect(); + + if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) { + window.scrollBy(0, rect.top); + } + } + + if (window.history && history.replaceState) { + history.replaceState(null, '', location.href.split('#', 1)[0]); + } + } + + cnt = document.createElement('div'); + cnt.id = 'threadWatcher'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-trackpos', 'TW-position'); + + if (Main.hasMobileLayout) { + cnt.style.display = 'none'; + } + else { + if (Config['TW-position']) { + cnt.style.cssText = Config['TW-position']; + } + else { + cnt.style.left = '10px'; + cnt.style.top = '380px'; + } + + if (Config.fixedThreadWatcher) { + cnt.style.position = 'fixed'; + } + else { + cnt.style.position = ''; + } + } + + cnt.innerHTML = '
    ' + + (Main.hasMobileLayout ? ('X') : '') + + 'Thread Watcher' + + (UA.hasCORS ? ('R
    ') : ''); + + this.listNode = document.createElement('ul'); + this.listNode.id = 'watchList'; + + this.load(); + + if (Main.tid) { + this.refreshCurrent(); + } + + this.build(); + + cnt.appendChild(this.listNode); + document.body.appendChild(cnt); + cnt.addEventListener('mouseup', this.onClick, false); + Draggable.set($.id('twHeader')); + window.addEventListener('storage', this.syncStorage, false); + + if (Main.hasMobileLayout) { + if (Main.tid) { + ThreadWatcher.initMobileButtons(); + } + } + else if (!Main.tid && this.canAutoRefresh()) { + this.refresh(); + } +}; + +ThreadWatcher.toggleList = function(e) { + var el = $.id('threadWatcher'); + + e && e.preventDefault(); + + if (!Main.tid && ThreadWatcher.canAutoRefresh()) { + ThreadWatcher.refresh(); + } + + if (el.style.display == 'none') { + el.style.top = (window.pageYOffset + 30) + 'px'; + el.style.display = ''; + } + else { + el.style.display = 'none'; + } +}; + +ThreadWatcher.syncStorage = function(e) { + var key; + + if (!e.key) { + return; + } + + key = e.key.split('-'); + + if (key[0] == '4chan' && key[1] == 'watch' && e.newValue != e.oldValue) { + ThreadWatcher.watched = JSON.parse(e.newValue); + ThreadWatcher.build(true); + } +}; + +ThreadWatcher.load = function() { + if (storage = localStorage.getItem('4chan-watch')) { + this.watched = JSON.parse(storage); + } +}; + +ThreadWatcher.build = function(rebuildButtons) { + var i, html, tuid, key, nodes, cls; + + html = ''; + + for (key in this.watched) { + tuid = key.split('-'); + html += '
  • × (' + this.watched[key][2] + ') '; + } + else { + html += (cls ? ('class="' + cls + '"') : '') + '>'; + } + } + + html += '/' + tuid[1] + '/ - ' + this.watched[key][0] + '
  • '; + } + + if (rebuildButtons) { + ThreadWatcher.rebuildButtons(); + } + + ThreadWatcher.listNode.innerHTML = html; +}; + +ThreadWatcher.rebuildButtons = function() { + var i, buttons, key; + + buttons = $.cls('wbtn'); + + for (i = 0; btn = buttons[i]; ++i) { + key = btn.getAttribute('data-id') + '-' + Main.board; + if (ThreadWatcher.watched[key]) { + if (!btn.hasAttribute('data-active')) { + btn.src = Main.icons.watched; + btn.setAttribute('data-active', '1'); + } + } + else { + if (btn.hasAttribute('data-active')) { + btn.src = Main.icons.notwatched; + btn.removeAttribute('data-active'); + } + } + } +}; + +ThreadWatcher.initMobileButtons = function() { + var el, cnt, key, ref; + + el = document.createElement('img'); + + key = Main.tid + '-' + Main.board; + + if (ThreadWatcher.watched[key]) { + el.src = Main.icons.watched; + el.setAttribute('data-active', '1'); + } + else { + el.src = Main.icons.notwatched; + } + + el.className = 'extButton wbtn wbtn-' + key; + el.setAttribute('data-cmd', 'watch'); + el.setAttribute('data-id', Main.tid); + el.alt = 'W'; + + cnt = document.createElement('span'); + cnt.className = 'mobileib button'; + + cnt.appendChild(el); + + if (ref = $.cls('navLinks')[0]) { + ref.appendChild(document.createTextNode(' ')); + ref.appendChild(cnt); + } + + if (ref = $.cls('navLinks')[3]) { + ref.appendChild(document.createTextNode(' ')); + ref.appendChild(cnt.cloneNode(true)); + } +}; + +ThreadWatcher.onClick = function(e) { + var t = e.target; + + if (t.hasAttribute('data-id')) { + ThreadWatcher.toggle( + t.getAttribute('data-id'), + t.getAttribute('data-board') + ); + } + else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) { + ThreadWatcher.refresh(); + } + else if (t.id == 'twClose') { + ThreadWatcher.toggleList(); + } +}; + +ThreadWatcher.toggle = function(tid, board, synced) { + var i, key, label, lastReply, thread; + + key = tid + '-' + (board || Main.board); + + if (this.watched[key]) { + delete this.watched[key]; + } + else { + if (label = $.cls('subject', $.id('pi' + tid))[0].textContent) { + label = label.slice(0, this.charLimit); + } + else if (label = $.id('m' + tid).innerHTML) { + label = label.replace(/(?:
    )+/g, ' ') + .replace(/<[^>]*?>/g, '').slice(0, this.charLimit); + } + else { + label = 'No.' + tid; + } + + if ((thread = $.id('t' + tid)).children[1]) { + lastReply = thread.lastElementChild.id.slice(2); + } + else { + lastReply = tid; + } + + this.watched[key] = [ label, lastReply, 0 ]; + } + this.save(); + this.load(); + this.build(true); +}; + +ThreadWatcher.save = function() { + ThreadWatcher.sortByBoard(); + + localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched)); +}; + +ThreadWatcher.sortByBoard = function() { + var i, self, key, sorted, keys; + + self = ThreadWatcher; + + sorted = {}; + keys = []; + + for (key in self.watched) { + keys.push(key); + } + + keys.sort(function(a, b) { + a = a.split('-')[1]; + b = b.split('-')[1]; + + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }); + + for (i = 0; key = keys[i]; ++i) { + sorted[key] = self.watched[key]; + } + + self.watched = sorted; +}; + +ThreadWatcher.canAutoRefresh = function() { + var time; + + if (time = localStorage.getItem('4chan-tw-timestamp')) { + return Date.now() - (+time) >= 60000; + } + return false; +}; + +ThreadWatcher.setRefreshTimestamp = function() { + localStorage.setItem('4chan-tw-timestamp', Date.now()); +}; + +ThreadWatcher.refresh = function() { + var i, to, key, total, img; + + if (total = $.id('watchList').children.length) { + i = to = 0; + img = $.id('twPrune'); + img.src = Main.icons.rotate; + ThreadWatcher.isRefreshing = true; + ThreadWatcher.setRefreshTimestamp(); + for (key in ThreadWatcher.watched) { + setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null); + to += 200; + } + } +}; + +ThreadWatcher.refreshCurrent = function(rebuild) { + var key, thread, lastReply; + + key = Main.tid + '-' + Main.board; + + if (this.watched[key]) { + if ((thread = $.id('t' + Main.tid)).children[1]) { + lastReply = thread.lastElementChild.id.slice(2); + } + else { + lastReply = Main.tid; + } + if (this.watched[key][1] < lastReply) { + this.watched[key][1] = lastReply; + } + + this.watched[key][2] = 0; + this.save(); + + if (rebuild) { + this.build(); + } + } +}; + +ThreadWatcher.setLastRead = function(pid, tid) { + var key = tid + '-' + Main.board; + + if (this.watched[key]) { + this.watched[key][1] = pid; + this.watched[key][2] = 0; + this.save(); + this.build(); + } +}; + +ThreadWatcher.onRefreshEnd = function(img) { + img.src = Main.icons.refresh; + this.isRefreshing = false; + this.save(); + this.load(); + this.build(); +}; + +ThreadWatcher.fetch = function(key, img) { + var tuid, xhr, li, method; + + li = $.id('watch-' + key); + + if (ThreadWatcher.watched[key][1] == -1) { + delete ThreadWatcher.watched[key]; + li.parentNode.removeChild(li); + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + return; + } + + tuid = key.split('-'); // tid, board + + xhr = new XMLHttpRequest(); + xhr.onload = function() { + var i, newReplies, posts, lastReply; + if (this.status == 200) { + posts = Parser.parseThreadJSON(this.responseText); + lastReply = ThreadWatcher.watched[key][1]; + newReplies = 0; + for (i = posts.length - 1; i >= 1; i--) { + if (posts[i].no <= lastReply) { + break; + } + ++newReplies; + } + if (newReplies > ThreadWatcher.watched[key][2]) { + ThreadWatcher.watched[key][2] = newReplies; + } + if (posts[0].archived) { + ThreadWatcher.watched[key][3] = 1; + } + } + else if (this.status == 404) { + ThreadWatcher.watched[key][1] = -1; + } + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + }; + if (img) { + xhr.onerror = xhr.onload; + } + xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json'); + xhr.send(null); +}; + +/** + * Thread expansion + */ +var ThreadExpansion = {}; + +ThreadExpansion.init = function() { + this.enabled = UA.hasCORS; +}; + +ThreadExpansion.expandComment = function(link) { + var ids, tid, pid, abbr; + + if (!(ids = link.getAttribute('href').match(/^(?:thread\/)([0-9]+)#p([0-9]+)$/))) { + return; + } + + tid = ids[1]; + pid = ids[2]; + + abbr = link.parentNode; + abbr.textContent = 'Loading...'; + + $.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json', + { + onload: function() { + var i, msg, com, post, posts; + + if (this.status == 200) { + msg = $.id('m' + pid); + + posts = Parser.parseThreadJSON(this.responseText); + + if (tid == pid) { + post = posts[0]; + } + else { + for (i = posts.length - 1; i > 0; i--) { + if (posts[i].no == pid) { + post = posts[i]; + break; + } + } + } + + if (post) { + post = Parser.buildHTMLFromJSON(post, Main.board); + + msg.innerHTML = $.cls('postMessage', post)[0].innerHTML; + + if (Parser.prettify) { + Parser.parseMarkup(msg); + } + if (window.jsMath) { + Parser.parseMathOne(msg); + } + } + else { + abbr.textContent = "This post doesn't exist anymore."; + } + } + else if (this.status == 404) { + abbr.textContent = "This thread doesn't exist anymore."; + } + else { + abbr.textContent = 'Connection Error'; + console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText); + } + }, + onerror: function() { + abbr.textContent = 'Connection Error'; + console.log('ThreadExpansion: xhr failed'); + } + } + ); +}; + +ThreadExpansion.toggle = function(tid) { + var thread, msg, expmsg, summary, tmp; + + thread = $.id('t' + tid); + summary = thread.children[1]; + if (thread.hasAttribute('data-truncated')) { + msg = $.id('m' + tid); + expmsg = msg.nextSibling; + } + + if ($.hasClass(thread, 'tExpanded')) { + thread.className = thread.className.replace(' tExpanded', ' tCollapsed'); + summary.children[0].src = Main.icons.plus; + summary.children[1].style.display = 'inline'; + summary.children[2].style.display = 'none'; + if (msg) { + tmp = msg.innerHTML; + msg.innerHTML = expmsg.textContent; + expmsg.textContent = tmp; + } + } + else if ($.hasClass(thread, 'tCollapsed')) { + thread.className = thread.className.replace(' tCollapsed', ' tExpanded'); + summary.children[0].src = Main.icons.minus; + summary.children[1].style.display = 'none'; + summary.children[2].style.display = 'inline'; + if (msg) { + tmp = msg.innerHTML; + msg.innerHTML = expmsg.textContent; + expmsg.textContent = tmp; + } + } + else { + summary.children[0].src = Main.icons.rotate; + ThreadExpansion.fetch(tid); + } +}; + +ThreadExpansion.fetch = function(tid) { + $.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json', + { + onload: function() { + var i, p, n, frag, thread, tail, posts, count, msg, metacap, + expmsg, summary, abbr; + + thread = $.id('t' + tid); + summary = thread.children[1]; + + if (this.status == 200) { + tail = $.cls('reply', thread); + + posts = Parser.parseThreadJSON(this.responseText); + + if (!Config.revealSpoilers && posts[0].custom_spoiler) { + Parser.setCustomSpoiler(Main.board, posts[0].custom_spoiler); + } + + frag = document.createDocumentFragment(); + + if (tail[0]) { + tail = +tail[0].id.slice(1); + + for (i = 1; p = posts[i]; ++i) { + if (p.no < tail) { + n = Parser.buildHTMLFromJSON(p, Main.board); + n.className += ' rExpanded'; + frag.appendChild(n); + } + else { + break; + } + } + } + else { + for (i = 1; p = posts[i]; ++i) { + n = Parser.buildHTMLFromJSON(p, Main.board); + n.className += ' rExpanded'; + frag.appendChild(n); + } + } + + msg = $.id('m' + tid); + if ((abbr = $.cls('abbr', msg)[0]) + && /^Comment/.test(abbr.textContent)) { + thread.setAttribute('data-truncated', '1'); + expmsg = document.createElement('div'); + expmsg.style.display = 'none'; + expmsg.textContent = msg.innerHTML; + msg.parentNode.insertBefore(expmsg, msg.nextSibling); + if (metacap = $.cls('capcodeReplies', msg)[0]) { + msg.innerHTML = posts[0].com + '

    '; + msg.appendChild(metacap); + } + else { + msg.innerHTML = posts[0].com; + } + if (Parser.prettify) { + Parser.parseMarkup(msg); + } + if (window.jsMath) { + Parser.parseMathOne(msg); + } + } + + thread.insertBefore(frag, summary.nextSibling); + Parser.parseThread(tid, 1, i - 1); + + thread.className += ' tExpanded'; + summary.children[0].src = Main.icons.minus; + summary.children[1].style.display = 'none'; + summary.children[2].style.display = 'inline'; + } + else if (this.status == 404) { + summary.children[0].src = Main.icons.plus; + summary.children[0].display = 'none'; + summary.children[1].textContent = "This thread doesn't exist anymore."; + } + else { + summary.children[0].src = Main.icons.plus; + console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText); + } + }, + onerror: function() { + $.id('t' + tid).children[1].children[0].src = Main.icons.plus; + console.log('ThreadExpansion: xhr failed'); + } + } + ); +}; + +/** + * Thread updater + */ +var ThreadUpdater = {}; + +ThreadUpdater.init = function() { + if (!UA.hasCORS) { + return; + } + + this.enabled = true; + + this.pageTitle = document.title; + + this.unreadCount = 0; + this.auto = this.hadAuto = false; + + this.delayId = 0; + this.delayIdHidden = 4; + this.delayRange = [ 10, 15, 20, 30, 60, 90, 120, 180, 240, 300 ]; + this.timeLeft = 0; + this.interval = null; + + this.lastModified = '0'; + this.lastReply = null; + + this.currentIcon = null; + this.iconPath = '//s.4cdn.org/image/'; + this.iconNode = document.head.querySelector('link[rel="shortcut icon"]'); + this.iconNode.type = 'image/x-icon'; + this.defaultIcon = this.iconNode.getAttribute('href').replace(this.iconPath, ''); + + this.deletionQueue = {}; + + if (Config.updaterSound) { + this.audioEnabled = false; + this.audio = document.createElement('audio'); + this.audio.src = '//s.4cdn.org/media/beep.ogg'; + } + + this.hidden = 'hidden'; + this.visibilitychange = 'visibilitychange'; + + this.adRefreshDelay = 1000; + this.adDebounce = 0; + this.ads = {}; + + if (typeof document.hidden === 'undefined') { + if ('mozHidden' in document) { + this.hidden = 'mozHidden'; + this.visibilitychange = 'mozvisibilitychange'; + } + else if ('webkitHidden' in document) { + this.hidden = 'webkitHidden'; + this.visibilitychange = 'webkitvisibilitychange'; + } + else if ('msHidden' in document) { + this.hidden = 'msHidden'; + this.visibilitychange = 'msvisibilitychange'; + } + } + + this.initAds(); + this.initControls(); + + document.addEventListener('scroll', this.onScroll, false); + + if (Config.alwaysAutoUpdate || sessionStorage.getItem('4chan-auto-' + Main.tid)) { + this.start(); + } +}; + +ThreadUpdater.buildMobileControl = function(el, bottom) { + var wrap, cnt, ctrl, cb, label, oldBtn, btn; + + bottom = (bottom ? 'Bot' : ''); + + wrap = document.createElement('div'); + wrap.className = 'btn-row'; + + // Update button + oldBtn = el.parentNode; + + btn = oldBtn.cloneNode(true); + btn.textContent = 'Update'; + btn.setAttribute('data-cmd', 'update'); + + wrap.appendChild(btn); + cnt = el.parentNode.parentNode; + ctrl = document.createElement('span'); + ctrl.className = 'mobileib button'; + + // Auto checkbox + label = document.createElement('label'); + cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.setAttribute('data-cmd', 'auto'); + this['autoNode' + bottom] = cb; + label.appendChild(cb); + label.appendChild(document.createTextNode('Auto')); + ctrl.appendChild(label); + wrap.appendChild(document.createTextNode(' ')); + wrap.appendChild(ctrl); + + // Status label + label = document.createElement('div'); + label.className = 'mobile-tu-status'; + + wrap.appendChild(this['statusNode' + bottom] = label); + + cnt.appendChild(wrap); + + // Remove Update button + oldBtn.parentNode.removeChild(oldBtn); + + $.id('mpostform').parentNode.style.marginTop = ''; +}; + +ThreadUpdater.buildDesktopControl = function(bottom) { + var frag, el, label, navlinks; + + bottom = (bottom ? 'Bot' : ''); + + frag = document.createDocumentFragment(); + + // Update button + frag.appendChild(document.createTextNode(' [')); + el = document.createElement('a'); + el.href = ''; + el.textContent = 'Update'; + el.setAttribute('data-cmd', 'update'); + frag.appendChild(el); + frag.appendChild(document.createTextNode(']')); + + // Auto checkbox + frag.appendChild(document.createTextNode(' [')); + label = document.createElement('label'); + el = document.createElement('input'); + el.type = 'checkbox'; + el.title = 'Fetch new replies automatically'; + el.setAttribute('data-cmd', 'auto'); + this['autoNode' + bottom] = el; + label.appendChild(el); + label.appendChild(document.createTextNode('Auto')); + frag.appendChild(label); + frag.appendChild(document.createTextNode('] ')); + + if (Config.updaterSound) { + // Sound checkbox + frag.appendChild(document.createTextNode(' [')); + label = document.createElement('label'); + el = document.createElement('input'); + el.type = 'checkbox'; + el.title = 'Play a sound on new replies to your posts'; + el.setAttribute('data-cmd', 'sound'); + this['soundNode' + bottom] = el; + label.appendChild(el); + label.appendChild(document.createTextNode('Sound')); + frag.appendChild(label); + frag.appendChild(document.createTextNode('] ')); + } + + // Status label + frag.appendChild( + this['statusNode' + bottom] = document.createElement('span') + ); + + if (bottom) { + navlinks = $.cls('navLinks' + bottom)[0]; + } + else { + navlinks = $.cls('navLinks')[1]; + } + + if (navlinks) { + navlinks.appendChild(frag); + } +}; + +ThreadUpdater.initControls = function() { + var i, j, frag, el, label, navlinks; + + // Mobile + if (Main.hasMobileLayout) { + this.buildMobileControl($.id('refresh_top')); + this.buildMobileControl($.id('refresh_bottom'), true); + } + // Desktop + else { + this.buildDesktopControl(); + this.buildDesktopControl(true); + } +}; + +ThreadUpdater.start = function() { + this.auto = this.hadAuto = true; + this.autoNode.checked = this.autoNodeBot.checked = true; + this.force = this.updating = false; + this.lastUpdated = Date.now(); + if (this.hidden) { + document.addEventListener(this.visibilitychange, + this.onVisibilityChange, false); + } + this.delayId = 0; + this.timeLeft = this.delayRange[0]; + this.pulse(); + sessionStorage.setItem('4chan-auto-' + Main.tid, 1); +}; + +ThreadUpdater.stop = function(manual) { + clearTimeout(this.interval); + this.auto = this.updating = this.force = false; + this.autoNode.checked = this.autoNodeBot.checked = false; + if (this.hidden) { + document.removeEventListener(this.visibilitychange, + this.onVisibilityChange, false); + } + if (manual) { + this.setStatus(''); + this.setIcon(null); + } + sessionStorage.removeItem('4chan-auto-' + Main.tid); +}; + +ThreadUpdater.pulse = function() { + var self = ThreadUpdater; + + if (self.timeLeft == 0) { + self.update(); + } + else { + self.setStatus(self.timeLeft--); + self.interval = setTimeout(self.pulse, 1000); + } +}; + +ThreadUpdater.adjustDelay = function(postCount) +{ + if (postCount == 0) { + if (!this.force) { + if (this.delayId < this.delayRange.length - 1) { + ++this.delayId; + } + } + } + else { + this.delayId = document[this.hidden] ? this.delayIdHidden : 0; + } + this.timeLeft = this.delayRange[this.delayId]; + if (this.auto) { + this.pulse(); + } +}; + +ThreadUpdater.onVisibilityChange = function(e) { + var self = ThreadUpdater; + + if (document[self.hidden] && self.delayId < self.delayIdHidden) { + self.delayId = self.delayIdHidden; + } + else { + self.delayId = 0; + self.refreshAds(); + } + + self.timeLeft = self.delayRange[0]; + self.lastUpdated = Date.now(); + clearTimeout(self.interval); + self.pulse(); +}; + +ThreadUpdater.onScroll = function(e) { + if (ThreadUpdater.hadAuto && + (document.documentElement.scrollHeight + <= (window.innerHeight + window.pageYOffset) + && !document[ThreadUpdater.hidden])) { + ThreadUpdater.clearUnread(); + } + + ThreadUpdater.refreshAds(); +}; + +ThreadUpdater.clearUnread = function() { + if (!this.dead) { + this.setIcon(null); + } + if (this.lastReply) { + this.unreadCount = 0; + document.title = this.pageTitle; + $.removeClass(this.lastReply, 'newPostsMarker'); + this.lastReply = null; + } +}; + +ThreadUpdater.forceUpdate = function() { + ThreadUpdater.force = true; + ThreadUpdater.update(); +}; + +ThreadUpdater.toggleAuto = function() { + if (this.updating) { + return; + } + this.auto ? this.stop(true) : this.start(); +}; + +ThreadUpdater.toggleSound = function() { + this.soundNode.checked = this.soundNodeBot.checked = + this.audioEnabled = !this.audioEnabled; +}; + +ThreadUpdater.update = function() { + var self, now = Date.now(); + + self = ThreadUpdater; + + if (self.updating) { + return; + } + + clearTimeout(self.interval); + + self.updating = true; + + self.setStatus('Updating...'); + + $.get('//a.4cdn.org/' + Main.board + '/thread/' + Main.tid + '.json', + { + onload: self.onload, + onerror: self.onerror + }, + { + 'If-Modified-Since': self.lastModified + } + ); +}; + +ThreadUpdater.initAds = function() { + var i, id, adIds = [ '_top_ad', '_middle_ad', '_bottom_ad' ]; + + for (i = 0; id = adIds[i]; ++i) { + ThreadUpdater.ads[id] = { + time: 0, + seenOnce: false, + isStale: false + }; + } +}; + +ThreadUpdater.invalidateAds = function() { + var id, self = ThreadUpdater; + + for (id in self.ads) { + meta = self.ads[id]; + if (meta.seenOnce) { + meta.isStale = true; + } + } +}; + +ThreadUpdater.refreshAds = function() { + var self, now, el, id, ad, meta, hidden, docHeight, offset; + + self = ThreadUpdater; + + now = Date.now(); + + if (now - self.adDebounce < 100) { + return; + } + + self.adDebounce = now; + + hidden = document[self.hidden]; + docHeight = document.documentElement.clientHeight; + + for (id in self.ads) { + meta = self.ads[id]; + + if (hidden) { + continue; + } + + ad = window[id]; + + if (!ad) { + continue; + } + + el = $.id(ad.D); + + if (!el) { + continue; + } + + offset = el.getBoundingClientRect(); + + if (offset.top < 0 || offset.bottom > docHeight) { + continue; + } + + meta.seenOnce = true; + + if (!meta.isStale || now - meta.time < self.adRefreshDelay) { + continue; + } + + meta.time = now; + meta.isStale = false; + + ados_refresh(ad, 0, false); + } +}; + +ThreadUpdater.markDeletedReplies = function(newposts) { + var i, j, posthash, oldposts, el; + + posthash = {}; + for (i = 0; j = newposts[i]; ++i) { + posthash['pc' + j.no] = 1; + } + + oldposts = $.cls('replyContainer'); + for (i = 0; j = oldposts[i]; ++i) { + if (!posthash[j.id] && !$.hasClass(j, 'deleted')) { + if (this.deletionQueue[j.id]) { + el = document.createElement('img'); + el.src = Main.icons2.trash; + el.className = 'trashIcon'; + el.title = 'This post has been deleted'; + $.addClass(j, 'deleted'); + $.cls('postNum', j)[1].appendChild(el); + delete this.deletionQueue[j.id]; + } + else { + this.deletionQueue[j.id] = 1; + } + } + } +}; + +ThreadUpdater.onload = function() { + var i, el, state, self, nodes, thread, newposts, frag, lastrep, lastid, + spoiler, op, doc, autoscroll, count, fromQR, lastRepPos; + + self = ThreadUpdater; + nodes = []; + + self.setStatus(''); + + if (this.status == 200) { + self.lastModified = this.getResponseHeader('Last-Modified'); + + thread = $.id('t' + Main.tid); + + lastrep = thread.children[thread.childElementCount - 1]; + lastid = +lastrep.id.slice(2); + + newposts = Parser.parseThreadJSON(this.responseText); + + state = !!newposts[0].archived; + if (window.thread_archived !== undefined && state != window.thread_archived) { + QR.enabled && $.id('quickReply') && QR.lock(); + Main.setThreadState('archived', state); + } + + state = !!newposts[0].closed; + if (state != Main.threadClosed) { + if (newposts[0].archived) { + state = false; + } + else if (QR.enabled && $.id('quickReply')) { + if (state) { + QR.lock(); + } + else { + QR.unlock(); + } + } + Main.setThreadState('closed', state); + } + + state = !!newposts[0].sticky; + if (state != Main.threadSticky) { + Main.setThreadState('sticky', state); + } + + state = !!newposts[0].imagelimit; + if (QR.enabled && state != QR.fileDisabled) { + QR.fileDisabled = state; + } + + if (!Config.revealSpoilers && newposts[0].custom_spoiler) { + Parser.setCustomSpoiler(Main.board, newposts[0].custom_spoiler); + } + + for (i = newposts.length - 1; i >= 0; i--) { + if (newposts[i].no <= lastid) { + break; + } + nodes.push(newposts[i]); + } + + count = nodes.length; + + if (count == 1 && QR.lastReplyId == nodes[0].no) { + fromQR = true; + QR.lastReplyId = null; + } + + if (!fromQR) { + self.markDeletedReplies(newposts); + } + + if (count) { + doc = document.documentElement; + + autoscroll = ( + Config.autoScroll + && document[self.hidden] + && doc.scrollHeight == (window.innerHeight + window.pageYOffset) + ); + + frag = document.createDocumentFragment(); + for (i = nodes.length - 1; i >= 0; i--) { + frag.appendChild(Parser.buildHTMLFromJSON(nodes[i], Main.board)); + } + thread.appendChild(frag); + + lastRepPos = lastrep.offsetTop; + + Parser.hasYouMarkers = false; + Parser.hasHighlightedPosts = false; + Parser.parseThread(thread.id.slice(1), -nodes.length); + + if (lastRepPos != lastrep.offsetTop) { + window.scrollBy(0, lastrep.offsetTop - lastRepPos); + } + + if (!fromQR) { + if (!self.force && doc.scrollHeight > window.innerHeight) { + if (!self.lastReply && lastid != Main.tid) { + (self.lastReply = lastrep.lastChild).className += ' newPostsMarker'; + } + if (Parser.hasYouMarkers) { + self.setIcon('rep'); + if (self.audioEnabled && document[self.hidden]) { + self.audio.play(); + } + } + else if (Parser.hasHighlightedPosts && self.currentIcon !== 'rep') { + self.setIcon('hl'); + } + else if (self.unreadCount == 0) { + self.setIcon('new'); + } + self.unreadCount += count; + document.title = '(' + self.unreadCount + ') ' + self.pageTitle; + } + else { + self.setStatus(count + ' new post' + (count > 1 ? 's' : '')); + } + } + + if (autoscroll) { + window.scrollTo(0, document.documentElement.scrollHeight); + } + + if (Config.threadWatcher) { + ThreadWatcher.refreshCurrent(true); + } + + if (Config.threadStats) { + op = newposts[0]; + ThreadStats.update(op.replies, op.images, op.bumplimit, op.imagelimit); + } + + self.invalidateAds(); + self.refreshAds(); + + UA.dispatchEvent('4chanThreadUpdated', { count: count }); + } + else { + self.setStatus('No new posts'); + } + + if (newposts[0].archived) { + self.setError('This thread is archived'); + if (!self.dead) { + self.setIcon('dead'); + window.thread_archived = true; + self.dead = true; + self.stop(); + } + } + } + else if (this.status == 304 || this.status == 0) { + self.setStatus('No new posts'); + } + else if (this.status == 404) { + self.setIcon('dead'); + self.setError('This thread has been pruned or deleted'); + self.dead = true; + self.stop(); + return; + } + + self.lastUpdated = Date.now(); + self.adjustDelay(nodes.length); + self.updating = self.force = false; +}; + +ThreadUpdater.onerror = function() { + var self = ThreadUpdater; + + if (UA.isOpera && !this.statusText && this.status == 0) { + self.setStatus('No new posts'); + } + else { + self.setError('Connection Error'); + } + + self.lastUpdated = Date.now(); + self.adjustDelay(0); + self.updating = self.force = false; +}; + +ThreadUpdater.setStatus = function(msg) { + this.statusNode.textContent = this.statusNodeBot.textContent = msg; +}; + +ThreadUpdater.setError = function(msg) { + this.statusNode.innerHTML + = this.statusNodeBot.innerHTML + = '' + msg + ''; +}; + +ThreadUpdater.setIcon = function(type) { + var icon; + + if (type === null) { + icon = this.defaultIcon; + } + else { + icon = this.icons[Main.type + type]; + } + + this.currentIcon = type; + this.iconNode.href = this.iconPath + icon; + document.head.appendChild(this.iconNode); +}; + +ThreadUpdater.icons = { + wsnew: 'favicon-ws-newposts.ico', + nwsnew: 'favicon-nws-newposts.ico', + wsrep: 'favicon-ws-newreplies.ico', + nwsrep: 'favicon-nws-newreplies.ico', + wsdead: 'favicon-ws-deadthread.ico', + nwsdead: 'favicon-nws-deadthread.ico', + wshl: 'favicon-ws-newfilters.ico', + nwshl: 'favicon-nws-newfilters.ico' +}; + +/** + * Thread stats + */ +var ThreadStats = {}; + +ThreadStats.init = function() { + var i, cnt; + + this.nodeTop = document.createElement('div'); + this.nodeTop.className = 'thread-stats'; + this.nodeBot = this.nodeTop.cloneNode(false); + + cnt = $.cls('navLinks'); + cnt[1] && cnt[1].appendChild(this.nodeTop); + cnt[2] && cnt[2].appendChild(this.nodeBot); + + this.pageNumber = null; + this.update(null, null, window.bumplimit, window.imagelimit); + + if (!window.thread_archived) { + this.updatePageNumber(); + this.pageInterval = setInterval(this.updatePageNumber, 3 * 60000); + } +}; + +ThreadStats.update = function(replies, images, isBumpFull, isImageFull) { + var stats, repStr, imgStr, pageStr, stateStr; + + if (replies === null) { + replies = $.cls('replyContainer').length; + images = $.cls('fileText').length - ($.id('fT' + Main.tid) ? 1 : 0); + } + + stats = []; + + if (Main.threadSticky) { + stats.push('Sticky'); + } + + if (window.thread_archived) { + stats.push('Archived'); + } + else if (Main.threadClosed) { + stats.push('Closed'); + } + + if (isBumpFull) { + stats.push('' + replies + ''); + } + else { + stats.push('' + replies + ''); + } + + if (isImageFull) { + stats.push('' + images + ''); + } + else { + stats.push('' + images + ''); + } + + if (!window.thread_archived) { + stats.push('' + (this.pageNumber || '?') + ''); + } + + this.nodeTop.innerHTML = this.nodeBot.innerHTML + = stats.join(' / '); +}; + +ThreadStats.updatePageNumber = function() { + $.get('//a.4cdn.org/' + Main.board + '/threads.json', + { + onload: ThreadStats.onCatalogLoad, + onerror: ThreadStats.onCatalogError + } + ); +}; + +ThreadStats.onCatalogLoad = function() { + var self, i, j, page, post, threads, catalog, tid, nodes; + + self = ThreadStats; + + if (this.status == 200) { + tid = +Main.tid; + catalog = JSON.parse(this.responseText); + for (i = 0; page = catalog[i]; ++i) { + threads = page.threads; + for (j = 0; post = threads[j]; ++j) { + if (post.no == tid) { + nodes = $.cls('ts-page'); + nodes[0].textContent = nodes[1].textContent = page.page + self.pageNumber = page.page; + return; + } + } + } + clearInterval(self.pageInterval); + } + else { + ThreadStats.onCatalogError(); + } +}; + +ThreadStats.onCatalogError = function() { + console.log('ThreadStats: couldn\'t get the catalog (' + this.status + ')'); +}; + +/** + * Filter + */ +var Filter = {}; + +Filter.init = function() { + this.entities = document.createElement('div'); + Filter.load(); +}; + +Filter.onClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'filters-add': + Filter.add(); + break; + case 'filters-save': + Filter.save(); + Filter.close(); + break; + case 'filters-close': + Filter.close(); + break; + case 'filters-palette': + Filter.openPalette(e.target); + break; + case 'filters-palette-close': + Filter.closePalette(); + break; + case 'filters-palette-clear': + Filter.clearPalette(); + break; + case 'filters-up': + Filter.moveUp(e.target.parentNode.parentNode); + break; + case 'filters-del': + Filter.remove(e.target.parentNode.parentNode); + break; + case 'filters-help-open': + Filter.openHelp(); + break; + case 'filters-help-close': + Filter.closeHelp(); + break; + } + } +}; + +Filter.onPaletteClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'palette-pick': + Filter.pickColor(e.target); + break; + case 'palette-clear': + Filter.pickColor(e.target, true); + break; + case 'palette-close': + Filter.closePalette(); + break; + } + } +}; + +Filter.exec = function(cnt, pi, msg, tid) { + var trip, name, com, uid, sub, fname, f, filters, hit, currentBoard; + + if (Parser.trackedReplies && Parser.trackedReplies['>>' + pi.id.slice(2)]) { + return false; + } + + currentBoard = Main.board; + filters = Filter.activeFilters; + hit = false; + + for (i = 0; f = filters[i]; ++i) { + // boards + if (f.boards && !f.boards[currentBoard]) { + continue; + } + // tripcode + if (f.type == 0) { + if ((trip !== undefined || (trip = pi.getElementsByClassName('postertrip')[0]) + ) && f.pattern == trip.textContent) { + hit = true; + break; + } + } + // name + else if (f.type == 1) { + if ((name || (name = pi.getElementsByClassName('name')[0])) + && f.pattern == name.textContent) { + hit = true; + break; + } + } + // comment + else if (f.type == 2) { + if (com === undefined) { + this.entities.innerHTML + = msg.innerHTML.replace(/
    /g, '\n').replace(/[<[^>]+>/g, ''); + com = this.entities.textContent; + } + if (f.pattern.test(com)) { + hit = true; + break; + } + } + // user id + else if (f.type == 4) { + if ((uid || + ((uid = pi.getElementsByClassName('posteruid')[0]) + && (uid = uid.firstElementChild.textContent) + ) + ) && f.pattern == uid) { + hit = true; + break; + } + } + // subject + else if (!Main.tid && f.type == 5) { + if ((sub || + ((sub = pi.getElementsByClassName('subject')[0]) + && (sub = sub.textContent) + ) + ) && f.pattern.test(sub)) { + hit = true; + break; + } + } + // filename + else if (f.type == 6) { + if (fname === undefined) { + if ((fname = pi.parentNode.getElementsByClassName('fileText')[0])) { + fname = fname.firstElementChild.textContent; + } + else { + fname = ''; + } + } + if (f.pattern.test(fname)) { + hit = true; + break; + } + } + } + + if (hit) { + if (f.hide) { + cnt.className += ' post-hidden'; + el = document.createElement('span'); + if (!tid) { + el.textContent = '[View]'; + el.setAttribute('data-filtered', '1'); + } + else { + el.innerHTML = '[View]'; + } + el.className = 'filter-preview'; + pi.appendChild(el); + return true; + } + else { + cnt.className += ' filter-hl'; + cnt.style.boxShadow = '-3px 0 ' + f.color; + Parser.hasHighlightedPosts = true; + } + } + return false; +}; + +Filter.load = function() { + var i, j, w, f, rawFilters, rawPattern, fid, regexEscape, regexType, + wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard; + + this.activeFilters = []; + + if (!(rawFilters = localStorage.getItem('4chan-filters'))) { + return; + } + + rawFilters = JSON.parse(rawFilters); + + regexEscape = new RegExp('(\\' + + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\') + + ')', 'g'); + regexType = /^\/(.*)\/(i?)$/; + wordSepS = '(?=.*\\b'; + wordSepE = '\\b)'; + regexWildcard = /\\\*/g; + replaceWildcard = '[^\\s]*'; + + try { + for (fid = 0; f = rawFilters[fid]; ++fid) { + if (f.active && f.pattern != '') { + // Boards + if (f.boards) { + tmp = f.boards.split(/[^a-z0-9]+/i); + boards = {}; + for (i = 0; j = tmp[i]; ++i) { + boards[j] = true; + } + } + else { + boards = false; + } + + rawPattern = f.pattern; + // Name, Tripcode or ID, string comparison + if (!f.type || f.type == 1 || f.type == 4) { + pattern = rawPattern; + } + // /RegExp/ + else if (match = rawPattern.match(regexType)) { + pattern = new RegExp(match[1], match[2]); + } + // "Exact match" + else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') { + pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); + } + // Full words, AND operator + else { + words = rawPattern.split(' '); + pattern = ''; + for (i = 0, j = words.length; i < j; ++i) { + inner = words[i] + .replace(regexEscape, '\\$1') + .replace(regexWildcard, replaceWildcard); + pattern += wordSepS + inner + wordSepE; + } + pattern = new RegExp('^' + pattern, 'im'); + } + //console.log('Resulting pattern: ' + pattern); + this.activeFilters.push({ + type: f.type, + pattern: pattern, + boards: boards, + color: f.color, + hide: f.hide + }); + } + } + } + catch (e) { + alert('There was an error processing one of the filters: ' + + e + ' in: ' + rawPattern); + } +}; + +Filter.addSelection = function() { + var text, type, node, sel = UA.getSelection(true); + + if (Filter.open() === false) { + return; + } + + if (typeof sel == 'string') { + text = sel.trim(); + } + else { + node = sel.anchorNode.parentNode; + text = sel.toString().trim(); + + if ($.hasClass(node, 'name')) { + type = 1; + } + else if ($.hasClass(node, 'postertrip')) { + type = 0; + } + else if ($.hasClass(node, 'subject')) { + type = 5; + } + else if ($.hasClass(node, 'posteruid') || $.hasClass(node, 'hand')) { + type = 4; + } + else if ($.hasClass(node, 'fileText')) { + type = 6; + } + else { + type = 2; + } + } + + Filter.add(text, type); +}; + +Filter.openHelp = function() { + var cnt; + + if ($.id('filtersHelp')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'filtersHelp'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'filters-help-close'); + cnt.innerHTML = '\ +
    Filters & Highlights Help\ +Close
    \ +

    Tripcode, Name and ID filters:

    \ +
    • Those use simple string comparison.
    • \ +
    • Type them exactly as they appear on 4chan, including the exclamation mark for tripcode filters.
    • \ +
    • Example: !Ep8pui8Vw2
    \ +

    Comment, Subject and E-mail filters:

    \ +
    • Matching whole words:
    • \ +
    • feel — will match "feel" but not "feeling". This search is case-insensitive.
    \ +
    • AND operator:
    • \ +
    • feel girlfriend — will match "feel" AND "girlfriend" in any order.
    \ +
    • Exact match:
    • \ +
    • "that feel when" — place double quotes around the pattern to search for an exact string
    \ +
    • Wildcards:
    • \ +
    • feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc…
    • \ +
    • idolm*ster — this can match "idolmaster" or "idolm@ster", etc…
    \ +
    • Regular expressions:
    • \ +
    • /feel when no (girl|boy)friend/i
    • \ +
    • /^(?!.*touhou).*$/i — NOT operator.
    • \ +
    • /^>/ — comments starting with a quote.
    • \ +
    • /^$/ — comments with no text.
    \ +

    Colors:

    \ +
    • The color field can accept any valid CSS color:
    • \ +
    • red, #0f0, #00ff00, rgba( 34, 12, 64, 0.3), etc…
    \ +

    Boards:

    \ +
    • A space separated list of boards on which the filter will be active. Leave blank to apply to all boards.
    \ +

    Shortcut:

    \ +
    • If you have Keyboard shortcuts enabled, pressing F will add the selected text to your filters.
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); +}; + +Filter.closeHelp = function() { + var cnt; + + if (cnt = $.id('filtersHelp')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Filter.open = function() { + var i, f, cnt, menu, html, rawFilters, filterId, filterList; + + if ($.id('filtersMenu')) { + return false; + } + + cnt = document.createElement('div'); + cnt.id = 'filtersMenu'; + cnt.className = 'UIPanel'; + cnt.style.display = 'none'; + cnt.setAttribute('data-cmd', 'filters-close'); + cnt.innerHTML = '\ +
    Filters & Highlights\ +HelpClose
    \ +\ +\ +\ +\ +\ +\ +\ +\ +\ +
    OrderOnPatternBoardsTypeColorHideDel
    \ +\ +\ +
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); + + filterList = $.id('filter-list'); + + if (rawFilters = localStorage.getItem('4chan-filters')) { + rawFilters = JSON.parse(rawFilters); + for (i = 0; f = rawFilters[i]; ++i) { + filterList.appendChild(this.buildEntry(f, i)); + } + } + + cnt.style.display = ''; +}; + +Filter.close = function() { + var cnt; + + if (cnt = $.id('filtersMenu')) { + this.closePalette(); + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Filter.moveUp = function(el) { + var prev; + + if (prev = el.previousElementSibling) { + el.parentNode.insertBefore(el, prev); + } +}; + +Filter.add = function(pattern, type, boards) { + var filter, id, el; + + filter = { + active: true, + type: type || 0, + pattern: pattern || '', + boards: boards || '', + color: '', + hide: false + }; + + id = this.getNextFilterId(); + el = this.buildEntry(filter, id); + + $.id('filter-list').appendChild(el); + $.cls('fPattern', el)[0].focus(); +}; + +Filter.remove = function(tr) { + $.id('filter-list').removeChild(tr); +}; + +Filter.save = function() { + var i, rawFilters, entries, tr, f, color, type; + + rawFilters = []; + entries = $.id('filter-list').children; + + for (i = 0; tr = entries[i]; ++i) { + type = tr.children[4].firstChild; + f = { + active: tr.children[1].firstChild.checked, + pattern: tr.children[2].firstChild.value, + boards: tr.children[3].firstChild.value, + type: +type.options[type.selectedIndex].value, + hide: tr.children[6].firstChild.checked + } + + color = tr.children[5].firstChild; + + if (!color.hasAttribute('data-nocolor')) { + f.color = color.style.backgroundColor; + } + + rawFilters.push(f); + } + + if (rawFilters[0]) { + localStorage.setItem('4chan-filters', JSON.stringify(rawFilters)); + } + else { + localStorage.removeItem('4chan-filters'); + } +}; + +Filter.getNextFilterId = function() { + var i, j, max, entries = $.id('filter-list').children; + + if (!entries.length) { + return 0; + } + else { + max = 0; + for (i = 0; j = entries[i]; ++i) { + j = +j.id.slice(7); + if (j > max) { + max = j; + } + } + return max + 1; + } +}; + +Filter.buildEntry = function(filter, id) { + var tr, html, sel; + + tr = document.createElement('tr'); + tr.id = 'filter-' + id; + + html = ''; + + // Move up + html += ''; + + // On + html += '' : '>'); + + // Pattern + html += ''; + + // Boards + html += ''; + + // FIXME + if (filter.type === 3) { + filter.type = 4; + } + + // Type + sel = [ '', '', '', '', '', '', '' ]; + sel[filter.type] = ' selected="selected"'; + + html += ''; + + // Color + html += ''; + } + html += ''; + + // Hide + html += '' : '>'); + + // Del + html += '×'; + + tr.innerHTML = html; + + return tr; +} + +Filter.buildPalette = function(id) { + var i, j, cnt, html, colors, rowCount, colCount; + + colors = [ + ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00'], + ['#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB'], + ['#00A550', '#007FFF', '#AF0A0F', '#B5BD68'] + ]; + + rowCount = colors.length; + colCount = colors[0].length; + + html = '
    '; + + for (i = 0; i < rowCount; ++i) { + html += '' + for (j = 0; j < colCount; ++j) { + html += ''; + } + html += '' + } + + html += '
    Custom\ +
    \ +
    \ +[Close]\ +[Clear]
    '; + + cnt = document.createElement('div'); + cnt.id = 'filter-palette'; + cnt.setAttribute('data-target', id); + cnt.setAttribute('data-cmd', 'palette-close'); + cnt.className = 'UIMenu'; + cnt.innerHTML = html; + + return cnt; +}; + +Filter.openPalette = function(target) { + var el, pos, id, picker; + + Filter.closePalette(); + + pos = target.getBoundingClientRect(); + id = target.parentNode.parentNode.id.slice(7); + + el = Filter.buildPalette(id); + document.body.appendChild(el); + + $.id('filter-palette').addEventListener('click', Filter.onPaletteClick, false); + $.id('palette-custom-input').addEventListener('keyup', Filter.setCustomColor, false); + + picker = el.firstElementChild; + picker.style.cssText = 'top:' + pos.top + 'px;left:' + + (pos.left - picker.clientWidth - 10) + 'px;'; +}; + +Filter.closePalette = function() { + var el; + + if (el = $.id('filter-palette')) { + $.id('filter-palette').removeEventListener('click', Filter.onPaletteClick, false); + $.id('palette-custom-input').removeEventListener('keyup', Filter.setCustomColor, false); + el.parentNode.removeChild(el); + } +}; + +Filter.pickColor = function(el, clear) { + var id, target; + + id = $.id('filter-palette').getAttribute('data-target'); + target = $.id('filter-' + id); + + if (!target) { + return; + } + + target = $.cls('colorbox', target)[0]; + + if (clear === true) { + target.setAttribute('data-nocolor', '1'); + target.innerHTML = '∕'; + target.style.background = ''; + } + else { + target.removeAttribute('data-nocolor'); + target.innerHTML = ''; + target.style.background = el.style.backgroundColor; + } + + Filter.closePalette(); +}; + +Filter.setCustomColor = function() { + var input, box; + + input = $.id('palette-custom-input'); + box = $.id('palette-custom-ok'); + + box.style.backgroundColor = input.value; +}; + +/** + * ID colors + */ +var IDColor = { + css: 'padding: 0 5px; border-radius: 6px; font-size: 0.8em;', + ids: {} +}; + +IDColor.init = function() { + var style; + + if (window.user_ids) { + this.enabled = true; + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = '.posteruid .hand {' + this.css + '}'; + document.head.appendChild(style); + } +}; + +IDColor.compute = function(str) { + var rgb, hash; + + rgb = []; + hash = $.hash(str); + + rgb[0] = (hash >> 24) & 0xFF; + rgb[1] = (hash >> 16) & 0xFF; + rgb[2] = (hash >> 8) & 0xFF; + rgb[3] = ((rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114)) > 125; + + this.ids[str] = rgb; + + return rgb; +}; + +IDColor.apply = function(uid) { + var rgb; + + rgb = IDColor.ids[uid.textContent] || IDColor.compute(uid.textContent); + uid.style.cssText = '\ + background-color: rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ');\ + color: ' + (rgb[3] ? 'black;' : 'white;'); +}; + +IDColor.applyRemote = function(uid) { + this.apply(uid); + uid.style.cssText += this.css; +}; + +/** + * SWF embed + */ +var SWFEmbed = {}; + +SWFEmbed.init = function() { + if (Main.tid) { + this.processThread(); + } + else { + this.processIndex(); + } +}; + +SWFEmbed.processThread = function() { + var fileText, el; + + fileText = $.id('fT' + Main.tid); + + if (!fileText) { + return; + } + + el = document.createElement('a'); + el.href = 'javascript:;'; + el.textContent = 'Embed'; + el.addEventListener('click', SWFEmbed.toggleThread, false); + + fileText.appendChild(document.createTextNode('-[')); + fileText.appendChild(el); + fileText.appendChild(document.createTextNode(']')); +}; + +SWFEmbed.processIndex = function() { + var i, tr, el, cnt, nodes, srcIndex, src; + + srcIndex = 2; + + cnt = $.cls('postblock')[0]; + + if (!cnt) { + return; + } + + tr = cnt.parentNode; + + el = document.createElement('td'); + el.className = 'postblock'; + tr.insertBefore(el, tr.children[srcIndex].nextElementSibling); + + cnt = $.cls('flashListing')[0]; + + if (!cnt) { + return; + } + + nodes = $.tag('tr', cnt); + + for (i = 1; tr = nodes[i]; ++i) { + src = tr.children[srcIndex].firstElementChild; + el = document.createElement('td'); + el.innerHTML = '[Embed]'; + el.firstElementChild.addEventListener('click', SWFEmbed.embedIndex, false); + tr.insertBefore(el, tr.children[srcIndex].nextElementSibling); + }; +}; + +SWFEmbed.toggleThread = function(e) { + var cnt, link, el, post, maxWidth, ratio, width, height; + + if (cnt = $.id('swf-embed')) { + cnt.parentNode.removeChild(cnt); + e.target.textContent = 'Embed'; + return; + } + + link = $.tag('a', e.target.parentNode)[0]; + + maxWidth = document.documentElement.clientWidth - 100; + + width = +link.getAttribute('data-width'); + height = +link.getAttribute('data-height'); + + if (width > maxWidth) { + ratio = width / height; + width = maxWidth; + height = Math.round(maxWidth / ratio); + } + + cnt = document.createElement('div'); + cnt.id = 'swf-embed'; + + el = document.createElement('embed'); + el.setAttribute('allowScriptAccess', 'never'); + el.type = 'application/x-shockwave-flash'; + el.width = width; + el.height = height; + el.src = link.href; + + cnt.appendChild(el); + + post = $.id('m' + Main.tid); + post.insertBefore(cnt, post.firstChild); + + $.cls('thread')[0].scrollIntoView(true); + + e.target.textContent = 'Remove'; +}; + +SWFEmbed.embedIndex = function(e) { + var el, cnt, header, icon, backdrop, width, height, cntWidth, cntHeight, + maxWidth, maxHeight, docWidth, docHeight, margins, headerHeight, fileName; + + e.preventDefault(); + + margins = 10; + headerHeight = 20; + + el = e.target.parentNode.parentNode.children[2].firstElementChild; + + fileName = el.getAttribute('title') || el.textContent; + + cntWidth = width = +el.getAttribute('data-width'); + cntHeight = height = +el.getAttribute('data-height'); + + docWidth = document.documentElement.clientWidth; + docHeight = document.documentElement.clientHeight; + + maxWidth = docWidth - margins; + maxHeight = docHeight - margins - headerHeight; + + ratio = width / height; + + if (cntWidth > maxWidth) { + cntWidth = maxWidth; + cntHeight = Math.round(maxWidth / ratio); + } + + if (cntHeight > maxHeight) { + cntHeight = maxHeight; + cntWidth = Math.round(maxHeight * ratio); + } + + el = document.createElement('embed'); + el.setAttribute('allowScriptAccess', 'never'); + el.src = e.target.href; + el.width = '100%'; + el.height = '100%'; + + cnt = document.createElement('div'); + cnt.style.position = 'fixed'; + cnt.style.width = cntWidth + 'px'; + cnt.style.height = cntHeight + 'px'; + cnt.style.top = '50%'; + cnt.style.left = '50%'; + cnt.style.marginTop = (-cntHeight / 2 - headerHeight / 2) + 'px'; + cnt.style.marginLeft = (-cntWidth / 2) + 'px'; + cnt.style.background = 'white'; + + header = document.createElement('div'); + header.id = 'swf-embed-header'; + header.className = 'postblock'; + header.textContent = fileName + ', ' + width + 'x' + height; + + icon = document.createElement('img'); + icon.id = 'swf-embed-close'; + icon.className = 'pointer'; + icon.src = Main.icons.cross; + + header.appendChild(icon); + + cnt.appendChild(header); + cnt.appendChild(el); + + backdrop = document.createElement('div'); + backdrop.id = 'swf-embed'; + backdrop.style.cssText = 'width: 100%; height: 100%; position: fixed;\ + top: 0; left: 0; background: rgba(128, 128, 128, 0.5)'; + + backdrop.appendChild(cnt); + backdrop.addEventListener('click', SWFEmbed.onBackdropClick, false); + + document.body.appendChild(backdrop); +}; + +SWFEmbed.onBackdropClick = function(e) { + var backdrop = $.id('swf-embed'); + + if (e.target === backdrop || e.target.id == 'swf-embed-close') { + backdrop.removeEventListener('click', SWFEmbed.onBackdropClick, false); + backdrop.parentNode.removeChild(backdrop); + } +}; + +/** + * Media + */ +var Media = {}; + +Media.init = function() { + this.matchSC = /(?:soundcloud\.com|snd\.sc)\/[^\s<]+(?:)?[^\s<]*/g; + this.matchYT = /(?:youtube\.com\/watch\?[^\s]*?v=|youtu\.be\/)[^\s<]+(?:)?[^\s<]*(?:)?[^\s<]*/g; + this.toggleYT = /(?:v=|\.be\/)([a-zA-Z0-9_-]{11})/; + this.timeYT = /#t=([ms0-9]+)/; + this.matchVocaroo = /vocaroo\.com\/i\/([a-z0-9]{12})/gi; + + this.map = { + yt: this.toggleYouTube, + sc: this.toggleSoundCloud, + vocaroo: this.toggleVocaroo + }; +}; + +Media.parseSoundCloud = function(msg) { + msg.innerHTML = msg.innerHTML.replace(this.matchSC, this.replaceSoundCloud); +}; + +Media.replaceSoundCloud = function(link) { + return '' + link + ' [Embed]'; +}; + +Media.toggleSoundCloud = function(node) { + var xhr, url; + + if (node.textContent == 'Remove') { + node.parentNode.removeChild(node.nextElementSibling); + node.textContent = 'Embed'; + } + else if (node.textContent == 'Embed') { + url = node.previousElementSibling.textContent; + + xhr = new XMLHttpRequest(); + xhr.open('GET', '//soundcloud.com/oembed?show_artwork=false&' + + 'maxwidth=500px&show_comments=false&format=json&url=' + + 'http://' + url); + xhr.onload = function() { + var el; + + if (this.status == 200 || this.status == 304) { + el = document.createElement('div'); + el.className = 'media-embed'; + el.innerHTML = JSON.parse(this.responseText).html; + node.parentNode.insertBefore(el, node.nextElementSibling); + node.textContent = 'Remove'; + } + else { + node.textContent = 'Error'; + console.log('SoundCloud Error (HTTP ' + this.status + ')'); + } + }; + node.textContent = 'Loading...'; + xhr.send(null); + } +}; + +Media.parseYouTube = function(msg) { + msg.innerHTML = msg.innerHTML.replace(this.matchYT, this.replaceYouTube); +}; + +Media.replaceYouTube = function(link) { + return '' + link + ' [Embed]'; +}; + +Media.showYTPreview = function(link) { + var cnt, img, vid, aabb, x, y, tw, th, pad; + + tw = 320; th = 180; pad = 5; + + aabb = link.getBoundingClientRect(); + + vid = link.previousElementSibling.textContent.match(this.toggleYT)[1]; + + if (aabb.right + tw + pad > $.docEl.clientWidth) { + x = aabb.left - tw - pad; + } + else { + x = aabb.right + pad; + } + + y = aabb.top - th / 2 + aabb.height / 2; + + img = document.createElement('img'); + img.width = tw; + img.height = th; + img.alt = ''; + img.src = '//i1.ytimg.com/vi/' + encodeURIComponent(vid) + '/mqdefault.jpg'; + + cnt = document.createElement('div'); + cnt.id = 'yt-preview'; + cnt.className = 'reply'; + cnt.style.left = (x + window.pageXOffset) + 'px'; + cnt.style.top = (y + window.pageYOffset) + 'px'; + + cnt.appendChild(img); + + document.body.appendChild(cnt); +}; + +Media.removeYTPreview = function() { + var el; + + if (el = $.id('yt-preview')) { + document.body.removeChild(el); + } +} + +Media.toggleYouTube = function(node) { + var vid, time, el, url; + + if (node.textContent == 'Remove') { + node.parentNode.removeChild(node.nextElementSibling); + node.textContent = 'Embed'; + } + else { + url = node.previousElementSibling.textContent; + vid = url.match(this.toggleYT); + time = url.match(this.timeYT); + + if (vid && (vid = vid[1])) { + vid = encodeURIComponent(vid); + + if (time && (time = time[1])) { + vid += '#t=' + encodeURIComponent(time); + } + + el = document.createElement('div'); + el.className = 'media-embed'; + el.innerHTML = '' + + node.parentNode.insertBefore(el, node.nextElementSibling); + + node.textContent = 'Remove'; + } + else { + node.textContent = 'Error'; + } + } +}; + +Media.parseVocaroo = function(msg) { + msg.innerHTML = msg.innerHTML.replace(this.matchVocaroo, this.replaceVocaroo); +}; + +Media.replaceVocaroo = function(link) { + return '' + link + ' [Embed]'; +}; + +Media.toggleVocaroo = function(node) { + var vid, time, el, url; + + if (node.textContent == 'Remove') { + node.parentNode.removeChild(node.nextElementSibling); + node.textContent = 'Embed'; + } + else { + url = node.previousElementSibling.textContent; + vid = url.match(Media.matchVocaroo); + + if (vid && (vid = vid[0].split('/').pop())) { + vid = encodeURIComponent(vid); + + el = document.createElement('div'); + el.className = 'media-embed'; + el.innerHTML = ''; + + node.parentNode.insertBefore(el, node.nextElementSibling); + + node.textContent = 'Remove'; + } + else { + node.textContent = 'Error'; + } + } +}; + +Media.toggleEmbed = function(node) { + var fn, type = node.getAttribute('data-type'); + + if (type && (fn = Media.map[type])) { + fn.call(this, node); + } +}; + +/** + * Custom CSS + */ +var CustomCSS = {}; + +CustomCSS.init = function() { + var style, css; + if (css = localStorage.getItem('4chan-css')) { + style = document.createElement('style'); + style.id = 'customCSS'; + style.setAttribute('type', 'text/css'); + style.textContent = css; + document.head.appendChild(style); + } +}; + +CustomCSS.open = function() { + var cnt, ta, data; + + if ($.id('customCSSMenu')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'customCSSMenu'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'css-close'); + cnt.innerHTML = '\ +
    Custom CSS\ +Close
    \ +\ +
    \ +
    '; + + document.body.appendChild(cnt); + + cnt.addEventListener('click', this.onClick, false); + + ta = $.id('customCSSBox'); + + if (data = localStorage.getItem('4chan-css')) { + ta.textContent = data; + } + + ta.focus(); +}; + +CustomCSS.save = function() { + var ta, style; + + if (ta = $.id('customCSSBox')) { + localStorage.setItem('4chan-css', ta.value); + if (Config.customCSS && (style = $.id('customCSS'))) { + document.head.removeChild(style); + CustomCSS.init(); + } + } +}; + +CustomCSS.close = function() { + var cnt; + + if (cnt = $.id('customCSSMenu')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +CustomCSS.onClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'css-close': + CustomCSS.close(); + break; + case 'css-save': + CustomCSS.save(); + CustomCSS.close(); + break; + } + } +}; + +/** + * Keyboard shortcuts + */ +var Keybinds = {}; + +Keybinds.init = function() { + this.map = { + // A + 65: function() { + if (ThreadUpdater.enabled) ThreadUpdater.toggleAuto(); + }, + // F + 70: function() { + if (Config.filter) { + Filter.addSelection(); + } + }, + // Q + 81: function() { + if (QR.enabled && Main.tid) { + QR.quotePost(Main.tid); + } + }, + // R + 82: function() { + if (ThreadUpdater.enabled) ThreadUpdater.forceUpdate(); + }, + // W + 87: function() { + if (Config.threadWatcher && Main.tid) ThreadWatcher.toggle(Main.tid); + }, + // B + 66: function() { + var el; + (el = $.cls('prev')[0]) && (el = $.tag('form', el)[0]) && el.submit(); + }, + // C + 67: function() { + location.href = '/' + Main.board + '/catalog'; + }, + // N + 78: function() { + var el; + (el = $.cls('next')[0]) && (el = $.tag('form', el)[0]) && el.submit(); + }, + // I + 73: function() { + location.href = '/' + Main.board + '/'; + } + }; + + document.addEventListener('keydown', this.resolve, false); +}; + +Keybinds.resolve = function(e) { + var bind, el = e.target; + + if (el.nodeName == 'TEXTAREA' || el.nodeName == 'INPUT') { + return; + } + + bind = Keybinds.map[e.keyCode]; + + if (bind && !e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + bind(); + } +}; + +Keybinds.open = function() { + var cnt; + + if ($.id('keybindsHelp')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'keybindsHelp'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'keybinds-close'); + cnt.innerHTML = '\ +
    Keyboard Shortcuts\ +Close
    \ +
      \ +
    • Global
    • \ +
    • A — Toggle auto-updater
    • \ +
    • Q — Open Quick Reply
    • \ +
    • R — Update thread
    • \ +
    • W — Watch/Unwatch thread
    • \ +
    • B — Previous page
    • \ +
    • N — Next page
    • \ +
    • I — Return to index
    • \ +
    • C — Open catalog
    • \ +
    • F — Filter selected text
    • \ +
      \ +
    • Quick Reply (always enabled)
    • \ +
    • Ctrl + Click the post number — Quote without linking
    • \ +
    • Ctrl + S — Spoiler tags
    • \ +
    • Esc — Close the Quick Reply
    • \ +
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); +}; + +Keybinds.close = function() { + var cnt; + + if (cnt = $.id('keybindsHelp')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Keybinds.onClick = function(e) { + var cmd; + + if ((cmd = e.target.getAttribute('data-cmd')) && cmd == 'keybinds-close') { + Keybinds.close(); + } +}; + +/** + * Reporting + */ +var Report = { + init: function() { + window.addEventListener('message', Report.onMessage, false); + } +}; + +Report.onMessage = function(e) { + var id; + + if (e.origin === 'https://sys.4chan.org' && /^done-report/.test(e.data)) { + id = e.data.split('-')[2]; + + if (Config.threadHiding && $.id('t' + id)) { + if (!ThreadHiding.isHidden(id)) { + ThreadHiding.hide(id); + ThreadHiding.save(); + } + + return; + } + + if ($.id('p' + id)) { + if (!ReplyHiding.isHidden(id)) { + ReplyHiding.hide(id); + ReplyHiding.save(); + } + + return; + } + } +}; + +Report.open = function(pid, board) { + window.open('https://sys.4chan.org/' + + (board || Main.board) + '/imgboard.php?mode=report&no=' + pid + , Date.now(), + "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=600,height=170"); +}; + +/** + * Custom Menu + */ +var CustomMenu = {}; + +CustomMenu.reset = function() { + var i, el, full, custom, navs; + + full = $.cls('boardList'); + custom = $.cls('customBoardList'); + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.removeEventListener('click', CustomMenu.reset, false); + } + + for (i = custom.length - 1; el = custom[i]; i--) { + full[i].style.display = null; + el.parentNode.removeChild(el); + } +}; + +CustomMenu.apply = function(str) { + var i, j, el, cntBottom, board, navs, boardList, more; + + if (!str) { + return; + } + + boardList = str.split(/[^0-9a-z]/i); + + cnt = document.createElement('span'); + cnt.className = 'customBoardList'; + + for (i = 0; board = boardList[i]; ++i) { + if (i) { + cnt.appendChild(document.createTextNode(' / ')); + } + else { + cnt.appendChild(document.createTextNode('[')); + } + el = document.createElement('a'); + el.textContent = board; + el.href = '//boards.4chan.org/' + board + '/'; + cnt.appendChild(el); + } + + cnt.appendChild(document.createTextNode(']')); + + cnt.appendChild(document.createTextNode(' [')); + el = document.createElement('a'); + el.textContent = '…'; + el.title = 'Show all'; + el.className = 'show-all-boards pointer'; + cnt.appendChild(el); + cnt.appendChild(document.createTextNode('] ')); + + cntBottom = cnt.cloneNode(true); + + navs = $.cls('boardList'); + + for (i = 0; el = navs[i]; ++i) { + el.style.display = 'none'; + el.parentNode.insertBefore(i ? cntBottom : cnt, el); + } + + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.addEventListener('click', CustomMenu.reset, false); + } +}; + +CustomMenu.onClick = function(e) { + var t; + + if ((t = e.target) == document) { + return; + } + + if (t.hasAttribute('data-close')) { + CustomMenu.closeEditor(); + } + else if (t.hasAttribute('data-save')) { + CustomMenu.save(); + } +}; + +CustomMenu.showEditor = function() { + var cnt; + + cnt = document.createElement('div'); + cnt.id = 'customMenu'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-close', '1'); + cnt.innerHTML = '\ +
    Custom Board List\ +Close
    \ +\ +
    '; + + document.body.appendChild(cnt); + + if (Config.customMenuList) { + $.id('customMenuBox').value = Config.customMenuList; + } + + cnt.addEventListener('click', CustomMenu.onClick, false); +}; + +CustomMenu.closeEditor = function() { + var el; + + if (el = $.id('customMenu')) { + el.removeEventListener('click', CustomMenu.onClick, false); + document.body.removeChild(el); + } +}; + +CustomMenu.save = function() { + var input; + + if (input = $.id('customMenuBox')) { + Config.customMenuList = input.value; + } + + CustomMenu.closeEditor(); +}; + +/** + * Draggable helper + */ +var Draggable = { + el: null, + key: null, + scrollX: null, + scrollY: null, + dx: null, dy: null, right: null, bottom: null, + + set: function(handle) { + handle.addEventListener('mousedown', Draggable.startDrag, false); + }, + + unset: function(handle) { + handle.removeEventListener('mousedown', Draggable.startDrag, false); + }, + + startDrag: function(e) { + var self, doc, offs; + + if (this.parentNode.hasAttribute('data-shiftkey') && !e.shiftKey) { + return; + } + + e.preventDefault(); + + self = Draggable; + doc = document.documentElement; + + self.el = this.parentNode; + + self.key = self.el.getAttribute('data-trackpos'); + offs = self.el.getBoundingClientRect(); + self.dx = e.clientX - offs.left; + self.dy = e.clientY - offs.top; + self.right = doc.clientWidth - offs.width; + self.bottom = doc.clientHeight - offs.height; + + if (getComputedStyle(self.el, null).position != 'fixed') { + self.scrollX = window.pageXOffset; + self.scrollY = window.pageYOffset; + } + else { + self.scrollX = self.scrollY = 0; + } + + document.addEventListener('mouseup', self.endDrag, false); + document.addEventListener('mousemove', self.onDrag, false); + }, + + endDrag: function(e) { + document.removeEventListener('mouseup', Draggable.endDrag, false); + document.removeEventListener('mousemove', Draggable.onDrag, false); + if (Draggable.key) { + Config[Draggable.key] = Draggable.el.style.cssText; + Config.save(); + } + delete Draggable.el; + }, + + onDrag: function(e) { + var left, top, style; + + left = e.clientX - Draggable.dx + Draggable.scrollX; + top = e.clientY - Draggable.dy + Draggable.scrollY; + style = Draggable.el.style; + if (left < 1) { + style.left = '0'; + style.right = ''; + } + else if (Draggable.right < left) { + style.left = ''; + style.right = '0'; + } + else { + style.left = (left / document.documentElement.clientWidth * 100) + '%'; + style.right = ''; + } + if (top < 1) { + style.top = '0'; + style.bottom = ''; + } + else if (Draggable.bottom < top) { + style.bottom = '0'; + style.top = ''; + } + else { + style.top = (top / document.documentElement.clientHeight * 100) + '%'; + style.bottom = ''; + } + } +}; + +/** + * User Agent + */ +var UA = {}; + +UA.init = function() { + document.head = document.head || $.tag('head')[0]; + + this.isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; + + this.hasCORS = 'withCredentials' in new XMLHttpRequest; + + this.hasFormData = 'FormData' in window; + + this.hasDragAndDrop = false; /*'draggable' in document.createElement('div');*/ +}; + +UA.dispatchEvent = function(name, detail) { + var e = document.createEvent('Event'); + e.initEvent(name, false, false); + if (detail) { + e.detail = detail; + } + document.dispatchEvent(e); +}; + +UA.getSelection = function(raw) { + var sel; + + if (UA.isOpera && typeof (sel = document.getSelection()) == 'string') {} + else { + sel = window.getSelection(); + + if (!raw) { + sel = sel.toString(); + } + } + + return sel; +}; + +/** + * Config + */ +var Config = { + quotePreview: true, + backlinks: true, + quickReply: true, + threadUpdater: true, + threadHiding: true, + + alwaysAutoUpdate: false, + topPageNav: false, + threadWatcher: false, + imageExpansion: true, + fitToScreenExpansion: false, + threadExpansion: true, + alwaysDepage: false, + localTime: true, + stickyNav: false, + keyBinds: false, + inlineQuotes: false, + + filter: false, + revealSpoilers: false, + imageHover: false, + threadStats: true, + IDColor: true, + noPictures: false, + embedYouTube: true, + embedSoundCloud: false, + updaterSound: false, + + customCSS: false, + autoScroll: false, + hideStubs: false, + compactThreads: false, + centeredThreads: false, + dropDownNav: false, + classicNav: false, + fixedThreadWatcher: false, + persistentQR: false, + forceHTTPS: false, + reportButton: false, + + disableAll: false +}; + +var ConfigMobile = { + embedYouTube: false, + compactThreads: false +}; + +Config.load = function() { + if (storage = localStorage.getItem('4chan-settings')) { + storage = JSON.parse(storage); + $.extend(Config, storage); + + if (Main.getCookie('https') === '1') { + Config.forceHTTPS = true; + } + else { + Config.forceHTTPS = false; + } + } + else { + Main.firstRun = true; + } +}; + +Config.loadFromURL = function() { + var cmd, data; + + cmd = location.href.split('=', 2); + + if (/#cfg$/.test(cmd[0])) { + try { + data = JSON.parse(decodeURIComponent(cmd[1])); + + history.replaceState(null, '', location.href.split('#', 1)[0]); + + $.extend(Config, JSON.parse(data.settings)); + + Config.save(); + + if (data.filters) { + localStorage.setItem('4chan-filters', data.filters); + } + + if (data.css) { + localStorage.setItem('4chan-css', data.css); + } + + if (data.catalogFilters) { + localStorage.setItem('catalog-filters', data.catalogFilters); + } + + if (data.catalogSettings) { + localStorage.setItem('catalog-settings', data.catalogSettings); + } + + return true; + } + catch (e) { + console.log(e); + } + } + + return false; +}; + +Config.toURL = function() { + var data, cfg = {}; + + cfg.settings = localStorage.getItem('4chan-settings'); + + if (data = localStorage.getItem('4chan-filters')) { + cfg.filters = data; + } + + if (data = localStorage.getItem('4chan-css')) { + cfg.css = data; + } + + if (data = localStorage.getItem('catalog-filters')) { + cfg.catalogFilters = data; + } + + if (data = localStorage.getItem('catalog-settings')) { + cfg.catalogSettings = data; + } + + return encodeURIComponent(JSON.stringify(cfg)); +}; + +Config.save = function() { + localStorage.setItem('4chan-settings', JSON.stringify(Config)); + + if (Config.forceHTTPS) { + Main.setCookie('https', 1); + } + else { + Main.removeCookie('https'); + } +}; + +/** + * Settings menu + */ +var SettingsMenu = {}; + +// [ Name, Subtitle, available on mobile?, is sub-option?, is mobile only? ] +SettingsMenu.options = { + 'Quotes & Replying': { + quotePreview: [ 'Quote preview', 'Show post when mousing over post links', true ], + backlinks: [ 'Backlinks', 'Show who has replied to a post', true ], + inlineQuotes: [ 'Inline quote links', 'Clicking quote links will inline expand the quoted post, Shift-click to bypass inlining' ], + quickReply: [ 'Quick Reply', 'Quickly respond to a post by clicking its post number', true ], + persistentQR: [ 'Persistent Quick Reply', 'Keep Quick Reply window open after posting' ] + }, + 'Monitoring': { + threadUpdater: [ 'Thread updater', 'Append new posts to bottom of thread without refreshing the page', true ], + alwaysAutoUpdate:[ 'Auto-update by default', 'Always auto-update threads', true ], + threadWatcher: [ 'Thread Watcher', 'Keep track of threads you\'re watching and see when they receive new posts', true ], + autoScroll: [ 'Auto-scroll with auto-updated posts', 'Automatically scroll the page as new posts are added' ], + updaterSound: [ 'Sound notification', 'Play a sound when somebody replies to your post(s)' ], + fixedThreadWatcher: [ 'Pin Thread Watcher to the page', 'Thread Watcher will scroll with you' ], + threadStats: [ 'Thread statistics', 'Display post and image counts on the right of the page, italics signify bump/image limit has been met' ], + }, + 'Filters & Post Hiding': { + filter: [ 'Filter and highlight specific threads/posts [Edit]', 'Enable pattern-based filters' ], + threadHiding: [ 'Thread hiding [Clear History]', 'Hide entire threads by clicking the minus button', true ], + hideStubs: [ 'Hide thread stubs', "Don't display stubs of hidden threads" ] + }, + 'Navigation': { + threadExpansion: [ 'Thread expansion', 'Expand threads inline on board indexes', true ], + dropDownNav: [ 'Use persistent drop-down navigation bar', '' ], + classicNav: [ 'Use traditional board list', '', false, true ], + customMenu: [ 'Custom board list [Edit]', 'Only show selected boards in top and bottom board lists' ], + alwaysDepage: [ 'Always use infinite scroll', 'Enable infinite scroll by default, so reaching the bottom of the board index will load subsequent pages' ], + topPageNav: [ 'Page navigation at top of page', 'Show the page switcher at the top of the page, hold Shift and drag to move' ], + stickyNav: [ 'Navigation arrows', 'Show top and bottom navigation arrows, hold Shift and drag to move' ], + keyBinds: [ 'Use keyboard shortcuts [Show]', 'Enable handy keyboard shortcuts for common actions' ] + }, + 'Images & Media': { + imageExpansion: [ 'Image expansion', 'Enable inline image expansion, limited to browser width', true ], + fitToScreenExpansion: [ 'Fit expanded images to screen', 'Limit expanded images to both browser width and height' ], + imageHover: [ 'Image hover', 'Mouse over images to view full size, limited to browser size' ], + revealSpoilers: [ "Don't spoiler images", 'Show image thumbnail and original filename instead of spoiler placeholders' ], + noPictures: [ 'Hide thumbnails', 'Don\'t display thumbnails while browsing', true ], + embedYouTube: [ 'Embed YouTube links', 'Embed YouTube player into replies' ], + embedSoundCloud: [ 'Embed SoundCloud links', 'Embed SoundCloud player into replies' ], + embedVocaroo: [ 'Embed Vocaroo links', 'Embed Vocaroo player into replies' ] + }, + 'Miscellaneous': { + customCSS: [ 'Custom CSS [Edit]', 'Include your own CSS rules', true ], + IDColor: [ 'Color user IDs', 'Assign unique colors to user IDs on boards that use them', true ], + compactThreads: [ 'Force long posts to wrap', 'Long posts will wrap at 75% browser width' ], + centeredThreads: [ 'Center threads', 'Align threads to the center of page', false ], + reportButton: [ 'Report button', 'Add a report button next to posts for easy reporting', true, false, true ], + localTime: [ 'Convert dates to local time', 'Convert 4chan server time (US Eastern Time) to your local time', true ], + forceHTTPS: [ 'Always use HTTPS', 'Rewrite 4chan URLs to always use HTTPS', true ] + } +}; + +SettingsMenu.save = function() { + var i, options, el, key; + + options = $.id('settingsMenu').getElementsByClassName('menuOption'); + + for (i = 0; el = options[i]; ++i) { + key = el.getAttribute('data-option'); + Config[key] = el.type == 'checkbox' ? el.checked : el.value; + } + + Config.save(); + + SettingsMenu.close(); + location.href = location.href.replace(/#.+$/, ''); +}; + +SettingsMenu.toggle = function() { + if ($.id('settingsMenu')) { + SettingsMenu.close(); + } + else { + SettingsMenu.open(); + } +}; + +SettingsMenu.open = function() { + var i, cat, categories, key, html, cnt, opts, mobileOpts, el; + + if (Main.firstRun) { + if (el = $.id('settingsTip')) { + el.parentNode.removeChild(el); + } + if (el = $.id('settingsTipBottom')) { + el.parentNode.removeChild(el); + } + Config.save(); + } + + cnt = document.createElement('div'); + cnt.id = 'settingsMenu'; + cnt.className = 'UIPanel'; + + html = '
    Settings' + + 'Close' + + '
      '; + + html += ''; + + if (Main.hasMobileLayout) { + categories = {}; + for (cat in SettingsMenu.options) { + mobileOpts = {}; + opts = SettingsMenu.options[cat]; + for (key in opts) { + if (opts[key][2]) { + mobileOpts[key] = opts[key]; + } + } + for (i in mobileOpts) { + categories[cat] = mobileOpts; + break; + } + } + } + else { + categories = SettingsMenu.options; + } + + for (cat in categories) { + opts = categories[cat]; + html += '
      • ' + + '' + + '' + + cat + '
        • '; + for (key in opts) { + // Mobile layout only? + if (opts[key][4] && !Main.hasMobileLayout) { + continue; + } + html += '' : '>') + + '' + + (opts[key][1] !== false ? '
        • ' : '">') + opts[key][1] : '') + + '
        • '; + } + html += '
      '; + } + + html += '
    • ' + + '
    ' + + '
    ' + + '
    '; + + cnt.innerHTML = html; + cnt.addEventListener('click', SettingsMenu.onClick, false); + document.body.appendChild(cnt); + + if (Main.firstRun) { + SettingsMenu.expandAll(); + } + + (el = $.cls('menuOption', cnt)[0]) && el.focus(); +}; + +SettingsMenu.showExport = function() { + var cnt, str, el; + + if ($.id('exportSettings')) { + return; + } + + str = location.href.replace(location.hash, '') + '#cfg=' + Config.toURL(); + + cnt = document.createElement('div'); + cnt.id = 'exportSettings'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'export-close'); + cnt.innerHTML = '\ +
    Export Settings\ +Close
    \ +

    Copy and save the URL below, and visit it from another \ +browser or computer to restore your extension and catalog settings.

    \ +

    \ +

    \ +

    Alternatively, you can drag the link below into your \ +bookmarks bar and click it to restore.

    \ +

    [Restore 4chan Settings]

    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onExportClick, false); + el = $.cls('export-field', cnt)[0]; + el.focus(); + el.select(); +}; + +SettingsMenu.closeExport = function() { + var cnt; + + if (cnt = $.id('exportSettings')) { + cnt.removeEventListener('click', this.onExportClick, false); + document.body.removeChild(cnt); + } +}; + +SettingsMenu.onExportClick = function(e) { + var el; + + if (e.target.id == 'exportSettings') { + e.preventDefault(); + e.stopPropagation(); + SettingsMenu.closeExport(); + } +}; + +SettingsMenu.expandAll = function() { + var i, el, nodes = $.cls('settings-expand'); + + for (i = 0; el = nodes[i]; ++i) { + el.src = Main.icons.minus; + el.parentNode.nextElementSibling.style.display = 'block'; + } +}; + +SettingsMenu.toggleCat = function(t) { + var icon, disp, el = t.parentNode.nextElementSibling; + + if (!el.style.display) { + disp = 'block'; + icon = 'minus'; + } + else { + disp = ''; + icon = 'plus'; + } + + el.style.display = disp; + t.parentNode.firstElementChild.src = Main.icons[icon]; +}; + +SettingsMenu.onClick = function(e) { + var el, t, i, j; + + t = e.target; + + if ($.hasClass(t, 'settings-expand')) { + SettingsMenu.toggleCat(t); + } + else if (t.getAttribute('data-cmd') == 'settings-exp-all') { + e.preventDefault(); + SettingsMenu.expandAll(); + } + else if (t.id == 'settingsMenu' && (el = $.id('settingsMenu'))) { + e.preventDefault(); + SettingsMenu.close(el); + } +}; + +SettingsMenu.close = function(el) { + if (el = (el || $.id('settingsMenu'))) { + el.removeEventListener('click', SettingsMenu.onClick, false); + document.body.removeChild(el); + } +}; + +/** + * Main + */ +var Main = {}; + +Main.addTooltip = function(link, message, id) { + var el, pos; + + el = document.createElement('div'); + el.className = 'click-me'; + if (id) { + el.id = id; + } + el.innerHTML = message || 'Change your settings'; + link.parentNode.appendChild(el); + + pos = (link.offsetWidth - el.offsetWidth + link.offsetLeft - el.offsetLeft) / 2; + el.style.marginLeft = pos + 'px'; + + return el; +}; + +Main.init = function() { + var params; + + document.addEventListener('DOMContentLoaded', Main.run, false); + + Main.now = Date.now(); + + UA.init(); + + Config.load(); + + if (Config.forceHTTPS && location.protocol != 'https:') { + location.href = location.href.replace(/^http:/, 'https:'); + return; + } + + if (Main.firstRun && Config.loadFromURL()) { + Main.firstRun = false; + } + + if (Main.stylesheet = Main.getCookie(style_group)) { + Main.stylesheet = Main.stylesheet.toLowerCase().replace(/ /g, '_'); + } + else { + Main.stylesheet = + style_group == 'nws_style' ? 'yotsuba_new' : 'yotsuba_b_new'; + } + + Main.passEnabled = Main.getCookie('pass_enabled'); + QR.noCaptcha = QR.noCaptcha || Main.passEnabled; + + Main.initIcons(); + + Main.addCSS(); + + Main.type = style_group.split('_')[0]; + + params = location.pathname.split(/\//); + Main.board = params[1]; + Main.page = params[2]; + Main.tid = params[3]; + + Report.init(); + + if (Config.IDColor) { + IDColor.init(); + } + + if (Config.customCSS) { + CustomCSS.init(); + } + + if (Config.keyBinds) { + Keybinds.init(); + } + + UA.dispatchEvent('4chanMainInit'); +}; + +Main.initPersistentNav = function() { + var el, top, bottom; + + top = $.id('boardNavDesktop'); + bottom = $.id('boardNavDesktopFoot'); + + if (Config.classicNav) { + el = document.createElement('div'); + el.className = 'pageJump'; + el.innerHTML = '' + + 'Settings' + + 'Home
    '; + + top.appendChild(el); + + $.id('settingsWindowLinkClassic') + .addEventListener('click', SettingsMenu.toggle, false); + + $.addClass(top, 'persistentNav'); + } + else { + top.style.display = 'none'; + $.removeClass($.id('boardNavMobile'), 'mobile'); + } + + bottom.style.display = 'none'; + + $.addClass(document.body, 'hasDropDownNav'); +}; + +Main.checkMobileLayout = function() { + var mobile, desktop; + + if (window.matchMedia) { + return window.matchMedia('(max-width: 480px)').matches + && localStorage.getItem('4chan_never_show_mobile') != 'true'; + } + + mobile = $.id('boardNavMobile'); + desktop = $.id('boardNavDesktop'); + + return mobile && desktop && mobile.offsetWidth > 0 && desktop.offsetWidth == 0; +}; + +Main.run = function() { + var thread; + + document.removeEventListener('DOMContentLoaded', Main.run, false); + + document.addEventListener('click', Main.onclick, false); + + $.id('settingsWindowLink').addEventListener('click', SettingsMenu.toggle, false); + $.id('settingsWindowLinkBot').addEventListener('click', SettingsMenu.toggle, false); + $.id('settingsWindowLinkMobile').addEventListener('click', SettingsMenu.toggle, false); + + if (Config.disableAll) { + return; + } + + Main.hasMobileLayout = Main.checkMobileLayout(); + Main.isMobileDevice = /Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent); + + if (Main.hasMobileLayout) { + $.extend(Config, ConfigMobile); + } + else { + $.id('bottomReportBtn').style.display = 'none'; + + if (Main.isMobileDevice) { + $.addClass(document.body, 'isMobileDevice'); + } + } + + if (Main.firstRun && Main.isMobileDevice) { + Config.topPageNav = false; + Config.dropDownNav = true; + } + + if (Config.dropDownNav && !Main.hasMobileLayout) { + Main.initPersistentNav(); + } + + $.addClass(document.body, Main.stylesheet); + $.addClass(document.body, Main.type); + + if (Config.compactThreads) { + $.addClass(document.body, 'compact'); + } + else if (Config.centeredThreads) { + $.addClass(document.body, 'centeredThreads'); + } + + if (Config.noPictures) { + $.addClass(document.body, 'noPictures'); + } + + if (Config.customMenu) { + CustomMenu.apply(Config.customMenuList); + } + + if (Config.quotePreview || Config.imageHover|| Config.filter) { + thread = $.id('delform'); + thread.addEventListener('mouseover', Main.onThreadMouseOver, false); + thread.addEventListener('mouseout', Main.onThreadMouseOut, false); + } + + if (!Main.hasMobileLayout) { + Main.initGlobalMessage(); + } + + if (Config.stickyNav) { + Main.setStickyNav(); + } + + if (Config.threadExpansion) { + ThreadExpansion.init(); + } + + if (Config.threadWatcher) { + ThreadWatcher.init(); + } + + if (Config.filter) { + Filter.init(); + } + + if (Config.embedSoundCloud || Config.embedYouTube || Config.embedVocaroo) { + Media.init(); + } + + ReplyHiding.init(); + + if (Config.quotePreview) { + QuotePreview.init(); + } + + Parser.init(); + + if (Main.tid) { + Main.threadClosed = !document.forms.post; + Main.threadSticky = !!$.cls('stickyIcon', $.id('pi' + Main.tid))[0]; + + if (Config.threadStats) { + ThreadStats.init(); + } + + Parser.parseThread(Main.tid); + + if (Config.threadUpdater) { + ThreadUpdater.init(); + } + } + else { + if (!Main.page) { + Depager.init(); + } + + if (Config.topPageNav) { + Main.setPageNav(); + } + if (Config.threadHiding) { + ThreadHiding.init(); + Parser.parseBoard(); + } + else { + Parser.parseBoard(); + } + } + + if (Main.board === 'f') { + SWFEmbed.init(); + } + + if (Config.quickReply) { + QR.init(); + } + + ReplyHiding.purge(); +}; + +Main.isThreadClosed = function(tid) { + return window.thread_archived || ((el = $.id('pi' + tid)) && $.cls('closedIcon', el)[0]) +}; + +Main.setThreadState = function(state, mode) { + var cnt, el, ref, cap; + + cap = state.charAt(0).toUpperCase() + state.slice(1); + + if (mode) { + cnt = $.cls('postNum', $.id('pi' + Main.tid))[0]; + el = document.createElement('img'); + el.className = state + 'Icon retina'; + el.title = cap; + el.src = Main.icons2[state]; + if (state == 'sticky' && (ref = $.cls('closedIcon', cnt)[0])) { + cnt.insertBefore(el, ref); + cnt.insertBefore(document.createTextNode(' '), ref); + } + else { + cnt.appendChild(document.createTextNode(' ')); + cnt.appendChild(el); + } + } + else { + if (el = $.cls(state + 'Icon', $.id('pi' + Main.tid))[0]) { + el.parentNode.removeChild(el.previousSibling); + el.parentNode.removeChild(el); + } + } + + Main['thread' + cap] = mode; +}; + +Main.icons = { + up: 'arrow_up.png', + down: 'arrow_down.png', + right: 'arrow_right.png', + download: 'arrow_down2.png', + refresh: 'refresh.png', + cross: 'cross.png', + gis: 'gis.png', + iqdb: 'iqdb.png', + minus: 'post_expand_minus.png', + plus: 'post_expand_plus.png', + rotate: 'post_expand_rotate.gif', + quote: 'quote.png', + report: 'report.png', + notwatched: 'watch_thread_off.png', + watched: 'watch_thread_on.png', + help: 'question.png' +}; + +Main.icons2 = { + archived: 'archived.gif', + closed: 'closed.gif', + sticky: 'sticky.gif', + trash: 'trash.gif' +}, + +Main.initIcons = function() { + var key, paths, url; + + paths = { + yotsuba_new: 'futaba/', + futaba_new: 'futaba/', + yotsuba_b_new: 'burichan/', + burichan_new: 'burichan/', + tomorrow: 'tomorrow/', + photon: 'photon/' + }; + + url = '//s.4cdn.org/image/' + + if (window.devicePixelRatio >= 2) { + for (key in Main.icons) { + Main.icons[key] = Main.icons[key].replace('.', '@2x.'); + } + for (key in Main.icons2) { + Main.icons2[key] = Main.icons2[key].replace('.', '@2x.'); + } + } + + for (key in Main.icons2) { + Main.icons2[key] = url + Main.icons2[key]; + } + + url += 'buttons/' + paths[Main.stylesheet]; + for (key in Main.icons) { + Main.icons[key] = url + Main.icons[key]; + } +}; + +Main.setPageNav = function() { + var el, cnt; + + cnt = document.createElement('div'); + cnt.setAttribute('data-shiftkey', '1'); + cnt.setAttribute('data-trackpos', 'TN-position'); + cnt.className = 'topPageNav'; + + if (Config['TN-position']) { + cnt.style.cssText = Config['TN-position']; + } + else { + cnt.style.left = '10px'; + cnt.style.top = '50px'; + } + + el = $.cls('pagelist')[0] + + if (!el) { + return; + } + + el = el.cloneNode(true); + cnt.appendChild(el); + Draggable.set(el); + document.body.appendChild(cnt); +}; + +Main.initGlobalMessage = function() { + var msg, btn, thisTs, oldTs; + + if ((msg = $.id('globalMessage')) && msg.textContent) { + msg.nextElementSibling.style.clear = 'both'; + + btn = document.createElement('img'); + btn.id = 'toggleMsgBtn'; + btn.className = 'extButton'; + btn.setAttribute('data-cmd', 'toggleMsg'); + btn.alt = 'Toggle'; + btn.title = 'Toggle announcement'; + + oldTs = localStorage.getItem('4chan-global-msg'); + thisTs = msg.getAttribute('data-utc'); + + if (oldTs && thisTs <= oldTs) { + msg.style.display = 'none'; + btn.style.opacity = '0.5'; + btn.src = Main.icons.plus; + } + else { + btn.src = Main.icons.minus; + } + + msg.parentNode.insertBefore(btn, msg); + } +}; + +Main.toggleGlobalMessage = function() { + var msg, btn; + + msg = $.id('globalMessage'); + btn = $.id('toggleMsgBtn'); + if (msg.style.display == 'none') { + msg.style.display = ''; + btn.src = Main.icons.minus; + btn.style.opacity = '1'; + localStorage.removeItem('4chan-global-msg'); + } + else { + msg.style.display = 'none'; + btn.src = Main.icons.plus; + btn.style.opacity = '0.5'; + localStorage.setItem('4chan-global-msg', msg.getAttribute('data-utc')); + } +}; + +Main.setStickyNav = function() { + var cnt, hdr; + + cnt = document.createElement('div'); + cnt.id = 'stickyNav'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-shiftkey', '1'); + cnt.setAttribute('data-trackpos', 'SN-position'); + + if (Config['SN-position']) { + cnt.style.cssText = Config['SN-position']; + } + else { + cnt.style.right = '10px'; + cnt.style.top = '50px'; + } + + hdr = document.createElement('div'); + hdr.innerHTML = '▲' + + '▼'; + Draggable.set(hdr); + + cnt.appendChild(hdr); + document.body.appendChild(cnt); +}; + +Main.getCookie = function(name) { + var i, c, ca, key; + + key = name + "="; + ca = document.cookie.split(';'); + + for (i = 0; c = ca[i]; ++i) { + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(key) == 0) { + return decodeURIComponent(c.substring(key.length, c.length)); + } + } + return null; +}; + +Main.setCookie = function(name, value) { + var date = new Date(); + + date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000)); + + document.cookie = name + '=' + value + + '; expires=' + date.toGMTString() + + '; path=/; domain=boards.4chan.org'; +}; + +Main.removeCookie = function(name) { + document.cookie = name + '=' + + '; expires=Thu, 01 Jan 1970 00:00:01 GMT;' + + '; path=/; domain=boards.4chan.org'; +}; + +Main.onclick = function(e) { + var t, cmd, tid; + + if ((t = e.target) == document) { + return; + } + + if (cmd = t.getAttribute('data-cmd')) { + id = t.getAttribute('data-id'); + switch (cmd) { + case 'update': + e.preventDefault(); + ThreadUpdater.forceUpdate(); + break; + case 'post-menu': + e.preventDefault(); + PostMenu.open(t); + break; + case 'auto': + ThreadUpdater.toggleAuto(); + break; + case 'totop': + case 'tobottom': + if (!e.shiftKey) { + location.href = '#' + cmd.slice(2); + } + break; + case 'hide': + ThreadHiding.toggle(id); + break; + case 'watch': + ThreadWatcher.toggle(id); + break; + case 'hide-r': + ReplyHiding.toggle(id); + break; + case 'expand': + ThreadExpansion.toggle(id); + break; + case 'open-qr': + e.preventDefault(); + QR.show(Main.tid); + $.tag('textarea', document.forms.qrPost)[0].focus(); + break; + case 'depage': + e.preventDefault(); + Depager.toggle(); + break; + case 'report': + Report.open(id, t.getAttribute('data-board')); + break; + case 'filter-sel': + e.preventDefault(); + Filter.addSelection(); + break; + case 'embed': + Media.toggleEmbed(t); + break + case 'sound': + ThreadUpdater.toggleSound(); + break; + case 'toggleMsg': + Main.toggleGlobalMessage(); + break; + case 'settings-toggle': + SettingsMenu.toggle(); + break; + case 'settings-save': + SettingsMenu.save(); + break; + case 'keybinds-open': + Keybinds.open(); + break; + case 'filters-open': + Filter.open(); + break; + case 'thread-hiding-clear': + ThreadHiding.clear(); + break; + case 'css-open': + CustomCSS.open(); + break; + case 'settings-export': + SettingsMenu.showExport(); + break; + case 'export-close': + SettingsMenu.closeExport(); + break; + case 'custom-menu-edit': + CustomMenu.showEditor(); + break; + } + } + else if (!Config.disableAll) { + if (QR.enabled && t.title == 'Reply to this post') { + e.preventDefault(); + tid = Main.tid || t.previousElementSibling.getAttribute('href').split('#')[0].split('/')[1]; + QR.quotePost(tid, !e.ctrlKey && t.textContent); + } + else if (Config.imageExpansion && e.which == 1 && t.parentNode + && $.hasClass(t.parentNode, 'fileThumb') + && t.parentNode.nodeName == 'A' + && !$.hasClass(t.parentNode, 'deleted')) { + + if (ImageExpansion.toggle(t)) { + e.preventDefault(); + } + } + else if (Config.inlineQuotes && e.which == 1 && $.hasClass(t, 'quotelink')) { + if (!e.shiftKey) { + QuoteInline.toggle(t, e); + } + else { + e.preventDefault(); + window.location = t.href; + } + } + else if (Config.threadExpansion && t.parentNode && $.hasClass(t.parentNode, 'abbr')) { + e.preventDefault(); + ThreadExpansion.expandComment(t); + } + else if (Main.isMobileDevice && Config.quotePreview) { + if ($.hasClass(t, 'quotelink') + && (cmd = t.getAttribute('href').match(QuotePreview.regex)) + && cmd[1] != 'rs') { + e.preventDefault(); + } + } + } +}; + +Main.onThreadMouseOver = function(e) { + var t = e.target; + + if (Config.quotePreview + && $.hasClass(t, 'quotelink') + && !$.hasClass(t, 'deadlink') + && !$.hasClass(t, 'linkfade')) { + QuotePreview.resolve(e.target); + } + else if (Config.imageHover && t.hasAttribute('data-md5') + && !$.hasClass(t.parentNode, 'deleted')) { + ImageHover.show(t); + } + else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) { + Media.showYTPreview(t); + } + else if (Config.filter && t.hasAttribute('data-filtered')) { + QuotePreview.show(t, + t.href ? t.parentNode.parentNode.parentNode : t.parentNode.parentNode); + } +}; + +Main.onThreadMouseOut = function(e) { + var t = e.target; + + if (Config.quotePreview && $.hasClass(t, 'quotelink')) { + QuotePreview.remove(t); + } + else if (Config.imageHover && t.hasAttribute('data-md5')) { + ImageHover.hide(); + } + else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) { + Media.removeYTPreview(); + } + else if (Config.filter && t.hasAttribute('data-filtered')) { + QuotePreview.remove(t); + } +}; + +Main.linkToThread = function(tid, board, post) { + return '//' + location.host + '/' + + (board || Main.board) + '/thread/' + + tid + (post > 0 ? ('#p' + post) : ''); +}; + +Main.addCSS = function() { + var style, css = '\ +body.hasDropDownNav {\ + margin-top: 45px;\ +}\ +.extButton.threadHideButton {\ + float: left;\ + margin-right: 5px;\ + margin-top: -1px;\ +}\ +.extButton.replyHideButton {\ + margin-top: 1px;\ +}\ +div.op > span .postHideButtonCollapsed {\ + margin-right: 1px;\ +}\ +.dropDownNav #boardNavMobile, {\ + display: block !important;\ +}\ +.extPanel {\ + border: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.tomorrow .extPanel {\ + border: 1px solid #111;\ +}\ +.extButton,\ +img.pointer {\ + width: 18px;\ + height: 18px;\ +}\ +.extControls {\ + display: inline;\ + margin-left: 5px;\ +}\ +.extButton {\ + cursor: pointer;\ + margin-bottom: -4px;\ +}\ +.trashIcon {\ + width: 16px;\ + height: 16px;\ + margin-bottom: -2px;\ + margin-left: 5px;\ +}\ +.threadUpdateStatus {\ + margin-left: 0.5ex;\ +}\ +.futaba_new .stub,\ +.burichan_new .stub {\ + line-height: 1;\ + padding-bottom: 1px;\ +}\ +.stub .extControls,\ +.stub .wbtn,\ +.stub input {\ + display: none;\ +}\ +.stub .threadHideButton {\ + float: none;\ + margin-right: 2px;\ +}\ +div.post div.postInfo {\ + width: auto;\ + display: inline;\ +}\ +.right {\ + float: right;\ +}\ +.center {\ + display: block;\ + margin: auto;\ +}\ +.pointer {\ + cursor: pointer;\ +}\ +.drag {\ + cursor: move !important;\ + user-select: none !important;\ + -moz-user-select: none !important;\ + -webkit-user-select: none !important;\ +}\ +#quickReport,\ +#quickReply {\ + display: block;\ + position: fixed;\ + padding: 2px;\ + font-size: 10pt;\ +}\ +#qrepHeader,\ +#qrHeader {\ + text-align: center;\ + margin-bottom: 1px;\ + padding: 0;\ + height: 18px;\ + line-height: 18px;\ +}\ +#qrepClose,\ +#qrClose {\ + float: right;\ +}\ +#quickReport iframe {\ + overflow: hidden;\ +}\ +#quickReport {\ + height: 190px;\ +}\ +#qrForm > div {\ + clear: both;\ +}\ +#quickReply input[type="text"],\ +#quickReply textarea,\ +#quickReply #recaptcha_response_field {\ + border: 1px solid #aaa;\ + font-family: arial,helvetica,sans-serif;\ + font-size: 10pt;\ + outline: medium none;\ + width: 296px;\ + padding: 2px;\ + margin: 0 0 1px 0;\ +}\ +#quickReply textarea {\ + min-width: 296px;\ + float: left;\ +}\ +#quickReply input::-moz-placeholder,\ +#quickReply textarea::-moz-placeholder {\ + color: #aaa !important;\ + opacity: 1 !important;\ +}\ +#quickReply input[type="submit"] {\ + width: 83px;\ + margin: 0;\ + font-size: 10pt;\ + float: left;\ +}\ +#quickReply #qrCapField {\ + display: block;\ + margin-top: 1px;\ +}\ +#qrCaptcha {\ + width: 300px;\ + height: 53px;\ + cursor: pointer;\ + border: 1px solid #aaa;\ + display: block;\ +}\ +#quickReply input.presubmit {\ + margin-right: 1px;\ + width: 212px;\ + float: left;\ +}\ +#qrFile {\ + width: 215px;\ + margin-right: 5px;\ +}\ +.qrRealFile {\ + position: absolute;\ + left: 0;\ + visibility: hidden;\ +}\ +.yotsuba_new #qrFile {\ + color:black;\ +}\ +#qrSpoiler {\ + display: inline;\ +}\ +#qrError {\ + width: 292px;\ + display: none;\ + font-family: monospace;\ + background-color: #E62020;\ + font-size: 12px;\ + color: white;\ + padding: 3px 5px;\ + text-shadow: 0 1px rgba(0, 0, 0, 0.20);\ + clear: both;\ +}\ +#qrError a:hover,\ +#qrError a {\ + color: white !important;\ + text-decoration: underline;\ +}\ +#twHeader {\ + font-weight: bold;\ + text-align: center;\ + height: 17px;\ +}\ +.futaba_new #twHeader,\ +.burichan_new #twHeader {\ + line-height: 1;\ +}\ +#twPrune {\ + margin-left: 3px;\ + margin-top: -1px;\ +}\ +#twClose {\ + float: left;\ + margin-top: -1px;\ +}\ +#threadWatcher {\ + max-width: 265px;\ + display: block;\ + position: absolute;\ + padding: 3px;\ +}\ +#watchList {\ + margin: 0;\ + padding: 0;\ + user-select: none;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ +}\ +#watchList li:first-child {\ + margin-top: 3px;\ + padding-top: 2px;\ + border-top: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.photon #watchList li:first-child {\ + border-top: 1px solid #ccc;\ +}\ +.yotsuba_new #watchList li:first-child {\ + border-top: 1px solid #d9bfb7;\ +}\ +.yotsuba_b_new #watchList li:first-child {\ + border-top: 1px solid #b7c5d9;\ +}\ +.tomorrow #watchList li:first-child {\ + border-top: 1px solid #111;\ +}\ +#watchList a {\ + text-decoration: none;\ +}\ +#watchList li {\ + overflow: hidden;\ + white-space: nowrap;\ + text-overflow: ellipsis;\ +}\ +div.post div.image-expanded {\ + display: table;\ +}\ +div.op div.file .image-expanded-anti {\ + margin-left: -3px;\ +}\ +#quote-preview {\ + display: block;\ + position: absolute;\ + top: 0;\ + padding: 3px 6px 6px 3px;\ + margin: 0;\ +}\ +#quote-preview .dateTime {\ + white-space: nowrap;\ +}\ +.yotsuba_new #quote-preview.highlight,\ +.yotsuba_b_new #quote-preview.highlight {\ + border-width: 1px 2px 2px 1px !important;\ + border-style: solid !important;\ +}\ +.yotsuba_new #quote-preview.highlight {\ + border-color: #D99F91 !important;\ +}\ +.yotsuba_b_new #quote-preview.highlight {\ + border-color: #BA9DBF !important;\ +}\ +.yotsuba_b_new .highlight-anti,\ +.burichan_new .highlight-anti {\ + border-width: 1px !important;\ + background-color: #bfa6ba !important;\ +}\ +.yotsuba_new .highlight-anti,\ +.futaba_new .highlight-anti {\ + background-color: #e8a690 !important;\ +}\ +.tomorrow .highlight-anti {\ + background-color: #111 !important;\ + border-color: #111;\ +}\ +.photon .highlight-anti {\ + background-color: #bbb !important;\ +}\ +.op.inlined {\ + display: block;\ +}\ +#quote-preview .inlined,\ +#quote-preview .postMenuBtn,\ +#quote-preview .extButton,\ +#quote-preview .extControls {\ + display: none;\ +}\ +.hasNewReplies {\ + font-weight: bold;\ +}\ +.archivelink {\ + opacity: 0.5;\ +}\ +.deadlink {\ + text-decoration: line-through !important;\ +}\ +div.backlink {\ + font-size: 0.8em !important;\ + display: inline;\ + padding: 0;\ + padding-left: 5px;\ +}\ +.backlink.mobile {\ + padding: 3px 5px;\ + display: block;\ + clear: both;\ + line-height: 2;\ +}\ +.op .backlink.mobile,\ +#quote-preview .backlink.mobile {\ + display: none !important;\ +}\ +.backlink.mobile .quoteLink {\ + padding-right: 2px;\ +}\ +.backlink span {\ + padding: 0;\ +}\ +.burichan_new .backlink a,\ +.yotsuba_b_new .backlink a {\ + color: #34345C !important;\ +}\ +.burichan_new .backlink a:hover,\ +.yotsuba_b_new .backlink a:hover {\ + color: #dd0000 !important;\ +}\ +.expbtn {\ + margin-right: 3px;\ + margin-left: 2px;\ +}\ +.tCollapsed .rExpanded {\ + display: none;\ +}\ +#stickyNav {\ + position: fixed;\ + font-size: 0;\ +}\ +#stickyNav img {\ + vertical-align: middle;\ +}\ +.tu-error {\ + color: red;\ +}\ +.topPageNav {\ + position: absolute;\ +}\ +.yotsuba_b_new .topPageNav {\ + border-top: 1px solid rgba(255, 255, 255, 0.25);\ + border-left: 1px solid rgba(255, 255, 255, 0.25);\ +}\ +.newPostsMarker:not(#quote-preview) {\ + box-shadow: 0 3px red;\ +}\ +#toggleMsgBtn {\ + float: left;\ + margin-bottom: 6px;\ +}\ +.panelHeader {\ + font-weight: bold;\ + font-size: 16px;\ + text-align: center;\ + margin-bottom: 5px;\ + margin-top: 5px;\ + padding-bottom: 5px;\ + border-bottom: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.yotsuba_new .panelHeader {\ + border-bottom: 1px solid #d9bfb7;\ +}\ +.yotsuba_b_new .panelHeader {\ + border-bottom: 1px solid #b7c5d9;\ +}\ +.tomorrow .panelHeader {\ + border-bottom: 1px solid #111;\ +}\ +.panelHeader span {\ + position: absolute;\ + right: 5px;\ + top: 5px;\ +}\ +.UIMenu,\ +.UIPanel {\ + position: fixed;\ + width: 100%;\ + height: 100%;\ + z-index: 9002;\ + top: 0;\ + left: 0;\ +}\ +.UIPanel {\ + line-height: 14px;\ + font-size: 14px;\ + background-color: rgba(0, 0, 0, 0.25);\ +}\ +.UIPanel:after {\ + display: inline-block;\ + height: 100%;\ + vertical-align: middle;\ + content: "";\ +}\ +.UIPanel > div {\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + display: inline-block;\ + height: auto;\ + max-height: 100%;\ + position: relative;\ + width: 400px;\ + left: 50%;\ + margin-left: -200px;\ + overflow: auto;\ + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\ + vertical-align: middle;\ +}\ +#settingsMenu > div {\ + top: 25px;;\ + vertical-align: top;\ + max-height: 85%;\ +}\ +.extPanel input[type="text"],\ +.extPanel textarea {\ + border: 1px solid #AAA;\ + outline: none;\ +}\ +.UIPanel .center {\ + margin-bottom: 5px;\ +}\ +.UIPanel button {\ + display: inline-block;\ + margin-right: 5px;\ +}\ +.UIPanel code {\ + background-color: #eee;\ + color: #000000;\ + padding: 1px 4px;\ + font-size: 12px;\ +}\ +.UIPanel ul {\ + list-style: none;\ + padding: 0;\ + margin: 0 0 10px;\ +}\ +.UIPanel .export-field {\ + width: 385px;\ +}\ +#settingsMenu label input {\ + margin-right: 5px;\ +}\ +.tomorrow #settingsMenu ul {\ + border-bottom: 1px solid #282a2e;\ +}\ +.settings-off {\ + padding-left: 3px;\ +}\ +.settings-cat-lbl {\ + font-weight: bold;\ + margin: 10px 0 5px;\ + padding-left: 5px;\ +}\ +.settings-cat-lbl img {\ + vertical-align: text-bottom;\ + margin-right: 5px;\ + cursor: pointer;\ + width: 18px;\ + height: 18px;\ +}\ +.settings-tip {\ + font-size: 0.85em;\ + margin: 2px 0 5px 0;\ + padding-left: 23px;\ +}\ +#settings-exp-all {\ + padding-left: 7px;\ + text-align: center;\ +}\ +#settingsMenu .settings-cat {\ + display: none;\ + margin-left: 3px;\ +}\ +#customCSSMenu textarea {\ + display: block;\ + max-width: 100%;\ + min-width: 100%;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + height: 200px;\ + margin: 0 0 5px;\ + font-family: monospace;\ +}\ +#customCSSMenu .right,\ +#settingsMenu .right {\ + margin-top: 2px;\ +}\ +#settingsMenu label {\ + display: inline-block;\ + user-select: none;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ +}\ +#filtersHelp > div {\ + width: 600px;\ + left: 50%;\ + margin-left: -300px;\ +}\ +#filtersHelp h4 {\ + font-size: 15px;\ + margin: 20px 0 0 10px;\ +}\ +#filtersHelp h4:before {\ + content: "»";\ + margin-right: 3px;\ +}\ +#filtersHelp ul {\ + padding: 0;\ + margin: 10px;\ +}\ +#filtersHelp li {\ + padding: 3px 0;\ + list-style: none;\ +}\ +#filtersMenu table {\ + width: 100%;\ +}\ +#filtersMenu th {\ + font-size: 12px;\ +}\ +#filtersMenu tbody {\ + text-align: center;\ +}\ +#filtersMenu select,\ +#filtersMenu .fPattern,\ +#filtersMenu .fBoards,\ +#palette-custom-input {\ + padding: 1px;\ + font-size: 11px;\ +}\ +#filtersMenu select {\ + width: 75px;\ +}\ +#filtersMenu tfoot td {\ + padding-top: 10px;\ +}\ +#keybindsHelp li {\ + padding: 3px 5px;\ +}\ +.fPattern {\ + width: 110px;\ +}\ +.fBoards {\ + width: 25px;\ +}\ +.fColor {\ + width: 60px;\ +}\ +.fDel {\ + font-size: 16px;\ +}\ +.filter-preview {\ + cursor: default;\ + margin-left: 3px;\ +}\ +#quote-preview iframe,\ +#quote-preview .filter-preview {\ + display: none;\ +}\ +.post-hidden .extButton,\ +.post-hidden:not(#quote-preview) .postInfo {\ + opacity: 0.5;\ +}\ +.post-hidden:not(.thread) .postInfo {\ + padding-left: 5px;\ +}\ +.post-hidden:not(#quote-preview) input,\ +.post-hidden:not(#quote-preview) .replyContainer,\ +.post-hidden:not(#quote-preview) .summary,\ +.post-hidden:not(#quote-preview) .op .file,\ +.post-hidden:not(#quote-preview) .file,\ +.post-hidden .wbtn,\ +.post-hidden .postNum span,\ +.post-hidden:not(#quote-preview) .backlink,\ +div.post-hidden:not(#quote-preview) div.file,\ +div.post-hidden:not(#quote-preview) blockquote.postMessage {\ + display: none;\ +}\ +.click-me {\ + border-radius: 5px;\ + margin-top: 5px;\ + padding: 2px 5px;\ + position: absolute;\ + font-weight: bold;\ + z-index: 2;\ + white-space: nowrap;\ +}\ +.yotsuba_new .click-me,\ +.futaba_new .click-me {\ + color: #800000;\ + background-color: #F0E0D6;\ + border: 2px solid #D9BFB7;\ +}\ +.yotsuba_b_new .click-me,\ +.burichan_new .click-me {\ + color: #000;\ + background-color: #D6DAF0;\ + border: 2px solid #B7C5D9;\ +}\ +.tomorrow .click-me {\ + color: #C5C8C6;\ + background-color: #282A2E;\ + border: 2px solid #111;\ +}\ +.photon .click-me {\ + color: #333;\ + background-color: #ddd;\ + border: 2px solid #ccc;\ +}\ +.click-me:before {\ + content: "";\ + border-width: 0 6px 6px;\ + border-style: solid;\ + left: 50%;\ + margin-left: -6px;\ + position: absolute;\ + width: 0;\ + height: 0;\ + top: -6px;\ +}\ +.yotsuba_new .click-me:before,\ +.futaba_new .click-me:before {\ + border-color: #D9BFB7 transparent;\ +}\ +.yotsuba_b_new .click-me:before,\ +.burichan_new .click-me:before {\ + border-color: #B7C5D9 transparent;\ +}\ +.tomorrow .click-me:before {\ + border-color: #111 transparent;\ +}\ +.photon .click-me:before {\ + border-color: #ccc transparent;\ +}\ +.click-me:after {\ + content: "";\ + border-width: 0 4px 4px;\ + top: -4px;\ + display: block;\ + left: 50%;\ + margin-left: -4px;\ + position: absolute;\ + width: 0;\ + height: 0;\ +}\ +.yotsuba_new .click-me:after,\ +.futaba_new .click-me:after {\ + border-color: #F0E0D6 transparent;\ + border-style: solid;\ +}\ +.yotsuba_b_new .click-me:after,\ +.burichan_new .click-me:after {\ + border-color: #D6DAF0 transparent;\ + border-style: solid;\ +}\ +.tomorrow .click-me:after {\ + border-color: #282A2E transparent;\ + border-style: solid;\ +}\ +.photon .click-me:after {\ + border-color: #DDD transparent;\ + border-style: solid;\ +}\ +#image-hover {\ + position: fixed;\ + max-width: 100%;\ + max-height: 100%;\ + top: 0px;\ + right: 0px;\ + z-index: 9002;\ +}\ +.thread-stats {\ + float: right;\ + margin-right: 5px;\ + cursor: default;\ +}\ +.compact .thread {\ + max-width: 75%;\ +}\ +.dotted {\ + text-decoration: none;\ + border-bottom: 1px dashed;\ +}\ +.linkfade {\ + opacity: 0.5;\ +}\ +#quote-preview .linkfade {\ + opacity: 1.0;\ +}\ +kbd {\ + background-color: #f7f7f7;\ + color: black;\ + border: 1px solid #ccc;\ + border-radius: 3px 3px 3px 3px;\ + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset;\ + font-family: monospace;\ + font-size: 11px;\ + line-height: 1.4;\ + padding: 0 5px;\ +}\ +.deleted {\ + opacity: 0.66;\ +}\ +.noPictures a.fileThumb img:not(.expanded-thumb) {\ + opacity: 0;\ +}\ +.noPictures.futaba_new a.fileThumb,\ +.noPictures.yotsuba_new a.fileThumb {\ + border: 1px solid #800;\ +}\ +.noPictures.burichan_new a.fileThumb,\ +.noPictures.yotsuba_b_new a.fileThumb {\ + border: 1px solid #34345C;\ +}\ +.noPictures.tomorrow a.fileThumb:not(.expanded-thumb) {\ + border: 1px solid #C5C8C6;\ +}\ +.noPictures.photon a.fileThumb:not(.expanded-thumb) {\ + border: 1px solid #004A99;\ +}\ +.spinner {\ + margin-top: 2px;\ + padding: 3px;\ + display: table;\ +}\ +#settings-presets {\ + position: relative;\ + top: -1px;\ +}\ +#colorpicker { \ + position: fixed;\ + text-align: center;\ +}\ +.colorbox {\ + font-size: 10px;\ + width: 16px;\ + height: 16px;\ + line-height: 17px;\ + display: inline-block;\ + text-align: center;\ + background-color: #fff;\ + border: 1px solid #aaa;\ + text-decoration: none;\ + color: #000;\ + cursor: pointer;\ + vertical-align: top;\ +}\ +#palette-custom-input {\ + vertical-align: top;\ + width: 45px;\ + margin-right: 2px;\ +}\ +#qrDummyFile {\ + float: left;\ + margin-right: 5px;\ + width: 220px;\ + cursor: default;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ + -ms-user-select: none;\ + user-select: none;\ + white-space: nowrap;\ + text-overflow: ellipsis;\ + overflow: hidden;\ +}\ +#qrDummyFileLabel {\ + margin-left: 3px;\ +}\ +.depageNumber {\ + position: absolute;\ + right: 5px;\ +}\ +.depagerEnabled .depagelink {\ + font-weight: bold;\ +}\ +.depagerEnabled strong {\ + font-weight: normal;\ +}\ +.depagelink {\ + display: inline-block;\ + padding: 4px 0;\ + cursor: pointer;\ + text-decoration: none;\ +}\ +.burichan_new .depagelink,\ +.futaba_new .depagelink {\ + text-decoration: underline;\ +}\ +#customMenuBox {\ + margin: 0 auto 5px auto;\ + width: 385px;\ + display: block;\ +}\ +.preview-summary {\ + display: block;\ +}\ +#swf-embed-header {\ + padding: 0 0 0 3px;\ + font-weight: normal;\ + height: 20px;\ + line-height: 20px;\ +}\ +.yotsuba_new #swf-embed-header,\ +.yotsuba_b_new #swf-embed-header {\ + height: 18px;\ + line-height: 18px;\ +}\ +#swf-embed-close {\ + position: absolute;\ + right: 0;\ + top: 1px;\ +}\ +.open-qr-wrap {\ + text-align: center;\ + width: 200px;\ + position: absolute;\ + margin-left: 50%;\ + left: -100px;\ +}\ +.postMenuBtn {\ + margin-left: 5px;\ + text-decoration: none;\ + line-height: 1em;\ + display: inline-block;\ + -webkit-transition: -webkit-transform 0.1s;\ + -moz-transition: -moz-transform 0.1s;\ + transition: transform 0.1s;\ + width: 1em;\ + height: 1em;\ + text-align: center;\ + outline: none;\ + opacity: 0.8;\ +}\ +.postMenuBtn:hover{\ + opacity: 1;\ +}\ +.yotsuba_new .postMenuBtn,\ +.futaba_new .postMenuBtn {\ + color: #000080;\ +}\ +.tomorrow .postMenuBtn {\ + color: #5F89AC !important;\ +}\ +.tomorrow .postMenuBtn:hover {\ + color: #81a2be !important;\ +}\ +.photon .postMenuBtn {\ + color: #FF6600 !important;\ +}\ +.photon .postMenuBtn:hover {\ + color: #FF3300 !important;\ +}\ +.menuOpen {\ + -webkit-transform: rotate(90deg);\ + -moz-transform: rotate(90deg);\ + -ms-transform: rotate(90deg);\ + transform: rotate(90deg);\ +}\ +.settings-sub label:before {\ + border-bottom: 1px solid;\ + border-left: 1px solid;\ + content: " ";\ + display: inline-block;\ + height: 8px;\ + margin-bottom: 5px;\ + width: 8px;\ +}\ +.settings-sub {\ + margin-left: 25px;\ +}\ +.settings-tip.settings-sub {\ + padding-left: 32px;\ +}\ +.centeredThreads .opContainer {\ + display: block;\ +}\ +.centeredThreads .postContainer {\ + margin: auto;\ + width: 75%;\ +}\ +.centeredThreads .sideArrows {\ + display: none;\ +}\ +.centre-exp {\ + width: auto !important;\ + clear: both;\ +}\ +.centeredThreads .expandedWebm {\ + float: none;\ +}\ +.centeredThreads .summary {\ + margin-left: 12.5%;\ + display: block;\ +}\ +.centre-exp div.op{\ + display: table;\ +}\ +#yt-preview { position: absolute; }\ +#yt-preview img { display: block; }\ +\ +@media only screen and (max-width: 480px) {\ +#threadWatcher {\ + max-width: none;\ + padding: 3px 0;\ + left: 0;\ + width: 100%;\ + border-left: none;\ + border-right: none;\ +}\ +#watchList {\ + padding: 0 10px;\ +}\ +.btn-row {\ + margin-top: 5px;\ +}\ +.image-expanded .mFileInfo {\ + display: none !important;\ +}\ +.mobile-report {\ + float: right;\ + font-size: 11px;\ + margin-bottom: 3px;\ + margin-left: 10px;\ +}\ +.mobile-report:after {\ + content: "]";\ +}\ +.mobile-report:before {\ + content: "[";\ +}\ +.nws .mobile-report:after {\ + color: #800000;\ +}\ +.nws .mobile-report:before {\ + color: #800000;\ +}\ +.ws .mobile-report {\ + color: #34345C;\ +}\ +.nws .mobile-report {\ + color:#0000EE;\ +}\ +.reply .mobile-report {\ + margin: 5px 5px 0 5px;\ +}\ +.postLink .mobileHideButton {\ + margin-right: 3px;\ +}\ +.board .mobile-hr-hidden {\ + margin-top: 10px !important;\ +}\ +.board > .mobileHideButton {\ + margin-top: -20px !important;\ +}\ +.board > .mobileHideButton:first-child {\ + margin-top: 10px !important;\ +}\ +.extButton.threadHideButton {\ + float: none;\ + margin: 0;\ + margin-bottom: 5px;\ +}\ +.mobile-post-hidden {\ + display: none;\ +}\ +#toggleMsgBtn {\ + display: none;\ +}\ +.mobile-tu-status {\ + height: 20px;\ + line-height: 20px;\ +}\ +.mobile-tu-show {\ + width: 150px;\ + margin: auto;\ + display: block;\ + text-align: center;\ +}\ +.button input {\ + margin: 0 3px 0 0;\ + position: relative;\ + top: -2px;\ + border-radius: 0;\ + height: 10px;\ + width: 10px;\ +}\ +.UIPanel > div {\ + width: 320px;\ + margin-left: -160px;\ +}\ +.UIPanel .export-field {\ + width: 300px;\ +}\ +.yotsuba_new #quote-preview.highlight,\ +#quote-preview {\ + border-width: 1px !important;\ +}\ +.yotsuba_new #quote-preview.highlight {\ + border-color: #D9BFB7 !important;\ +}\ +#quickReply input[type="text"],\ +#quickReply textarea,\ +.extPanel input[type="text"],\ +.extPanel textarea {\ + font-size: 16px;\ +}\ +#quickReply {\ + position: absolute;\ + left: 50%;\ + margin-left: -154px;\ +}\ +}\ +'; + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = css; + document.head.appendChild(style); +}; + +Main.init(); diff --git a/js/extension-test.js b/js/extension-test.js new file mode 100644 index 0000000..40fd710 --- /dev/null +++ b/js/extension-test.js @@ -0,0 +1,11227 @@ +/******************************** + * * + * 4chan Extension * + * * + ********************************/ + +/** + * Helpers + */ +var $ = {}; + +$.id = function(id) { + return document.getElementById(id); +}; + +$.cls = function(klass, root) { + return (root || document).getElementsByClassName(klass); +}; + +$.byName = function(name) { + return document.getElementsByName(name); +}; + +$.tag = function(tag, root) { + return (root || document).getElementsByTagName(tag); +}; + +$.el = function(tag) { + return document.createElement(tag); +}; + +$.qs = function(sel, root) { + return (root || document).querySelector(sel); +}; + +$.qsa = function(selector, root) { + return (root || document).querySelectorAll(selector); +}; + +$.extend = function(destination, source) { + for (var key in source) { + destination[key] = source[key]; + } +}; + +$.on = function(n, e, h) { + n.addEventListener(e, h, false); +}; + +$.off = function(n, e, h) { + n.removeEventListener(e, h, false); +}; + +if (!document.documentElement.classList) { + $.hasClass = function(el, klass) { + return (' ' + el.className + ' ').indexOf(' ' + klass + ' ') != -1; + }; + + $.addClass = function(el, klass) { + el.className = (el.className === '') ? klass : el.className + ' ' + klass; + }; + + $.removeClass = function(el, klass) { + el.className = (' ' + el.className + ' ').replace(' ' + klass + ' ', ''); + }; +} +else { + $.hasClass = function(el, klass) { + return el.classList.contains(klass); + }; + + $.addClass = function(el, klass) { + el.classList.add(klass); + }; + + $.removeClass = function(el, klass) { + el.classList.remove(klass); + }; +} + +$.get = function(url, callbacks, headers) { + var key, xhr; + + xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + if (callbacks) { + for (key in callbacks) { + xhr[key] = callbacks[key]; + } + } + if (headers) { + for (key in headers) { + xhr.setRequestHeader(key, headers[key]); + } + } + xhr.send(null); + return xhr; +}; + +$.xhr = function(method, url, callbacks, data) { + var key, xhr, form; + + xhr = new XMLHttpRequest(); + + xhr.open(method, url, true); + + if (callbacks) { + for (key in callbacks) { + xhr[key] = callbacks[key]; + } + } + + if (data) { + form = new FormData(); + for (key in data) { + form.append(key, data[key]); + } + data = form; + } + else { + data = null; + } + + xhr.send(data); + + return xhr; +}; + +$.fit = function(w, h, maxW, maxH) { + var r, outW, outH; + + r = w / h; + + if (w > maxW) { + outW = maxW; + outH = Math.round(outW / r); + + if (outH > maxH) { + outH = maxH; + outW = Math.round(outH * r); + } + } + else if (h > maxH) { + outH = maxH; + outW = Math.round(outH * r); + + if (outW > maxW) { + outW = maxW; + outH = Math.round(outW / r); + } + } + else { + outW = w; + outH = h; + } + + return [outW, outH]; +}; + +$.ago = function(timestamp) { + var delta, count, head, tail; + + delta = Date.now() / 1000 - timestamp; + + if (delta < 1) { + return 'moments ago'; + } + + if (delta < 60) { + return (0 | delta) + ' seconds ago'; + } + + if (delta < 3600) { + count = 0 | (delta / 60); + + if (count > 1) { + return count + ' minutes ago'; + } + else { + return 'one minute ago'; + } + } + + if (delta < 86400) { + count = 0 | (delta / 3600); + + if (count > 1) { + head = count + ' hours'; + } + else { + head = 'one hour'; + } + + tail = 0 | (delta / 60 - count * 60); + + if (tail > 1) { + head += ' and ' + tail + ' minutes'; + } + + return head + ' ago'; + } + + count = 0 | (delta / 86400); + + if (count > 1) { + head = count + ' days'; + } + else { + head = 'one day'; + } + + tail = 0 | (delta / 3600 - count * 24); + + if (tail > 1) { + head += ' and ' + tail + ' hours'; + } + + return head + ' ago'; +}; + +$.hash = function(str) { + var i, j, msg = 0; + for (i = 0, j = str.length; i < j; ++i) { + msg = ((msg << 5) - msg) + str.charCodeAt(i); + } + return msg; +}; + +$.prettySeconds = function(fs) { + var m, s; + + m = Math.floor(fs / 60); + s = Math.round(fs - m * 60); + + return [ m, s ]; +}; + +$.docEl = document.documentElement; + +$.cache = {}; + +/** + * Parser + */ +var Parser = { + tipTimeout: null +}; + +Parser.init = function() { + var o, a, h, m, tail, staticPath; + + if (Config.filter || Config.linkify || Config.embedSoundCloud + || Config.embedYouTube || Main.hasMobileLayout) { + this.needMsg = true; + } + + staticPath = '//s.4cdn.org/image/'; + + tail = window.devicePixelRatio >= 2 ? '@2x.gif' : '.gif'; + + this.icons = { + admin: staticPath + 'adminicon' + tail, + founder: staticPath + 'foundericon' + tail, + mod: staticPath + 'modicon' + tail, + dev: staticPath + 'developericon' + tail, + manager: staticPath + 'managericon' + tail, + del: staticPath + 'filedeleted-res' + tail + }; + + this.prettify = typeof prettyPrint == 'function'; + + this.customSpoiler = {}; + + if (Config.localTime) { + if (o = (new Date()).getTimezoneOffset()) { + a = Math.abs(o); + h = (0 | (a / 60)); + + this.utcOffset = 'Timezone: UTC' + (o < 0 ? '+' : '-') + + h + ((m = a - h * 60) ? (':' + m) : ''); + } + else { + this.utcOffset = 'Timezone: UTC'; + } + + this.weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + } + + if (Main.tid) { + this.trackedReplies = this.getTrackedReplies(Main.board, Main.tid); + + if (this.trackedReplies) { + this.touchTrackedReplies(Main.tid); + } + else { + this.trackedReplies = {}; + } + + this.pruneTrackedReplies(); + } + + this.postMenuIcon = Main.hasMobileLayout ? '...' : '▶'; +}; + +Parser.getTrackedReplies = function(board, tid) { + var tracked = null; + + if (tracked = localStorage.getItem('4chan-track-' + board + '-' + tid)) { + tracked = JSON.parse(tracked); + } + + return tracked; +}; + +Parser.saveTrackedReplies = function(tid, replies) { + var key = '4chan-track-' + Main.board + '-' + tid; + + localStorage.setItem(key, JSON.stringify(replies)); + + //StorageSync.sync(key); +}; + +Parser.touchTrackedReplies = function(tid) { + var tracked, key; + + key = '4chan-track-' + Main.board + '-ts'; + + if (tracked = localStorage.getItem(key)) { + tracked = JSON.parse(tracked); + } + else { + tracked = {}; + } + + tracked[tid] = 0 | (Date.now() / 1000); + localStorage.setItem(key, JSON.stringify(tracked)); +}; + +Parser.pruneTrackedReplies = function() { + var tid, tracked, now, thres, ttl, pfx, flag; + + pfx = '4chan-track-' + Main.board + '-'; + + if (tracked = localStorage.getItem(pfx + 'ts')) { + ttl = 259200; + now = 0 | (Date.now() / 1000); + thres = now - ttl; + + flag = false; + + tracked = JSON.parse(tracked); + + if (Main.tid && tracked[Main.tid]) { + tracked[Main.tid] = now; + flag = true; + } + + for (tid in tracked) { + if (tracked[tid] <= thres) { + flag = true; + delete tracked[tid]; + localStorage.removeItem(pfx + tid); + //StorageSync.queue.push(pfx + tid); + } + } + + if (flag) { + localStorage.removeItem(pfx + 'ts'); + + for (tid in tracked) { + localStorage.setItem(pfx + 'ts', JSON.stringify(tracked)); + break; + } + + //StorageSync.queue.push(pfx + 'ts'); + } + + //StorageSync.send(); + } +}; + +Parser.parseThreadJSON = function(data) { + var thread; + + try { + thread = JSON.parse(data).posts; + } + catch (e) { + console.log(e); + thread = []; + } + + return thread; +}; + +Parser.parseCatalogJSON = function(data) { + var catalog; + + try { + catalog = JSON.parse(data); + } + catch (e) { + console.log(e); + catalog = []; + } + + return catalog; +}; + +Parser.setCustomSpoiler = function(board, val) { + var s; + if (!this.customSpoiler[board] && (val = parseInt(val))) { + if (board == Main.board && (s = $.cls('imgspoiler')[0])) { + this.customSpoiler[board] = + s.firstChild.src.match(/spoiler(-[a-z0-9]+)\.png$/)[1]; + } + else { + this.customSpoiler[board] = '-' + board + + (Math.floor(Math.random() * val) + 1); + } + } +}; + +Parser.buildPost = function(thread, board, pid) { + var i, j, uid, el = null; + + for (i = 0; j = thread[i]; ++i) { + if (j.no != pid) { + continue; + } + + if (!Config.revealSpoilers && thread[0].custom_spoiler) { + Parser.setCustomSpoiler(board, thread[0].custom_spoiler); + } + + el = Parser.buildHTMLFromJSON(j, board, false, true).lastElementChild; + + if (Config.IDColor && (uid = $.cls('posteruid', el)[Main.hasMobileLayout ? 0 : 1])) { + IDColor.applyRemote(uid.firstElementChild); + } + } + + return el; +}; + +Parser.decodeSpecialChars = function(str) { + return str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>'); +}; + +Parser.encodeSpecialChars = function(str) { + return str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +}; + +Parser.onDateMouseOver = function(el) { + if (Parser.tipTimeout) { + clearTimeout(Parser.tipTimeout); + Parser.tipTimeout = null; + } + + Parser.tipTimeout = setTimeout(Tip.show, 500, el, $.ago(+el.getAttribute('data-utc'))); +}; + +Parser.onTipMouseOut = function() { + if (Parser.tipTimeout) { + clearTimeout(Parser.tipTimeout); + Parser.tipTimeout = null; + } +}; + +Parser.onUIDMouseOver = function(el) { + var p; + + if (!$.hasClass(el.parentNode, 'posteruid')) { + return; + } + + if (!Main.tid) { + p = el.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode; + + if (!$.hasClass(p, 'tExpanded')) { + return; + } + } + + if (Parser.tipTimeout) { + clearTimeout(Parser.tipTimeout); + Parser.tipTimeout = null; + } + + Parser.tipTimeout = setTimeout(Parser.showUIDCount, 500, el, el.textContent); +}; + +Parser.showUIDCount = function(t, uid) { + var i, el, nodes, count, msg; + + count = 0; + nodes = $.qsa('.postInfo .hand'); + + for (i = 0; el = nodes[i]; ++i) { + if (el.textContent === uid) { + ++count; + } + } + + msg = count + ' post' + (count != 1 ? 's' : '') + ' by this ID'; + + Tip.show(t, msg); +}; + +Parser.buildHTMLFromJSON = function(data, board, standalone, fromQuote) { + var + container = document.createElement('div'), + isOP = false, + + userId, + fileDims = '', + imgSrc = '', + fileInfo = '', + fileHtml = '', + fileThumb, + filePath, + fileName, + fileSpoilerTip = '"', + size = '', + fileClass = '', + shortFile = '', + longFile = '', + tripcode = '', + capcodeStart = '', + capcodeClass = '', + capcode = '', + flag, + highlight = '', + emailStart = '', + emailEnd = '', + name, mName, + subject, + noLink, + quoteLink, + replySpan = '', + noFilename, + decodedFilename, + mobileLink = '', + postType = 'reply', + summary = '', + postCountStr, + resto, + capcode_replies = '', + threadIcons = '', + boardTag = '', + needFileTip = false, + + i, q, href, quotes, tmp, + + imgDir; + /* + if (board !== 'f') { + if (data.no % 3 > 2) { + imgDir = '//is.4chan.org/' + board; + } + else { + imgDir = '//is2.4chan.org/' + board; + } + } + else {*/ + imgDir = '//i.4cdn.org/' + board; + //} + + if (!data.resto) { + isOP = true; + + if (standalone) { + if (data.replies > 0) { + tmp = data.replies + ' Repl' + (data.replies > 1 ? 'ies' : 'y'); + if (data.images > 0) { + tmp += ' / ' + data.images + ' Image' + (data.images > 1 ? 's' : ''); + } + } + else { + tmp = ''; + } + + mobileLink = ''; + postType = 'op'; + replySpan = '  [Reply]'; + } + + if (board != Main.board) { + boardTag = '/' + board + '/ '; + } + + resto = data.no; + } + else { + resto = data.resto; + } + + + if (!Main.tid || board != Main.board) { + noLink = '//boards.' + $L.d(board) + '/' + board + '/thread/' + resto + '#p' + data.no; + quoteLink = '//boards.' + $L.d(board) + '/' + board + '/thread/' + resto + '#q' + data.no; + } + else { + noLink = '#p' + data.no; + quoteLink = 'javascript:quote(\'' + data.no + '\')'; + } + + if (!data.capcode && data.id) { + userId = ' (ID: ' + + data.id + ') '; + } + else { + userId = ''; + } + + switch (data.capcode) { + case 'admin_highlight': + highlight = ' highlightPost'; + /* falls through */ + case 'admin': + capcodeStart = ' ## Admin'; + capcodeClass = ' capcodeAdmin'; + + capcode = ' '; + break; + case 'mod': + capcodeStart = ' ## Mod'; + capcodeClass = ' capcodeMod'; + + capcode = ' '; + break; + case 'developer': + capcodeStart = ' ## Developer'; + capcodeClass = ' capcodeDeveloper'; + + capcode = ' '; + break; + case 'manager': + capcodeStart = ' ## Manager'; + capcodeClass = ' capcodeManager'; + + capcode = ' '; + break; + case 'founder': + capcodeStart = ' ## Founder'; + capcodeClass = ' capcodeAdmin'; + + capcode = ' '; + break; + case 'verified': + capcodeStart = ' ## Verified'; + capcodeClass = ' capcodeVerified'; + + capcode = ''; + break; + } + + if (data.email) { + emailStart = ''; + emailEnd = ''; + } + + if (data.flag_name) { + flag = ' '; + } + else if (data.country_name) { + flag = ' '; + } + else { + flag = ''; + } + + if (data.filedeleted) { + fileHtml = '
    File deleted.
    '; + } + else if (data.ext) { + decodedFilename = Parser.decodeSpecialChars(data.filename); + + shortFile = longFile = data.filename + data.ext; + + if (decodedFilename.length > (isOP ? 40 : 30)) { + shortFile = Parser.encodeSpecialChars( + decodedFilename.slice(0, isOP ? 35 : 25) + ) + '(...)' + data.ext; + + needFileTip = true; + } + + if (!data.tn_w && !data.tn_h && data.ext == '.gif') { + data.tn_w = data.w; + data.tn_h = data.h; + } + if (data.fsize >= 1048576) { + size = ((0 | (data.fsize / 1048576 * 100 + 0.5)) / 100) + ' M'; + } + else if (data.fsize > 1024) { + size = (0 | (data.fsize / 1024 + 0.5)) + ' K'; + } + else { + size = data.fsize + ' '; + } + + if (data.spoiler) { + if (!Config.revealSpoilers) { + fileName = 'Spoiler Image'; + fileSpoilerTip = '" title="' + longFile + '"'; + fileClass = ' imgspoiler'; + + fileThumb = '//s.4cdn.org/image/spoiler' + + (Parser.customSpoiler[board] || '') + '.png'; + data.tn_w = 100; + data.tn_h = 100; + + noFilename = true; + } + else { + fileName = shortFile; + } + } + else { + fileName = shortFile; + } + + if (!fileThumb) { + fileThumb = '//i.4cdn.org/' + board + '/' + data.tim + 's.jpg'; + } + + fileDims = data.ext == '.pdf' ? 'PDF' : data.w + 'x' + data.h; + + if (board != 'f') { + filePath = imgDir + '/' + data.tim + data.ext; + + imgSrc = '' + size + 'B' + + '
    ' + + size + 'B ' + data.ext.slice(1).toUpperCase() + + '
    '; + + fileInfo = '
    ' + + fileName + ' (' + size + 'B, ' + fileDims + ')
    '; + } + else { + filePath = imgDir + '/' + data.filename + data.ext; + + fileDims += ', ' + data.tag; + + fileInfo = '
    File: ' + + data.filename + '.swf (' + size + 'B, ' + fileDims + ')
    '; + } + + fileHtml = '
    ' + + fileInfo + imgSrc + '
    '; + } + + if (data.trip) { + tripcode = ' ' + data.trip + ''; + } + + name = data.name || ''; + + if (Main.hasMobileLayout && name.length > 30) { + mName = '' + + Parser.truncate(name, 30) + '(...) '; + } + else { + mName = '' + name + ' '; + } + + if (isOP) { + if (data.capcode_replies) { + capcode_replies = Parser.buildCapcodeReplies(data.capcode_replies, board, data.no); + } + + if (fromQuote && data.replies) { + postCountStr = data.replies + ' repl' + (data.replies > 1 ? 'ies' : 'y'); + + if (data.images) { + postCountStr += ' and ' + data.images + ' image' + + (data.images > 1 ? 's' : ''); + } + + summary = '' + postCountStr + '.'; + } + + if (data.sticky) { + threadIcons += 'Sticky '; + } + + if (data.closed) { + if (data.archived) { + threadIcons += 'Archived '; + } + else { + threadIcons += 'Closed '; + } + } + + if (data.sub === undefined) { + subject = ' '; + } + else if (Main.hasMobileLayout && data.sub.length > 30) { + subject = '' + + Parser.truncate(data.sub, 30) + '(...) '; + } + else { + subject = '' + data.sub + ' '; + } + } + else { + subject = ''; + } + + container.className = 'postContainer ' + postType + 'Container'; + container.id = 'pc' + data.no; + + if (data.xa24) { + container.className += ' p-xa24-' + data.xa24; + } + + container.innerHTML = + (isOP ? '' : '
    >>
    ') + + '
    ' + + '' + + (isOP ? fileHtml : '') + + '' + + (isOP ? '' : fileHtml) + + '
    ' + + (data.com || '') + capcode_replies + summary + '
    ' + + '
    ' + mobileLink; + + if (!Main.tid || board != Main.board) { + quotes = container.getElementsByClassName('quotelink'); + for (i = 0; q = quotes[i]; ++i) { + href = q.getAttribute('href'); + if (href.charAt(0) != '/') { + q.href = '//boards.' + $L.d(board) + '/' + board + '/thread/' + resto + href; + } + } + } + + return container; +}; + +Parser.truncate = function(str, len) { + str = str.replace(',', ','); + str = Parser.decodeSpecialChars(str); + str = str.slice(0, len); + str = Parser.encodeSpecialChars(str); + return str; +}; + +Parser.buildCapcodeReplies = function(replies, board, tid) { + var i, capcode, id, html, map, post_ids, prelink, pretext; + + map = { + admin: 'Administrator', + mod: 'Moderator', + developer: 'Developer', + manager: 'Manager' + }; + + if (board != Main.board) { + prelink = '/' + board + '/thread/'; + pretext = '>>>/' + board + '/'; + } + else { + prelink = ''; + pretext = '>>'; + } + + html = '

    '; + + for (capcode in replies) { + html += '' + map[capcode] + ' Replies: '; + + post_ids = replies[capcode]; + + for (i = 0; id = post_ids[i]; ++i) { + html += '' + pretext + id + ' '; + } + } + + return html + ''; +}; + +Parser.parseBoard = function() { + var i, threads = document.getElementsByClassName('thread'); + + for (i = 0; threads[i]; ++i) { + Parser.parseThread(threads[i].id.slice(1)); + } +}; + +Parser.parseThread = function(tid, offset, limit) { + var i, j, thread, posts, pi, el, frag, summary, omitted, key, filtered, cnt; + + thread = $.id('t' + tid); + posts = thread.getElementsByClassName('post'); + + if (!offset) { + pi = document.getElementById('pi' + tid); + + if (!Main.tid) { + if (Config.filter) { + filtered = Filter.exec( + thread, + pi, + document.getElementById('m' + tid), + tid + ); + } + + if (Config.threadHiding && !filtered) { + if (!Main.hasMobileLayout) { + el = document.createElement('span'); + el.innerHTML = 'H'; + posts[0].insertBefore(el, posts[0].firstChild); + el.id = 'sa' + tid; + } + if (ThreadHiding.hidden[tid]) { + ThreadHiding.hidden[tid] = Main.now; + ThreadHiding.hide(tid); + } + } + + if (ThreadExpansion.enabled + && (summary = $.cls('summary', thread)[0])) { + frag = document.createDocumentFragment(); + + omitted = summary.cloneNode(true); + omitted.className = ''; + summary.textContent = ''; + + el = document.createElement('img'); + el.className = 'extButton expbtn'; + el.title = 'Expand thread'; + el.alt = '+'; + el.setAttribute('data-cmd', 'expand'); + el.setAttribute('data-id', tid); + el.src = Main.icons.plus; + frag.appendChild(el); + + frag.appendChild(omitted); + + el = document.createElement('span'); + el.style.display = 'none'; + el.textContent = 'Showing all replies.'; + frag.appendChild(el); + + summary.appendChild(frag); + } + } + + if (Main.tid && Config.threadWatcher) { + el = document.createElement('img'); + + if (ThreadWatcher.watched[key = tid + '-' + Main.board]) { + el.src = Main.icons.watched; + el.setAttribute('data-active', '1'); + } + else { + el.src = Main.icons.notwatched; + } + + el.className = 'extButton wbtn wbtn-' + key; + el.setAttribute('data-cmd', 'watch'); + el.setAttribute('data-id', tid); + el.alt = 'W'; + el.title = 'Add to watch list'; + + cnt = $.cls('navLinks'); + + for (i = 1; i < 3 && (j = cnt[i]); ++i) { + frag = document.createDocumentFragment(); + frag.appendChild(document.createTextNode('[')); + frag.appendChild(el.cloneNode(true)); + frag.appendChild(document.createTextNode('] ')); + j.insertBefore(frag, j.firstChild); + } + } + } + + j = offset ? offset < 0 ? posts.length + offset : offset : 0; + limit = limit ? j + limit : posts.length; + + if (Main.isMobileDevice && Config.quotePreview) { + for (i = j; i < limit; ++i) { + Parser.parseMobileQuotelinks(posts[i]); + } + } + + if (Parser.trackedReplies) { + for (i = j; i < limit; ++i) { + Parser.parseTrackedReplies(posts[i]); + } + } + + for (i = j; i < limit; ++i) { + Parser.parsePost(posts[i].id.slice(1), tid); + } + + if (offset) { + if (Parser.prettify) { + for (i = j; i < limit; ++i) { + Parser.parseMarkup(posts[i]); + } + } + if (window.math_tags) { + if (window.MathJax) { + for (i = j; i < limit; ++i) { + if (Parser.postHasMath(posts[i])) { + window.cleanWbr(posts[i]); + } + MathJax.Hub.Queue(['Typeset', MathJax.Hub, posts[i]]); + } + } + else { + for (i = j; i < limit; ++i) { + if (Parser.postHasMath(posts[i])) { + window.loadMathJax(); + } + } + } + } + } + + UA.dispatchEvent('4chanParsingDone', { threadId: tid, offset: j, limit: limit }); +}; + +Parser.postHasMath = function(el) { + return /\[(?:eqn|math)\]/.test(el.innerHTML); +}; + +Parser.parseMathOne = function(node) { + if (window.MathJax) { + MathJax.Hub.Queue(['Typeset', MathJax.Hub, node]); + } + else if (Parser.postHasMath(node)) { + window.loadMathJax(); + } +}; + +Parser.parseTrackedReplies = function(post) { + var i, link, quotelinks; + + quotelinks = $.cls('quotelink', post); + + for (i = 0; link = quotelinks[i]; ++i) { + if (Parser.trackedReplies[link.textContent]) { + link.className += ' ql-tracked'; + link.textContent += ' (You)'; + Parser.hasYouMarkers = true; + } + } +}; + +Parser.parseMobileQuotelinks = function(post) { + var i, link, quotelinks, t, el; + + quotelinks = $.cls('quotelink', post); + + for (i = 0; link = quotelinks[i]; ++i) { + t = link.getAttribute('href').match(/(?:\/([a-z0-9]+)\/thread\/)?([0-9]+)?#p([0-9]+)$/); + + if (!t) { + continue; + } + + el = document.createElement('a'); + el.href = link.href; + el.textContent = ' #'; + el.className = 'quoteLink'; + + link.parentNode.insertBefore(el, link.nextSibling); + } +}; + +Parser.parseMarkup = function(post) { + var i, pre, el; + + if ((pre = post.getElementsByClassName('prettyprint'))[0]) { + for (i = 0; el = pre[i]; ++i) { + el.innerHTML = window.prettyPrintOne(el.innerHTML); + } + } +}; + +Parser.revealImageSpoiler = function(fileThumb) { + var img, isOP, filename, finfo, txt; + + img = fileThumb.firstElementChild; + fileThumb.removeChild(img); + img.removeAttribute('style'); + isOP = $.hasClass(fileThumb.parentNode.parentNode, 'op'); + img.style.maxWidth = img.style.maxHeight = isOP ? '250px' : '125px'; + img.src = '//i.4cdn.org' + + (fileThumb.pathname.replace(/\/([0-9]+).+$/, '/$1s.jpg')); + + filename = fileThumb.previousElementSibling; + finfo = filename.title.split('.'); + + if (finfo[0].length > (isOP ? 40 : 30)) { + txt = finfo[0].slice(0, isOP ? 35 : 25) + '(...)' + finfo[1]; + } + else { + txt = filename.title; + filename.removeAttribute('title'); + } + + filename.firstElementChild.innerHTML = txt; + fileThumb.insertBefore(img, fileThumb.firstElementChild); +}; + +Parser.parsePost = function(pid, tid) { + var hasMobileLayout, cnt, el, pi, file, msg, filtered, uid; + + hasMobileLayout = Main.hasMobileLayout; + + if (!tid) { + pi = pid.getElementsByClassName('postInfo')[0]; + pid = pi.id.slice(2); + } + else { + pi = document.getElementById('pi' + pid); + } + + if (Parser.needMsg) { + msg = document.getElementById('m' + pid); + } + + el = document.createElement('a'); + el.href = '#'; + el.className = 'postMenuBtn'; + el.title = 'Post menu'; + el.setAttribute('data-cmd', 'post-menu'); + el.textContent = Parser.postMenuIcon; + + if (hasMobileLayout) { + cnt = document.getElementById('pim' + pid); + cnt.insertBefore(el, cnt.firstElementChild); + } + else { + pi.appendChild(el); + } + + if (tid) { + if (pid != tid) { + if (Config.filter) { + filtered = Filter.exec(pi.parentNode, pi, msg); + } + + if (!filtered && ReplyHiding.hidden[pid]) { + ReplyHiding.hidden[pid] = Main.now; + ReplyHiding.hide(pid); + } + /* + if (ReplyHiding.hiddenR[pid]) { + ReplyHiding.hideR(pid, pid); + } + else if (ReplyHiding.hasR) { + if (ppid = ReplyHiding.shouldToggleR(msg)) { + ReplyHiding.hideR(pid, ppid); + } + } + */ + } + + if (Config.backlinks) { + Parser.parseBacklinks(pid, tid); + } + + if (Main.isOekakiBoard && Main.tid) { + Parser.addOekakiEditLink(pid, tid); + } + } + + if (IDColor.enabled && (uid = $.cls('posteruid', pi.parentNode)[hasMobileLayout ? 0 : 1])) { + IDColor.apply(uid.firstElementChild); + } + + if (Config.linkify) { + Linkify.exec(msg); + } + + if (Config.embedSoundCloud) { + Media.parseSoundCloud(msg); + } + + if (Config.embedYouTube || hasMobileLayout) { + Media.parseYouTube(msg); + } + + if (Config.revealSpoilers + && (file = document.getElementById('f' + pid)) + && (file = file.children[1]) + ) { + if ($.hasClass(file, 'imgspoiler')) { + Parser.revealImageSpoiler(file); + } + } + + if (Config.localTime) { + if (hasMobileLayout) { + el = pi.parentNode.getElementsByClassName('dateTime')[0]; + el.firstChild.nodeValue + = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)) + ' '; + } + else { + el = pi.getElementsByClassName('dateTime')[0]; + //el.title = this.utcOffset; + el.textContent + = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)); + } + } +}; + +Parser.addOekakiEditLink = function(pid, tid) { + var cnt, el, a; + + cnt = $.id('fT' + pid); + + if (!cnt) { + return; + } + + a = cnt.firstElementChild; + + if (!a.href || !/\.(png|jpg)$/.test(a.href)) { + return; + } + + el = $.el('small'); + el.innerHTML = ' Edit'; + cnt.appendChild(el); +}; + +Parser.getLocaleDate = function(date) { + return ('0' + (1 + date.getMonth())).slice(-2) + '/' + + ('0' + date.getDate()).slice(-2) + '/' + + ('0' + date.getFullYear()).slice(-2) + '(' + + this.weekdays[date.getDay()] + ')' + + ('0' + date.getHours()).slice(-2) + ':' + + ('0' + date.getMinutes()).slice(-2) + ':' + + ('0' + date.getSeconds()).slice(-2); +}; + +Parser.parseBacklinks = function(pid, tid) { + var i, j, msg, backlinks, linklist, ids, target, bl, el, href; + + msg = document.getElementById('m' + pid); + + if (!(backlinks = msg.getElementsByClassName('quotelink'))) { + return; + } + + linklist = {}; + + for (i = 0; j = backlinks[i]; ++i) { + // [tid, pid] + ids = j.getAttribute('href').split('#p'); + + if (!ids[1]) { + continue; + } + + if (ids[1] == tid) { + j.textContent += ' (OP)'; + } + + if (!(target = document.getElementById('pi' + ids[1]))) { + if (Main.tid && j.textContent.charAt(2) != '>' ) { + j.textContent += ' →'; + } + continue; + } + + // Already processed? + if (linklist[ids[1]]) { + continue; + } + + linklist[ids[1]] = true; + + // Backlink node + bl = document.createElement('span'); + + if (!Main.tid) { + href = 'thread/' + tid + '#p' + pid; + } + else { + href = '#p' + pid; + } + + if (!Main.hasMobileLayout) { + bl.innerHTML = '>>' + pid + ' '; + } + else { + bl.innerHTML = '>>' + pid + + ' # '; + } + + // Backlinks container + if (!(el = document.getElementById('bl_' + ids[1]))) { + el = document.createElement('div'); + el.id = 'bl_' + ids[1]; + el.className = 'backlink'; + + if (Main.hasMobileLayout) { + el.className = 'backlink mobile'; + target = document.getElementById('p' + ids[1]); + } + + target.appendChild(el); + } + + el.appendChild(bl); + } +}; + +Parser.buildSummary = function(tid, oRep, oImg) { + var el; + + if (oRep) { + oRep = oRep + ' repl' + (oRep > 1 ? 'ies' : 'y'); + } + else { + return null; + } + + if (oImg) { + oImg = ' and ' + oImg + ' image' + (oImg > 1 ? 's' : ''); + } + else { + oImg = ''; + } + + el = document.createElement('span'); + el.className = 'summary desktop'; + el.innerHTML = oRep + oImg + + ' omitted. Click here to view.'; + + return el; +}; + +var OgvCtrl = { + ogv: null, + cnt: null, + ctrl: {}, + + seeking: false, + visible: false, + tick: null, + + attach: function(ogv) { + this.detach(); + ogv.parentNode.appendChild(this.cnt); + $.on(ogv, 'mouseup', this.toggleCtrl); + this.ogv = ogv; + }, + + detach: function() { + if (!this.ogv) { + return; + } + this.ogv.stop(); + $.off(this.ogv, 'mouseup', this.toggleCtrl); + this.ctrl.play.classList.remove('ogv-toggled'); + this.ctrl.mute.classList.remove('ogv-toggled'); + this.hideCtrl(); + this.ogv = null; + this.seeking = false; + this.cnt.remove(); + }, + + init: function() { + if (this.cnt) { + return; + } + + let cnt = $.el('div'); + cnt.className = 'ogv-ctrl'; + + let el = $.el('div'); + el.className = 'ogv-btn'; + el.innerHTML = ''; + $.on(el, 'click', this.togglePlay, false); + this.ctrl.play = el; + cnt.appendChild(el); + + el = $.el('input'); + el.className = 'ogv-seek'; + el.type = 'range'; + el.min = 0; + el.value = 0; + el.max = 100; + el.step = 0.1; + $.on(el, 'change', this.onSeek, false); + $.on(el, 'mousedown', this.toggleSeek, false); + $.on(el, 'mouseup', this.toggleSeek, false); + this.ctrl.seek = el; + cnt.appendChild(el); + + el = $.el('div'); + el.className = 'ogv-ts'; + el.textContent = '0:00 / 0:00'; + this.ctrl.ts = el; + cnt.appendChild(el); + + el = $.el('div'); + el.className = 'ogv-btn'; + el.innerHTML = ''; + $.on(el, 'click', this.toggleMute, false); + this.ctrl.mute = el; + cnt.appendChild(el); + + el = $.el('input'); + el.className = 'ogv-vol'; + el.type = 'range'; + el.min = 0; + el.value = 50; + el.step = 0.1; + el.max = 100; + $.on(el, 'input', this.onVolInput, false); + this.ctrl.vol = el; + cnt.appendChild(el); + + el = $.el('div'); + el.className = 'ogv-btn'; + el.innerHTML = ''; + $.on(el, 'click', this.toggleFullscreen, false); + this.ctrl.fs = el; + cnt.appendChild(el); + + this.cnt = cnt; + }, + + onPlayEnd: function() { + if (OgvCtrl.ogv.seekable.length) { + OgvCtrl.ogv.currentTime = 0; + } + else { + OgvCtrl.ogv.stop(); + } + OgvCtrl.ogv.play(); + }, + + toggleCtrl: function() { + if (OgvCtrl.visible) { + OgvCtrl.hideCtrl(); + } + else { + OgvCtrl.cnt.style.display = 'flex'; + OgvCtrl.setTickTimeout(); + OgvCtrl.updateTimes(); + OgvCtrl.visible = true; + } + }, + + hideCtrl: function() { + OgvCtrl.cnt.style.display = 'none'; + OgvCtrl.clearTickTimeout(); + OgvCtrl.visible = false; + }, + + toggleSeek: function() { + OgvCtrl.seeking = !OgvCtrl.seeking; + }, + + seekTick: function() { + OgvCtrl.setTickTimeout(); + OgvCtrl.updateTimes(); + }, + + setTickTimeout: function() { + OgvCtrl.tick = setTimeout(OgvCtrl.seekTick, 500); + }, + + clearTickTimeout: function() { + clearTimeout(OgvCtrl.tick); + OgvCtrl.tick = null; + }, + + updateTimes: function() { + if (!OgvCtrl.ogv.duration) { + return; + } + + if (!OgvCtrl.seeking) { + OgvCtrl.ctrl.seek.value = ((OgvCtrl.ogv.currentTime / OgvCtrl.ogv.duration) * 100).toFixed(2); + } + + let dm = Math.floor(OgvCtrl.ogv.duration / 60); + let ds = Math.floor(OgvCtrl.ogv.duration - dm * 60); + + let m = Math.floor(OgvCtrl.ogv.currentTime / 60); + let s = Math.floor(OgvCtrl.ogv.currentTime - m * 60); + + OgvCtrl.ctrl.ts.textContent = `${m}:${s.toString().padStart(2, '0')} / ${dm}:${ds.toString().padStart(2, '0')}`; + }, + + togglePlay: function() { + if (OgvCtrl.ogv.paused) { + OgvCtrl.ogv.play(); + } + else { + OgvCtrl.ogv.pause(); + } + OgvCtrl.ctrl.play.classList.toggle('ogv-toggled'); + }, + + onSeek: function() { + OgvCtrl.ogv.currentTime = (this.value / 100) * OgvCtrl.ogv.duration; + }, + + toggleMute: function() { + OgvCtrl.ogv.muted = !OgvCtrl.ogv.muted; + OgvCtrl.ctrl.mute.classList.toggle('ogv-toggled'); + }, + + onVolInput: function() { + OgvCtrl.ogv.volume = this.value / 100; + }, + + toggleFullscreen: function() { + if (document.fullscreenElement) { + document.exitFullscreen(); + } + else { + OgvCtrl.ogv.parentNode.requestFullscreen(); + } + + OgvCtrl.ctrl.fs.classList.toggle('ogv-toggled'); + } +}; + +/** + * Post Menu + */ +var PostMenu = { + activeBtn: null +}; + +PostMenu.open = function(btn) { + var div, html, pid, board, btnPos, el, href, left, limit, isOP, file; + + if (PostMenu.activeBtn == btn) { + PostMenu.close(); + return; + } + + PostMenu.close(); + + pid = btn.parentNode.id.replace(/^[0-9]*[^0-9]+/, ''); + + board = btn.parentNode.getAttribute('data-board'); + + isOP = !board && !!$.id('t' + pid); + + html = '
    • Report post
    • '; + + if (isOP) { + if (Config.threadHiding && !Main.tid) { + html += '
    • ' + + ($.hasClass($.id('t' + pid), 'post-hidden') ? 'Unhide' : 'Hide') + + ' thread
    • '; + } + if (Config.threadWatcher) { + html += '
    • ' + + (ThreadWatcher.watched[pid + '-' + Main.board] ? 'Remove from' : 'Add to') + + ' watch list
    • '; + } + } + else if (el = $.id('pc' + pid)) { + html += '
    • ' + + ($.hasClass(el, 'post-hidden') ? 'Unhide' : 'Hide') + + ' post
    • '; + } + + if (Main.hasMobileLayout) { + html += '
    • Delete post
    • '; + } + + if (file = $.id('fT' + pid)) { + el = $.cls('fileThumb', file.parentNode)[0]; + + if (el) { + if (/(gif|jpg|png)$/.test(el.href)) { + href = el.href; + } + else { + href = 'http://i.4cdn.org/' + Main.board + '/' + + el.href.match(/\/([0-9]+)m?\..+$/)[1] + 's.jpg'; + } + + if (Main.hasMobileLayout) { + html += '
    • Delete file
    • ' + + '
    • Open original file
    • ' + + '
    • Search image on Google
    • ' + + '
    • Search image on Yandex
    • ' + + '
    • Search image on SauceNAO
    • '; + } + else { + html += '
    • Image search »
    • '; + } + } + } + + if (Config.filter) { + html += '
    • Filter selected text
    • '; + } + + div = document.createElement('div'); + div.id = 'post-menu'; + div.className = 'dd-menu'; + div.innerHTML = html + '
    '; + + btnPos = btn.getBoundingClientRect(); + + div.style.top = btnPos.bottom + 3 + window.pageYOffset + 'px'; + + document.addEventListener('click', PostMenu.close, false); + + $.addClass(btn, 'menuOpen'); + PostMenu.activeBtn = btn; + + UA.dispatchEvent('4chanPostMenuReady', { postId: pid, isOP: isOP, node: div.firstElementChild }); + + document.body.appendChild(div); + + left = btnPos.left + window.pageXOffset; + limit = $.docEl.clientWidth - div.offsetWidth; + + if (left > (limit - 75)) { + div.className += ' dd-menu-left'; + } + + if (left > limit) { + left = limit; + } + + div.style.left = left + 'px'; +}; + +PostMenu.close = function() { + var el; + + if (el = $.id('post-menu')) { + el.parentNode.removeChild(el); + document.removeEventListener('click', PostMenu.close, false); + $.removeClass(PostMenu.activeBtn, 'menuOpen'); + PostMenu.activeBtn = null; + } +}; + +/** + * + */ +var Search = { + xhr: null, + + pageSize: 10, + + maxPages: 10, + + init: function() { + var el; + + if (el = $.id('g-search-form')) { + $.on(el, 'submit', Search.onSearch); + $.on(window, 'hashchange', Search.onHashChanged); + + if (location.host == 'boards.4channel.org') { + Search.initSelector(); + } + + Search.initFromURL(true); + } + }, + + initSelector: function() { + var i, el, nodes, sel, len; + + sel = $.id('js-sf-bf'); + nodes = sel.options; + len = nodes.length; + + for (i = len - 1; i >= 0; i--) { + el = nodes[i]; + if (el.value === '' || $L.d(el.value) === '4chan.org') { + sel.removeChild(el); + } + } + }, + + initFromURL: function(init) { + var self, frag, i, el, nodes, opt; + + self = Search; + + self.query = ''; + self.board = ''; + self.offset = 0; + + frag = window.location.hash; + + if (frag !== '' && frag.length <= 512) { + frag = frag.split('/').slice(1); + + if (frag[0]) { + self.query = decodeURIComponent(frag[0]); + } + else { + self.query = ''; + } + + self.board = frag[1] || ''; + + if (frag[1]) { + if (frag[1] === 'all') { + self.board = ''; + } + else { + self.board = frag[1]; + } + } + + self.offset = self.pageToOffset(0 | frag[2]); + } + + if (init && self.query === '') { + return; + } + + $.id('js-sf-qf').value = self.query; + + el = $.id('js-sf-bf'); + + el.selectedIndex = 0; + + for (i = 0, nodes = el.options; opt = nodes[i]; ++i) { + if (opt.value === self.board) { + el.selectedIndex = i; + break; + } + } + + if (el.selectedIndex === 0 && self.board !== '') { + self.board = ''; + } + + Search.exec(self.query, self.board, self.offset); + }, + + onHashChanged: function() { + Search.initFromURL(); + }, + + pageToOffset: function(p) { + if (p < 1 || p > Search.maxPages) { + p = 1; + } + + return (p - 1) * Search.pageSize; + }, + + offsetToPage: function(o) { + var p = o / Search.pageSize + 1; + + if (p < 1 || p > Search.maxPages) { + p = 1; + } + + return p; + }, + + updateURL: function() { + var self, frags = []; + + self = Search; + + if (self.query !== '') { + frags.push(encodeURIComponent(self.query)); + + if (self.offset > 0) { + if (!self.board || self.board === '') { + frags.push('all'); + } + else { + frags.push(self.board); + } + + frags.push(Search.offsetToPage(self.offset)); + } + else if (self.board) { + frags.push(self.board); + } + } + + if (frags.length) { + window.history.replaceState(null, '', '#/' + frags.join('/')); + } + else { + window.history.replaceState(null, '', + window.location.href.replace(/#.*$/, '') + ); + } + }, + + onSearch: function(e) { + var qf, bf; + + e && e.preventDefault(); + + Search.query = qf = $.id('js-sf-qf').value; + Search.board = bf = $.id('js-sf-bf').value; + + Search.exec(qf, bf, 0); + }, + + exec: function(query, board, offset) { + var self, qs = []; + + self = Search; + + self.toggleSpinner(false); + + self.updateCtrl(false); + + if (self.xhr) { + self.xhr.abort(); + self.xhr = null; + } + + if (query === '') { + return; + } + + qs.push('q=' + encodeURIComponent(query)); + + if (board !== '') { + qs.push('b=' + board); + } + + if (offset) { + qs.push('o=' + (0 | offset)); + } + + qs = qs.join('&'); + + self.query = query; + self.board = board; + self.offset = offset; + + self.updateURL(); + + self.toggleSpinner(true); + + self.xhr = $.get('https://find.' + location.host.replace(/^boards\./, '') + '/api?' + qs, { + onload: self.onLoad, + onerror: self.onError, + withCredentials: true + }); + }, + + onPageClick: function() { + var offset; + + offset = +this.getAttribute('data-o'); + + Search.exec(Search.query, Search.board, offset); + }, + + onLoad: function() { + var data; + + Search.toggleSpinner(false); + + try { + data = JSON.parse(this.responseText); + } + catch (err) { + Search.showError('Something went wrong.'); + console.log(err); + return; + } + + Search.buildResults(data); + }, + + onError: function() { + Search.toggleSpinner(false); + Search.showError('Connection error.'); + }, + + updateCtrl: function(offset, total) { + var cnt, el, el2, maxPage, curPage; + + if (offset === false) { + el = $.id('js-sf-pl'); + + if (el) { + el.parentNode.removeChild(el); + } + + return; + } + + cnt = $.id('js-sf-pl'); + + if (cnt) { + cnt.parentNode.removeChild(cnt); + } + + cnt = $.el('div'); + cnt.id = 'js-sf-pl'; + + if (!Main.hasMobileLayout) { + cnt.className = 'pagelist desktop'; + } + else { + cnt.className = 'mPagelist mobile'; + } + + total = +total; + offset = +offset; + + maxPage = Math.ceil(total / Search.pageSize); + + if (maxPage > Search.maxPages) { + maxPage = Search.maxPages; + } + + curPage = offset / Search.pageSize + 1; + + if (curPage > 1) { + el = $.el('div'); + el.className = 'prev'; + + if (!Main.hasMobileLayout) { + el2 = $.el('input'); + el2.type = 'button'; + el2.value = 'Previous'; + } + else { + el2 = $.el('a'); + el2.className = 'button'; + el2.textContent = 'Previous'; + } + + el2.setAttribute('data-o', offset - Search.pageSize); + + $.on(el2, 'click', Search.onPageClick); + + el.appendChild(el2); + + cnt.appendChild(el); + } + + el = $.el('div'); + el.className = 'pages'; + el.textContent = 'Page ' + curPage + ' / ' + maxPage; + + cnt.appendChild(el); + + if (curPage < maxPage) { + el = $.el('div'); + el.className = 'next'; + + if (!Main.hasMobileLayout) { + el2 = $.el('input'); + el2.type = 'button'; + el2.value = 'Next'; + } + else { + el2 = $.el('a'); + el2.className = 'button'; + el2.textContent = 'Next'; + } + + el2.setAttribute('data-o', offset + Search.pageSize); + + $.on(el2, 'click', Search.onPageClick); + + el.appendChild(el2); + + cnt.appendChild(el); + } + + el = $.id('delform'); + + el.parentNode.insertBefore(cnt, el.nextElementSibling); + }, + + showStatus: function(msg, cls) { + var cnt; + + cnt = $.cls('board')[0]; + + if (msg) { + cnt.innerHTML = '
    ' + msg + '
    '; + } + else { + cnt.innerHTML = ''; + } + }, + + showError: function(msg) { + Search.showStatus(msg, 'error'); + }, + + toggleSpinner: function(flag) { + var el = $.id('js-sf-btn'); + + if (flag) { + el.disabled = true; + Search.showStatus('Searching…', 'spnr'); + } + else { + el.disabled = false; + Search.showStatus(false); + } + }, + + buildResults: function(data) { + var j, k, op, cnt, threads, thread, boardDiv, reply; + + threads = data.threads; + + if (threads.length < 1) { + Search.showError('Nothing found.'); + Search.updateCtrl(false); + return; + } + + boardDiv = $.cls('board')[0]; + + boardDiv.textContent = ''; + + for (j = 0; thread = threads[j]; ++j) { + op = thread.posts[0]; + + cnt = $.el('div'); + cnt.id = 't' + op.no; + cnt.className = 'thread'; + + cnt.appendChild(Parser.buildHTMLFromJSON(op, thread.board, true)); + + for (k = 1; reply = thread.posts[k]; ++k) { + cnt.appendChild(Parser.buildHTMLFromJSON(reply, thread.board)); + } + + boardDiv.appendChild(cnt); + + boardDiv.appendChild($.el('hr')); + } + + Search.updateCtrl(data.offset, data.nhits); + } +}; + +/** + * Depager + */ +var Depager = {}; + +Depager.init = function() { + var el, el2, cnt; + + this.isLoading = false; + this.isEnabled = false; + this.isComplete = false; + this.threadsLoaded = false; + this.threadQueue = []; + this.debounce = 100; + this.threshold = 350; + + this.adId = 'azk53379'; + this.adZones = [ 16258, 16260 ]; + + this.boardHasAds = !!$.id(this.adId); + + if (this.boardHasAds) { + el = $.cls('ad-plea'); + this.adPlea = el[el.length - 1]; + } + + if (Main.hasMobileLayout) { + el = $.cls('next')[1]; + + if (!el) { + return; + } + + el = el.firstElementChild; + el.textContent = 'Load More'; + el.className += ' m-depagelink'; + el.setAttribute('data-cmd', 'depage'); + } + else { + el = $.cls('prev')[0]; + + if (!el) { + return; + } + + el.innerHTML = '[All]'; + el = el.firstElementChild; + } + + if (Config.alwaysDepage) { + this.isEnabled = true; + el.parentNode.parentNode.className += ' depagerEnabled'; + Depager.bindHandlers(); + + if (!Main.hasMobileLayout && (cnt = $.cls('board')[0])) { + el2 = document.createElement('span'); + el2.className = 'depageNumber'; + el2.textContent = 'Page 1'; + cnt.insertBefore(el2, cnt.firstElementChild); + } + } + else { + el.setAttribute('data-cmd', 'depage'); + } +}; + +Depager.onScroll = function() { + if (document.documentElement.scrollHeight + <= (Math.ceil(window.innerHeight + window.pageYOffset) + Depager.threshold)) { + if (Depager.threadsLoaded) { + Depager.renderNext(); + } + else { + Depager.depage(); + } + } +}; + +Depager.trackPageview = function(pageId) { + var url; + + try { + if (window._gat) { + url = '/' + Main.board + '/' + pageId; + window._gat._getTrackerByName()._trackPageview(url); + } + + if (window.__qc) { + window.__qc.qpixelsent = []; + window._qevents.push({ qacct: window.__qc.qopts.qacct }); + window.__qc.firepixels(); + } + } + catch(e) { + console.log(e); + } +}; + +Depager.insertAd = function(pageId, frag, zone, isLastPage) { + var wrap, cnt, nodes; + + if (!Depager.boardHasAds || !window.ados_add_placement) { + return; + } + + if (isLastPage) { + nodes = $.cls('bottomad'); + wrap = nodes[nodes.length - 1]; + cnt = document.createElement('div'); + cnt.id = 'azkDepage' + (pageId + 1); + wrap.appendChild(cnt); + window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone); + } + + wrap = document.createElement('div'); + wrap.className = 'bottomad center depaged-ad'; + + if (pageId == 2) { + cnt = $.id(Depager.adId); + } + else { + cnt = document.createElement('div'); + cnt.id = 'azkDepage' + pageId; + } + + wrap.appendChild(cnt); + frag.appendChild(wrap); + + if (Depager.adPlea) { + frag.appendChild(Depager.adPlea.cloneNode(true)); + } + + frag.appendChild(document.createElement('hr')); + + if (pageId != 2) { + window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone); + } +}; + +Depager.loadAds = function() { + if (!Depager.boardHasAds || !window.ados_load) { + return; + } + + window.ados_load(); +}; + +Depager.renderNext = function() { + var el, frag, i, j, k, threads, op, summary, cnt, reply, parseList, scroll, + last_replies, boardDiv, pageId, data, isLastPage; + + parseList = []; + + scroll = window.pageYOffset; + + frag = document.createDocumentFragment(); + + data = Depager.threadQueue.shift(); + + if (!data) { + return; + } + + threads = data.threads; + pageId = data.page; + + isLastPage = !Depager.threadQueue.length; + + Depager.insertAd(pageId, frag, data.adZone, isLastPage); + + el = document.createElement('span'); + el.className = 'depageNumber'; + el.textContent = 'Page ' + pageId; + frag.appendChild(el); + + for (j = 0; op = threads[j]; ++j) { + if ($.id('t' + op.no)) { + continue; + } + + cnt = document.createElement('div'); + cnt.id = 't' + op.no; + cnt.className = 'thread'; + + cnt.appendChild(Parser.buildHTMLFromJSON(op, Main.board, true)); + + if (summary = Parser.buildSummary(op.no, op.omitted_posts, op.omitted_images)) { + cnt.appendChild(summary); + } + + if (op.replies) { + last_replies = op.last_replies; + + for (k = 0; reply = last_replies[k]; ++k) { + cnt.appendChild(Parser.buildHTMLFromJSON(reply, Main.board)); + } + } + + frag.appendChild(cnt); + + frag.appendChild(document.createElement('hr')); + + parseList.push(op.no); + } + + if (isLastPage) { + Depager.unbindHandlers(); + Depager.isComplete = true; + Depager.setStatus('disabled'); + } + + boardDiv = $.cls('board')[0]; + boardDiv.insertBefore(frag, boardDiv.lastElementChild); + + Depager.trackPageview(pageId); + + Depager.loadAds(); + + for (i = 0; op = parseList[i]; ++i) { + Parser.parseThread(op); + } + + window.scrollTo(0, scroll); +}; + +Depager.bindHandlers = function() { + window.addEventListener('scroll', Depager.onScroll, false); + window.addEventListener('resize', Depager.onScroll, false); +}; + +Depager.unbindHandlers = function() { + window.removeEventListener('scroll', Depager.onScroll, false); + window.removeEventListener('resize', Depager.onScroll, false); +}; + +Depager.setStatus = function(type) { + var i, el, links, p, caption; + + if (!Main.hasMobileLayout) { + links = $.cls('depagelink'); + caption = 'All'; + } + else { + links = $.cls('m-depagelink'); + caption = 'Load More'; + } + + if (!links.length) { + return; + } + + if (type == 'enabled') { + for (i = 0; el = links[i]; ++i) { + el.textContent = caption; + p = el.parentNode.parentNode; + if (!$.hasClass(p, 'depagerEnabled')) { + $.addClass(p,'depagerEnabled'); + } + } + } + else if (type == 'loading') { + for (i = 0; el = links[i]; ++i) { + el.textContent = 'Loading…'; + } + } + else if (type == 'disabled') { + for (i = 0; el = links[i]; ++i) { + if (!Main.hasMobileLayout) { + el.textContent = caption; + $.removeClass(el.parentNode.parentNode,'depagerEnabled'); + } + else { + el.parentNode.parentNode.removeChild(el.parentNode); + } + } + } + else if (type == 'error') { + for (i = 0; el = links[i]; ++i) { + el.textContent = 'Error'; + el.removeAttribute('title'); + el.removeAttribute('data-cmd'); + $.removeClass(el.parentNode.parentNode, 'depagerEnabled'); + } + } +}; + +Depager.toggle = function() { + if (Depager.isLoading || Depager.isComplete) { + return; + } + + if (Depager.isEnabled) { + Depager.disable(); + } + else { + Depager.enable(); + } + + Depager.isEnabled = !Depager.isEnabled; +}; + +Depager.enable = function() { + Depager.bindHandlers(); + Depager.setStatus('enabled'); + Depager.onScroll(); +}; + +Depager.disable = function() { + Depager.unbindHandlers(); + Depager.setStatus('disabled'); +}; + +Depager.depage = function() { + if (Depager.isLoading) { + return; + } + + Depager.isLoading = true; + + $.get('//a.4cdn.org/' + Main.board + '/catalog.json', { + onload: Depager.onLoad, + onerror: Depager.onError + }); + + Depager.setStatus('loading'); +}; + +Depager.onLoad = function() { + var catalog, i, page, queue, adZone; + + Depager.isLoading = false; + Depager.threadsLoaded = true; + + if (this.status == 200) { + Depager.setStatus('enabled'); + + if (!Config.alwaysDepage) { + Depager.bindHandlers(); + } + + catalog = Parser.parseCatalogJSON(this.responseText); + + queue = Depager.threadQueue; + + adZone = 0; + for (i = 1; page = catalog[i]; ++i) { + page.adZone = Depager.adZones[adZone]; + queue.push(page); + adZone = adZone ? 0 : 1; + } + + Depager.renderNext(); + } + else if (this.status == 404) { + Depager.unbindHandlers(); + Depager.setStatus('error'); + } + else { + Depager.unbindHandlers(); + console.log('Error: ' + this.status); + Depager.setStatus('error'); + } +}; + +Depager.onError = function() { + Depager.isLoading = false; + Depager.unbindHandlers(); + console.log('Error: ' + this.status); + Depager.setStatus('error'); +}; + +/** + * Quote inlining + */ +var QuoteInline = {}; + +QuoteInline.isSelfQuote = function(node, pid, board) { + if (board && board != Main.board) { + return false; + } + + node = node.parentNode; + + if ((node.nodeName == 'BLOCKQUOTE' && node.id.split('m')[1] == pid) + || node.parentNode.id.split('_')[1] == pid) { + return true; + } + + return false; +}; + +QuoteInline.toggle = function(link, e) { + var i, j, t, pfx, src, el, count, media; + + t = link.getAttribute('href').match(/(?:\/([a-z0-9]+)\/thread\/)?([0-9]+)?#p([0-9]+)$/); + + if (!t || QuoteInline.isSelfQuote(link, t[3], t[1])) { + return; + } + + e && e.preventDefault(); + + if (pfx = link.getAttribute('data-pfx')) { + link.removeAttribute('data-pfx'); + $.removeClass(link, 'linkfade'); + + el = $.id(pfx + 'p' + t[3]); + + media = $.cls('expandedWebm', el); + + for (i = 0; j = media[i]; ++i) { + j.pause(); + } + + el.parentNode.removeChild(el); + + if (link.parentNode.parentNode.className == 'backlink') { + el = $.id('pc' + t[3]); + count = +el.getAttribute('data-inline-count') - 1; + if (count === 0) { + el.style.display = ''; + el.removeAttribute('data-inline-count'); + } + else { + el.setAttribute('data-inline-count', count); + } + } + + return; + } + + if (src = $.id('p' + t[3])) { + QuoteInline.inline(link, src, t[3]); + } + else { + QuoteInline.inlineRemote(link, t[1] || Main.board, t[2], t[3]); + } +}; + +QuoteInline.inlineRemote = function(link, board, tid, pid) { + var onload, onerror, cached, key, el, dummy; + + if (link.hasAttribute('data-loading')) { + return; + } + + key = board + '-' + tid; + + if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) { + Parser.parsePost(el); + QuoteInline.inline(link, el); + return; + } + + if ((dummy = link.nextElementSibling) && $.hasClass(dummy, 'spinner')) { + dummy.parentNode.removeChild(dummy); + return; + } + else { + dummy = document.createElement('div'); + } + + dummy.className = 'preview spinner inlined'; + dummy.textContent = 'Loading...'; + link.parentNode.insertBefore(dummy, link.nextSibling); + + onload = function() { + var el, thread; + + link.removeAttribute('data-loading'); + + if (this.status === 200 || this.status === 304 || this.status === 0) { + thread = Parser.parseThreadJSON(this.responseText); + + $.cache[key] = thread; + + if (el = Parser.buildPost(thread, board, pid)) { + dummy.parentNode && dummy.parentNode.removeChild(dummy); + Parser.parsePost(el); + QuoteInline.inline(link, el); + } + else { + $.addClass(link, 'deadlink'); + dummy.textContent = 'This post doesn\'t exist anymore'; + } + } + else if (this.status === 404) { + $.addClass(link, 'deadlink'); + dummy.textContent = 'This thread doesn\'t exist anymore'; + } + else { + this.onerror(); + } + }; + + onerror = function() { + dummy.textContent = 'Error: ' + this.statusText + ' (' + this.status + ')'; + link.removeAttribute('data-loading'); + }; + + link.setAttribute('data-loading', '1'); + + $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json', + { + onload: onload, + onerror: onerror + } + ); +}; + +QuoteInline.inline = function(link, src, id) { + var i, j, now, el, blcnt, isBl, inner, tblcnt, pfx, dest, count, cnt; + + now = Date.now(); + + if (id) { + if ((blcnt = link.parentNode.parentNode).className == 'backlink') { + el = blcnt.parentNode.parentNode.parentNode; + isBl = true; + } + else { + el = blcnt.parentNode; + } + + while (el.parentNode !== document) { + if (el.id.split('m')[1] == id) { + return; + } + el = el.parentNode; + } + } + + link.className += ' linkfade'; + link.setAttribute('data-pfx', now); + + QuotePreview.stopMedia(src); + + el = src.cloneNode(true); + el.id = now + el.id; + el.setAttribute('data-pfx', now); + el.className += ' preview inlined'; + $.removeClass(el, 'highlight'); + $.removeClass(el, 'highlight-anti'); + + if ((inner = $.cls('inlined', el))[0]) { + while (j = inner[0]) { + j.parentNode.removeChild(j); + } + inner = $.cls('quotelink', el); + for (i = 0; j = inner[i]; ++i) { + j.removeAttribute('data-pfx'); + $.removeClass(j, 'linkfade'); + } + } + + for (i = 0; j = el.children[i]; ++i) { + j.id = now + j.id; + } + + if (tblcnt = $.cls('backlink', el)[0]) { + tblcnt.id = now + tblcnt.id; + } + + if (isBl) { + pfx = blcnt.parentNode.parentNode.getAttribute('data-pfx') || ''; + dest = $.id(pfx + 'm' + blcnt.id.split('_')[1]); + dest.insertBefore(el, dest.firstChild); + if (count = src.parentNode.getAttribute('data-inline-count')) { + count = +count + 1; + } + else { + count = 1; + src.parentNode.style.display = 'none'; + } + src.parentNode.setAttribute('data-inline-count', count); + } + else { + if ($.hasClass(link.parentNode, 'quote')) { + link = link.parentNode; + cnt = link.parentNode; + } + else { + cnt = link.parentNode; + } + + while (cnt.nodeName === 'S') { + link = cnt; + cnt = cnt.parentNode; + } + + cnt.insertBefore(el, link.nextSibling); + } +}; + +/** + * Quote preview + */ +var QuotePreview = {}; + +QuotePreview.init = function() { + this.regex = /(?:\/([a-z0-9]+)\/thread\/)?([0-9]+)?#p([0-9]+)$/; + this.highlight = null; + this.highlightAnti = null; + this.cur = null; +}; + +QuotePreview.resolve = function(link) { + var self, t, post, offset, pfx; + + self = QuotePreview; + self.cur = null; + + t = link.getAttribute('href').match(self.regex); + + if (!t) { + return; + } + + // Quoted post in scope + pfx = link.getAttribute('data-pfx') || ''; + + if (post = document.getElementById(pfx + 'p' + t[3])) { + // Visible and not filtered out? + offset = post.getBoundingClientRect(); + if (offset.top > 0 + && offset.bottom < document.documentElement.clientHeight + && !$.hasClass(post.parentNode, 'post-hidden')) { + if (!$.hasClass(post, 'highlight') && location.hash.slice(1) != post.id) { + self.highlight = post; + $.addClass(post, 'highlight'); + } + else if (!$.hasClass(post, 'op')) { + self.highlightAnti = post; + $.addClass(post, 'highlight-anti'); + } + return; + } + // Nope + self.show(link, post); + } + // Quoted post out of scope + else { + if (!UA.hasCORS) { + return; + } + self.showRemote(link, t[1] || Main.board, t[2], t[3]); + } +}; + +QuotePreview.showRemote = function(link, board, tid, pid) { + var onload, onerror, el, cached, key; + + key = board + '-' + tid; + + QuotePreview.cur = key; + + if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) { + QuotePreview.show(link, el); + return; + } + + link.style.cursor = 'wait'; + + onload = function() { + var el, thread; + + link.style.cursor = ''; + + if (this.status === 200 || this.status === 304 || this.status === 0) { + thread = Parser.parseThreadJSON(this.responseText); + + $.cache[key] = thread; + + if ($.id('quote-preview') || QuotePreview.cur != key) { + return; + } + + if (el = Parser.buildPost(thread, board, pid)) { + el.className = 'post preview'; + el.style.display = 'none'; + el.id = 'quote-preview'; + document.body.appendChild(el); + QuotePreview.show(link, el, true); + } + else { + $.addClass(link, 'deadlink'); + } + } + else if (this.status === 404) { + $.addClass(link, 'deadlink'); + } + }; + + onerror = function() { + link.style.cursor = ''; + }; + + $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json', + { + onload: onload, + onerror: onerror + } + ); +}; + +QuotePreview.show = function(link, post, remote) { + var rect, postHeight, doc, docWidth, style, pos, quotes, i, j, qid, + top, scrollTop, img, media; + + QuotePreview.stopMedia(post); + + if (remote) { + Parser.parsePost(post); + post.style.display = ''; + } + else { + post = post.cloneNode(true); + if (location.hash && location.hash == ('#' + post.id)) { + post.className += ' highlight'; + } + post.id = 'quote-preview'; + post.className += ' preview' + + (!$.hasClass(link.parentNode.parentNode, 'backlink') ? ' reveal-spoilers' : ''); + + if (Config.imageExpansion && (img = $.cls('expanded-thumb', post)[0])) { + ImageExpansion.contract(img); + } + } + + if (media = $.cls('expandedWebm', post)[0]) { + media.controls = false; + media.autoplay = false; + } + + if (!link.parentNode.className) { + quotes = $.qsa( + '#' + $.cls('postMessage', post)[0].id + ' > .quotelink', post + ); + if (quotes[1]) { + qid = '>>' + link.parentNode.parentNode.id.split('_')[1]; + for (i = 0; j = quotes[i]; ++i) { + if (j.textContent == qid) { + $.addClass(j, 'dotted'); + break; + } + } + } + } + + rect = link.getBoundingClientRect(); + doc = document.documentElement; + docWidth = doc.offsetWidth; + style = post.style; + + document.body.appendChild(post); + + if (Main.isMobileDevice) { + style.top = rect.top + link.offsetHeight + window.pageYOffset + 'px'; + + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + style.right = docWidth - rect.right + 'px'; + } + else { + style.left = rect.left + 'px'; + } + } + else { + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + pos = docWidth - rect.left; + style.right = pos + 5 + 'px'; + } + else { + pos = rect.left + rect.width; + style.left = pos + 5 + 'px'; + } + + top = rect.top + link.offsetHeight + window.pageYOffset + - post.offsetHeight / 2 - rect.height / 2; + + postHeight = post.getBoundingClientRect().height; + + if (doc.scrollTop != document.body.scrollTop) { + scrollTop = doc.scrollTop + document.body.scrollTop; + } else { + scrollTop = document.body.scrollTop; + } + + if (top < scrollTop) { + style.top = scrollTop + 'px'; + } + else if (top + postHeight > scrollTop + doc.clientHeight) { + style.top = scrollTop + doc.clientHeight - postHeight + 'px'; + } + else { + style.top = top + 'px'; + } + } +}; + +QuotePreview.remove = function(el) { + var self, cnt; + + self = QuotePreview; + self.cur = null; + + if (self.highlight) { + $.removeClass(self.highlight, 'highlight'); + self.highlight = null; + } + else if (self.highlightAnti) { + $.removeClass(self.highlightAnti, 'highlight-anti'); + self.highlightAnti = null; + } + + if (el) { + el.style.cursor = ''; + } + + if (cnt = $.id('quote-preview')) { + document.body.removeChild(cnt); + } +}; + +QuotePreview.stopMedia = function(el) { + var i, media; + + if ((media = $.tag('VIDEO', el))[0]) { + for (i = 0; el = media[i]; ++i) { + el.autoplay = false; + } + } + + if ((media = $.tag('AUDIO', el))[0]) { + for (i = 0; el = media[i]; ++i) { + el.autoplay = false; + } + } +}; + +/** + * Image expansion + */ +var ImageExpansion = { + activeVideos: [], + timeout: null, + pendingTarget: null +}; + +ImageExpansion.loadOgv = function(target) { + ImageExpansion.pendingTarget = target; + + if ($.id('js-ogv-scr')) { + return; + } + + let s = $.el('script'); + s.id = 'js-ogv-scr'; + s.onload = ImageExpansion.onOgvLoaded; + s.src = 'https://s.4cdn.org/js/ogv/ogv.js'; + document.body.appendChild(s); +}; + +ImageExpansion.onOgvLoaded = function() { + let self = ImageExpansion; + if (self.pendingTarget) { + self.expandWebm(self.pendingTarget); + } +}; + +ImageExpansion.expand = function(thumb) { + var img, href, ext, a; + + if (Config.imageHover) { + ImageHover.hide(); + } + + a = thumb.parentNode; + + href = a.getAttribute('href'); + + if (ext = href.match(/\.(?:webm|mp4|pdf)$/)) { + if (ext[0] == '.webm' || ext[0] == '.mp4') { + return ImageExpansion.expandWebm(thumb); + } + return false; + } + + if (Main.hasMobileLayout && a.hasAttribute('data-m')) { + href = ImageExpansion.setMobileSrc(a); + } + + thumb.setAttribute('data-expanding', '1'); + + img = document.createElement('img'); + img.alt = 'Image'; + img.setAttribute('src', href); + img.className = 'expanded-thumb'; + img.style.display = 'none'; + img.onerror = this.onError; + + thumb.parentNode.insertBefore(img, thumb.nextElementSibling); + + if (UA.hasCORS) { + thumb.style.opacity = '0.75'; + this.timeout = this.checkLoadStart(img, thumb); + } + else { + this.onLoadStart(img, thumb); + } + + return true; +}; + +ImageExpansion.contract = function(img) { + var cnt, p; + + clearTimeout(this.timeout); + + p = img.parentNode; + cnt = p.parentNode.parentNode; + + $.removeClass(p.parentNode, 'image-expanded'); + + if (Config.centeredThreads) { + $.removeClass(cnt.parentNode, 'centre-exp'); + cnt.parentNode.style.marginLeft = ''; + } + + if (!Main.tid && Config.threadHiding) { + $.removeClass(p, 'image-expanded-anti'); + } + + p.firstChild.style.display = ''; + + p.removeChild(img); + + if (cnt.offsetTop < window.pageYOffset) { + cnt.scrollIntoView(); + } +}; + +ImageExpansion.toggle = function(t) { + if (t.hasAttribute('data-md5')) { + if (!t.hasAttribute('data-expanding')) { + return ImageExpansion.expand(t); + } + } + else { + ImageExpansion.contract(t); + } + + return true; +}; + +ImageExpansion.setMobileSrc = function(a) { + var href; + + a.removeAttribute('data-m'); + href = a.getAttribute('href'); + a.setAttribute('data-orig', href); + href = href.replace(/\/([0-9]+).+$/, '/$1m.jpg'); + a.setAttribute('href', href); + + return href; +}; + +ImageExpansion.expandWebm = function(thumb) { + var el, link, fileText, left, href, maxWidth, self, cnt; + + self = ImageExpansion; + + if (el = document.getElementById('image-hover')) { + document.body.removeChild(el); + } + + link = thumb.parentNode; + + href = link.getAttribute('href'); + + left = link.getBoundingClientRect().left; + maxWidth = document.documentElement.clientWidth - left - 25; + + link.style.display = 'none'; + + if (href.match(/\.webm$/) && /iPhone|iPad/.test(navigator.userAgent)) { + if (!window.OGVPlayer) { + OgvCtrl.init(); + self.loadOgv(thumb); + return true; + } + + if (OgvCtrl.ogv) { + self.detachOgv(OgvCtrl.ogv); + } + + cnt = document.createElement('div'); + cnt.className = 'ogv-cnt expandedWebm'; + el = new OGVPlayer({ + wasm: true, + threading: false, + simd: false + }); + el.onloadedmetadata = self.fitWebm; + el.onvolumechange = Main.getWebmVolumeChangeCb(); + $.on(el, 'ended', OgvCtrl.onPlayEnd); + cnt.appendChild(el); + link.parentNode.appendChild(cnt); + el.src = href.replace(/\/\/.+\.4chan\.org\//, '//i.4cdn.org/'); + OgvCtrl.attach(el); + if (!Config.unmuteWebm) { + OgvCtrl.toggleMute(); + } + OgvCtrl.togglePlay(); + } + else { + el = document.createElement('video'); + el.muted = !Config.unmuteWebm; + el.controls = true; + el.loop = true; + el.autoplay = true; + el.className = 'expandedWebm'; + el.onloadedmetadata = self.fitWebm; + el.onvolumechange = Main.getWebmVolumeChangeCb(); + el.onplay = self.onWebmPlay; + link.parentNode.appendChild(el); + el.src = href; + } + + if (Config.unmuteWebm) { + el.volume = Main.getWebmVolume(); + } + + if (Main.hasMobileLayout) { + el = document.createElement('div'); + el.className = 'collapseWebm'; + el.innerHTML = 'Close'; + link.parentNode.appendChild(el); + } + else { + fileText = thumb.parentNode.previousElementSibling; + el = document.createElement('span'); + el.className = 'collapseWebm'; + el.innerHTML = '-[Close]'; + fileText.appendChild(el); + } + + $.addClass(link.parentNode, 'image-expanded'); + + el.firstElementChild.addEventListener('click', self.collapseWebm, false); + + return true; +}; + +ImageExpansion.fitWebm = function() { + var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, cntEl, + centerWidth, ofs, player, target; + + player = this; + + if (OgvCtrl.ogv) { + target = player.parentNode; + $.addClass(target, 'ogv-loaded'); + } + else { + target = player; + } + + if (Config.centeredThreads) { + centerWidth = $.cls('opContainer')[0].offsetWidth; + cntEl = target.parentNode.parentNode.parentNode; + $.addClass(cntEl, 'centre-exp'); + } + + left = player.getBoundingClientRect().left; + + maxWidth = document.documentElement.clientWidth - left; + maxHeight = document.documentElement.clientHeight; + + if (!Main.hasMobileLayout) { + maxWidth -= 25; + } + + imgWidth = player.videoWidth; + imgHeight = player.videoHeight; + + if (imgWidth > maxWidth) { + ratio = maxWidth / imgWidth; + imgWidth = maxWidth; + imgHeight = imgHeight * ratio; + } + + if (Config.fitToScreenExpansion && imgHeight > maxHeight) { + ratio = maxHeight / imgHeight; + imgHeight = maxHeight; + imgWidth = imgWidth * ratio; + } + + target.style.width = (0 | imgWidth) + 'px'; + target.style.height = (0 | imgHeight) + 'px'; + + if (player !== target) { + player.style.width = target.style.width; + player.style.height = target.style.height; + } + + if (Config.centeredThreads) { + left = target.getBoundingClientRect().left; + ofs = target.offsetWidth + left * 2; + if (ofs > centerWidth) { + left = Math.floor(($.docEl.clientWidth - ofs) / 2); + + if (left > 0) { + cntEl.style.marginLeft = left + 'px'; + } + } + else { + $.removeClass(cntEl, 'centre-exp'); + } + } +}; + +ImageExpansion.onWebmPlay = function() { + var self = ImageExpansion; + + if (!self.activeVideos.length) { + document.addEventListener('scroll', self.onScroll, false); + } + + self.activeVideos.push(this); +}; + +ImageExpansion.collapseWebm = function(e) { + var cnt, el, el2; + + e.preventDefault(); + + this.removeEventListener('click', ImageExpansion.collapseWebm, false); + + cnt = this.parentNode; + + $.removeClass(cnt.parentNode, 'image-expanded'); + + if (Main.hasMobileLayout) { + el = cnt.previousElementSibling; + } + else { + el = cnt.parentNode.parentNode.getElementsByClassName('expandedWebm')[0]; + } + + if (el.classList.contains('ogv-cnt')) { + if (!el.classList.contains('ogv-detached')) { + el.firstElementChild.stop(); + OgvCtrl.detach(); + } + } + else { + el.pause(); + } + + if (Config.centeredThreads) { + el2 = el.parentNode.parentNode.parentNode; + $.removeClass(el2, 'centre-exp'); + el2.style.marginLeft = ''; + } + + el.previousElementSibling.style.display = ''; + el.parentNode.removeChild(el); + cnt.parentNode.removeChild(cnt); +}; + +ImageExpansion.detachOgv = function(ogv) { + let cnt = ogv.parentNode; + cnt.style.width = ogv.style.width; + cnt.style.height = ogv.style.height; + cnt.classList.add('ogv-detached'); + ogv.remove(); +}; + +ImageExpansion.onScroll = function() { + clearTimeout(ImageExpansion.timeout); + ImageExpansion.timeout = setTimeout(ImageExpansion.pauseVideos, 500); +}; + +ImageExpansion.pauseVideos = function() { + var self, i, el, pos, min, max, nodes; + + self = ImageExpansion; + + nodes = []; + min = window.pageYOffset; + max = window.pageYOffset + $.docEl.clientHeight; + + for (i = 0; el = self.activeVideos[i]; ++i) { + pos = el.getBoundingClientRect(); + if (pos.top + window.pageYOffset > max || pos.bottom + window.pageYOffset < min) { + el.pause(); + } + else if (!el.paused){ + nodes.push(el); + } + } + + if (!nodes.length) { + document.removeEventListener('scroll', self.onScroll, false); + } + + self.activeVideos = nodes; +}; + +ImageExpansion.onError = function(e) { + var thumb, img; + + Feedback.error('File no longer exists (404).', 2000); + + img = e.target; + thumb = $.qs('img[data-expanding]', img.parentNode); + + img.parentNode.removeChild(img); + thumb.style.opacity = ''; + thumb.removeAttribute('data-expanding'); +}; + +ImageExpansion.onLoadStart = function(img, thumb) { + var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, fileEl, cntEl, + centerWidth, ofs, el; + + thumb.removeAttribute('data-expanding'); + + fileEl = thumb.parentNode.parentNode; + + if (Config.centeredThreads) { + cntEl = fileEl.parentNode.parentNode; + centerWidth = $.cls('opContainer')[0].offsetWidth; + $.addClass(cntEl, 'centre-exp'); + } + + left = thumb.getBoundingClientRect().left; + + maxWidth = $.docEl.clientWidth - left - 25; + maxHeight = $.docEl.clientHeight; + + imgWidth = img.naturalWidth; + imgHeight = img.naturalHeight; + + if (imgWidth > maxWidth) { + ratio = maxWidth / imgWidth; + imgWidth = maxWidth; + imgHeight = imgHeight * ratio; + } + + if (Config.fitToScreenExpansion && imgHeight > maxHeight) { + ratio = maxHeight / imgHeight; + imgHeight = maxHeight; + imgWidth = imgWidth * ratio; + } + + img.style.maxWidth = imgWidth + 'px'; + img.style.maxHeight = imgHeight + 'px'; + + $.addClass(fileEl, 'image-expanded'); + + if (!Main.tid && Config.threadHiding) { + $.addClass(thumb.parentNode, 'image-expanded-anti'); + } + + img.style.display = ''; + thumb.style.display = 'none'; + + if (Config.centeredThreads) { + left = img.getBoundingClientRect().left; + ofs = img.offsetWidth + left * 2; + if (ofs > centerWidth) { + left = Math.floor(($.docEl.clientWidth - ofs) / 2); + + if (left > 0) { + cntEl.style.marginLeft = left + 'px'; + } + } + else { + $.removeClass(cntEl, 'centre-exp'); + } + } + else if (Main.hasMobileLayout) { + cntEl = thumb.parentNode.lastElementChild; + if (!cntEl.firstElementChild) { + fileEl = document.createElement('div'); + fileEl.className = 'mFileName'; + if (el = thumb.parentNode.parentNode.getElementsByClassName('fileText')[0]) { + el = el.firstElementChild; + fileEl.innerHTML = el.getAttribute('title') || el.innerHTML; + } + cntEl.insertBefore(fileEl, cntEl.firstChild); + } + } +}; + +ImageExpansion.checkLoadStart = function(img, thumb) { + if (img.naturalWidth) { + ImageExpansion.onLoadStart(img, thumb); + thumb.style.opacity = ''; + } + else { + return setTimeout(ImageExpansion.checkLoadStart, 15, img, thumb); + } +}; + +/** + * Image hover + */ +var ImageHover = {}; + +ImageHover.show = function(thumb) { + var el, href, ext; + + if (thumb.nodeName !== 'A') { + href = thumb.parentNode.getAttribute('href'); + } + else { + href = thumb.getAttribute('href'); + } + + if (ext = href.match(/\.(?:webm|mp4|pdf)$/)) { + if (ext[0] == '.webm' || ext[0] == '.mp4') { + ImageHover.showWebm(thumb); + } + return; + } + + el = document.createElement('img'); + el.id = 'image-hover'; + el.alt = 'Image'; + el.onerror = ImageHover.onLoadError; + el.src = href; + + if (Config.imageHoverBg) { + el.style.backgroundColor = 'inherit'; + } + + document.body.appendChild(el); + + if (UA.hasCORS) { + el.style.display = 'none'; + this.timeout = ImageHover.checkLoadStart(el, thumb); + } + else { + el.style.left = thumb.getBoundingClientRect().right + 10 + 'px'; + } +}; + +ImageHover.hide = function() { + var img; + + clearTimeout(this.timeout); + + if (img = $.id('image-hover')) { + if (img.play) { + img.pause(); + Tip.hide(); + } + document.body.removeChild(img); + } +}; + +ImageHover.showWebm = function(thumb) { + var el; + + el = document.createElement('video'); + el.id = 'image-hover'; + + if (Config.imageHoverBg) { + el.style.backgroundColor = 'inherit'; + } + + if (thumb.nodeName !== 'A') { + el.src = thumb.parentNode.getAttribute('href'); + } + else { + el.src = thumb.getAttribute('href'); + } + + el.loop = true; + el.muted = !Config.unmuteWebm; + el.autoplay = true; + el.onerror = ImageHover.onLoadError; + el.onloadedmetadata = function() { ImageHover.showWebMDuration(this, thumb); }; + el.onvolumechange = Main.getWebmVolumeChangeCb(); + + document.body.appendChild(el); + + if (Config.unmuteWebm) { + el.volume = Main.getWebmVolume(); + } +}; + +ImageHover.showWebMDuration = function(el, thumb) { + var w, h, aabb; + + if (!el.parentNode) { + return; + } + + var sound, ms = $.prettySeconds(el.duration); + + if (el.mozHasAudio === true + || el.webkitAudioDecodedByteCount > 0 + || (el.audioTracks && el.audioTracks.length)) { + sound = ' (audio)'; + } + else { + sound = ''; + } + + aabb = thumb.getBoundingClientRect(); + + [w, h] = $.fit(el.videoWidth, el.videoHeight, + window.innerWidth - aabb.right - 20, window.innerHeight); + + el.style.width = w + 'px'; + el.style.height = h + 'px'; + + Tip.show(thumb, ms[0] + ':' + ('0' + ms[1]).slice(-2) + sound); +}; + +ImageHover.onLoadError = function() { + Feedback.error('File no longer exists (404).', 2000); +}; + +ImageHover.onLoadStart = function(img, thumb) { + var bounds, limit; + + bounds = thumb.getBoundingClientRect(); + limit = window.innerWidth - bounds.right - 20; + + if (img.naturalWidth > limit) { + img.style.maxWidth = limit + 'px'; + } + + img.style.display = ''; +}; + +ImageHover.checkLoadStart = function(img, thumb) { + if (img.naturalWidth) { + ImageHover.onLoadStart(img, thumb); + } + else { + return setTimeout(ImageHover.checkLoadStart, 15, img, thumb); + } +}; + +/** + * Quick reply + */ +var QR = {}; + +QR.init = function() { + var item; + + if (!UA.hasFormData) { + return; + } + + this.enabled = !!document.forms.post; + this.currentTid = null; + this.cooldown = null; + this.timestamp = null; + this.auto = false; + + this.btn = null; + this.comField = null; + this.comLength = window.comlen; + this.comCheckTimeout = null; + + this.cdElapsed = 0; + this.activeDelay = 0; + + this.ctTTL = 290; + this.ctTimeout = null; + + this.cooldowns = {}; + + for (item in window.cooldowns) { + this.cooldowns[item] = window.cooldowns[item] * 1000; + } + + if (this.noCaptcha) { + for (item in this.cooldowns) { + this.cooldowns[item] = Math.ceil(this.cooldowns[item] / 2); + } + } + + this.painterTime = 0; + this.painterData = null; + this.painterSrc = null; + this.replayBlob = null; + this.canvasLoading = false; + + this.captchaWidgetCnt = null; + this.captchaWidgetId = null; + this.pulse = null; + this.xhr = null; + + this.fileDisabled = !!window.imagelimit; + + this.tracked = {}; + + if (Main.tid && !Main.hasMobileLayout && !Main.threadClosed) { + QR.addReplyLink(); + } + + window.addEventListener('storage', this.syncStorage, false); +}; + +QR.openTeXPreview = function() { + var el; + + QR.closeTeXPreview(); + + if (!window.MathJax) { + window.loadMathJax(); + } + + el = document.createElement('div'); + el.id = 'tex-preview-cnt'; + el.className = 'UIPanel'; + el.setAttribute('data-cmd', 'close-tex-preview'); + el.innerHTML = '\ +
    Preview\ +Close
    Use [math][/math] tags for inline, and [eqn][/eqn] tags for block equations.
    \ +
    '; + + document.body.appendChild(el); + + el = $.id('input-tex-preview'); + $.on(el, 'keyup', QR.onTeXChanged); +}; + +QR.closeTeXPreview = function() { + var el; + + if (el = $.id('input-tex-preview')) { + $.off(el, 'keyup', QR.onTeXChanged); + + el = $.id('tex-preview-cnt'); + el.parentNode.removeChild(el); + } +}; + +QR.onTeXChanged = function() { + clearTimeout(QR.timeoutTeX); + QR.timeoutTeX = setTimeout(QR.processTeX, 50); +}; + +QR.processTeX = function() { + var src, dest; + + if (QR.processingTeX || !window.MathJax || !(src = $.id('input-tex-preview'))) { + return; + } + + dest = $.id('output-tex-preview'); + + dest.textContent = src.value; + + QR.processingTeX = true; + + MathJax.Hub.Queue(['Typeset', MathJax.Hub, dest], ['onTeXReady', QR]); +}; + +QR.onTeXReady = function() { + QR.processingTeX = false; +}; + +QR.validateCT = function() { + var ct, now, d; + + if (!QR.captchaWidgetCnt) { + return; + } + + ct = Main.getCookie('_ct'); + + if (!ct) { + if (QR.ctTimeout) { + QR.setCTTag(); + } + return; + } + + if (QR.ctTimeout) { + return; + } + + ct = ct.split('.')[1]; + now = 0 | (Date.now() / 1000); + + d = now - ct; + + if (d >= QR.ctTTL) { + QR.setCTTag(); + } + else { + QR.setCTTag(QR.ctTTL - d); + } +}; + +QR.setCTTag = function(sec) { + if (window.t_captcha) { + return; + } + + var el = QR.captchaWidgetCnt; + + QR.clearCTTimeout(); + + if (sec && sec > 0) { + QR.ctTimeout = setTimeout(QR.setCTTag, sec * 1000); + el.style.opacity = '0.25'; + $.on(el, 'mouseover', QR.onCTMouseOver); + $.on(el, 'mouseout', QR.onCTMouseOut); + } + else { + el.style.opacity = ''; + $.off(el, 'mouseover', QR.onCTMouseOver); + $.off(el, 'mouseout', QR.onCTMouseOut); + } +}; + +QR.onCTMouseOver = function() { + QR.onCTMouseOut(); + QR.ctTipTimeout = setTimeout(Tip.show, Tip.delay, QR.captchaWidgetCnt, + 'Verification not required for your next post.'); +}; + +QR.onCTMouseOut = function() { + clearTimeout(QR.ctTipTimeout); + Tip.hide(); +}; + +QR.clearCTTimeout = function() { + clearTimeout(QR.ctTimeout); + QR.ctTimeout = null; +}; + +QR.addReplyLink = function() { + var cnt, el; + + cnt = $.cls('navLinks')[2]; + + el = document.createElement('div'); + el.className = 'open-qr-wrap'; + el.innerHTML = '[Post a Reply]'; + + cnt.insertBefore(el, cnt.firstChild); +}; + +QR.lock = function() { + QR.showPostError('This thread is closed.', 'closed', true); +}; + +QR.unlock = function() { + QR.hidePostError('closed'); +}; + +QR.syncStorage = function(e) { + var key; + + if (!e.key) { + return; + } + + key = e.key.split('-'); + + if (key[0] != '4chan') { + return; + } + + if (key[1] == 'cd' && e.newValue && Main.board == key[2]) { + QR.startCooldown(); + } +}; + +QR.onOpenInPainterClick = function(btn) { + var img, el, tid, pid; + + if (QR.canvasLoading) { + Feedback.error('An image is already being loaded.'); + return; + } + + pid = +btn.getAttribute('data-pid'); + tid = +btn.getAttribute('data-tid'); + + if (!pid || !tid) { + return false; + } + + el = $.qs('#f' + pid + ' a[class="fileThumb"]'); + + if (!el) { + return false; + } + + if (/\.(png|jpg)$/.test(el.href) === false) { + return false; + } + + QR.canvasLoading = true; + + img = new Image(); + img.crossOrigin = 'Anonymous'; + img.onload = QR.onPainterCanvasLoaded; + img.onerror = QR.onPainterCanvasError; + img._pid = pid; + + Feedback.notify('Loading…', 0); + + img.src = el.href.replace('is2.4chan.org', 'i.4cdn.org'); + + QR.show(tid); +}; + +QR.onPainterCanvasError = function() { + QR.canvasLoading = false; + Feedback.error("Couldn't load the image.", 5000); +}; + +QR.onPainterCanvasLoaded = function() { + Feedback.hideMessage(); + + QR.canvasLoading = false; + + if (!QR.currentTid) { + return; + } + + if (this.naturalWidth < 1 || this.naturalHeight < 1) { + return; + } + + if (Tegaki.startTimeStamp) { + Tegaki.destroy(); + } + + Keybinds.enabled = false; + + QR.painterSrc = this._pid; + + Tegaki.open({ + onDone: QR.onPainterDone, + onCancel: QR.onPainterCancel, + saveReplay: false, + width: 1, + height: 1 + }); + + Tegaki.onOpenImageLoaded.call(this); +}; + +QR.openPainter = function() { + var w, h, dims, cb; + + dims = $.tag('input', $.id('qr-painter-ctrl')); + + w = +dims[0].value; + h = +dims[1].value; + + if (w < 1 || h < 1) { + return; + } + + cb = $.cls('oe-r-cb', $.id('qr-painter-ctrl'))[0]; + + Keybinds.enabled = false; + + window.Tegaki.open({ + onDone: QR.onPainterDone, + onCancel: QR.onPainterCancel, + saveReplay: cb && cb.checked, + width: w, + height: h + }); +}; + +QR.onPainterDone = function() { + var el, cnt; + + Keybinds.enabled = true; + + QR.painterData = Tegaki.flatten().toDataURL('image/png'); + + if (Tegaki.saveReplay) { + QR.replayBlob = Tegaki.replayRecorder.toBlob(); + } + + QR.painterTime = 0; + + if (Tegaki.startTimeStamp) { + if (!Tegaki.hasCustomCanvas || QR.painterSrc) { + QR.painterTime = Math.round((Date.now() - Tegaki.startTimeStamp) / 1000); + } + } + + if (el = $.id('qrFile')) { + el.style.visibility = 'hidden'; + } + + cnt = $.id('qr-painter-ctrl'); + + if (el = $.tag('button', cnt)[0]) { + el.textContent = 'Edit'; + } + + if (el = $.tag('button', cnt)[1]) { + el.disabled = false; + } + + for (el of $.tag('input', cnt)) { + el.disabled = true; + } +}; + +QR.onPainterCancel = function() { + var el, cnt; + + Keybinds.enabled = true; + + QR.painterData = null; + QR.painterSrc = null; + QR.replayBlob = null; + QR.painterTime = 0; + + if (el = $.id('qrFile')) { + el.style.visibility = ''; + } + + cnt = $.id('qr-painter-ctrl'); + + if (el = $.tag('button', cnt)[0]) { + el.textContent = 'Draw'; + } + + if (el = $.tag('button', cnt)[1]) { + el.disabled = true; + } + + for (el of $.tag('input', cnt)) { + el.disabled = false; + } +}; + +QR.quotePost = function(tid, pid) { + if (!QR.noCooldown + && (Main.threadClosed || (!Main.tid && Main.isThreadClosed(tid)))) { + alert('This thread is closed'); + return; + } + QR.show(tid); + QR.addQuote(pid); +}; + +QR.addQuote = function(pid) { + var q, pos, sel, ta; + + ta = $.tag('textarea', document.forms.qrPost)[0]; + + pos = ta.selectionStart; + + sel = UA.getSelection(); + + if (pid) { + q = '>>' + pid + '\n'; + } + else { + q = ''; + } + + if (sel) { + q += '>' + sel.trim().replace(/[\r\n]+/g, '\n>') + '\n'; + } + + if (ta.value) { + ta.value = ta.value.slice(0, pos) + + q + ta.value.slice(ta.selectionEnd); + } + else { + ta.value = q; + } + if (UA.isOpera) { + pos += q.split('\n').length; + } + + ta.selectionStart = ta.selectionEnd = pos + q.length; + + if (ta.selectionStart == ta.value.length) { + ta.scrollTop = ta.scrollHeight; + } + + ta.focus(); +}; + +QR.show = function(tid) { + var i, j, cnt, postForm, form, qrForm, fields, row, spoiler, file, + el, el2, placeholder, qrError, cookie, mfs; + + window.checkIncognito && window.checkIncognito(); + + if (QR.currentTid) { + if (!Main.tid && QR.currentTid != tid) { + $.id('qrTid').textContent = $.id('qrResto').value = QR.currentTid = tid; + $.byName('com')[1].value = ''; + + QR.startCooldown(); + } + + if (Main.hasMobileLayout) { + $.id('quickReply').style.top = window.pageYOffset + 25 + 'px'; + } + + return; + } + + QR.currentTid = tid; + + postForm = $.id('postForm'); + + cnt = document.createElement('div'); + cnt.id = 'quickReply'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-trackpos', 'QR-position'); + + if (Main.hasMobileLayout) { + cnt.style.top = window.pageYOffset + 28 + 'px'; + } + else if (Config['QR-position']) { + cnt.style.cssText = Config['QR-position']; + } + else { + cnt.style.right = '0px'; + cnt.style.top = '10%'; + } + + cnt.innerHTML = + '
    ' + + (window.math_tags ? '' : '') + + 'Reply to Thread No.' + + tid + 'X
    '; + + form = postForm.parentNode.cloneNode(false); + form.setAttribute('name', 'qrPost'); + + form.innerHTML = + ( + (mfs = $.byName('MAX_FILE_SIZE')[0]) + ? ('') + : '' + ) + + '' + + ''; + + qrForm = document.createElement('div'); + qrForm.id = 'qrForm'; + + this.btn = null; + + fields = postForm.firstElementChild.children; + for (i = 0, j = fields.length - 1; i < j; ++i) { + row = document.createElement('div'); + if (fields[i].id == 'captchaFormPart') { + if (QR.noCaptcha) { + continue; + } + row.id = 'qrCaptchaContainer'; + row.classList.add('t-qr-root'); + QR.captchaWidgetCnt = row; + } + else { + placeholder = fields[i].getAttribute('data-type'); + if (placeholder == 'Password' || placeholder == 'Spoilers') { + continue; + } + else if (placeholder == 'File') { + file = fields[i].children[1].firstChild.cloneNode(false); + file.tabIndex = ''; + file.id = 'qrFile'; + file.size = '19'; + file.addEventListener('change', QR.onFileChange, false); + row.appendChild(file); + + file.title = 'Shift + Click to remove the file'; + } + else if (placeholder == 'Painter') { + row.innerHTML = fields[i].children[1].innerHTML; + row.id = 'qr-painter-ctrl'; + row.className = 'desktop'; + el = row.getElementsByTagName('button'); + el[0].setAttribute('data-cmd', 'qr-painter-draw'); + el[1].setAttribute('data-cmd', 'qr-painter-clear'); + } + else { + row.innerHTML = fields[i].children[1].innerHTML; + if (row.firstChild.type == 'hidden') { + el = row.lastChild.previousSibling; + } + else { + el = row.firstChild; + } + el.tabIndex = ''; + if (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA') { + if (el.name == 'name') { + if (cookie = Main.getCookie('4chan_name')) { + el.value = cookie; + } + } + else if (el.name == 'email') { + el.id = 'qrEmail'; + if (cookie = Main.getCookie('options')) { + el.value = cookie; + } + if (el.nextElementSibling) { + el.parentNode.removeChild(el.nextElementSibling); + } + } + else if (el.name == 'com') { + QR.comField = el; + el.addEventListener('keydown', QR.onKeyDown, false); + el.addEventListener('paste', QR.onKeyDown, false); + el.addEventListener('cut', QR.onKeyDown, false); + if (row.children[1]) { + row.removeChild(el.nextSibling); + } + } + else if (el.name == 'sub') { + continue; + } + if (placeholder !== null) { + el.setAttribute('placeholder', placeholder); + } + } + else if ((el.name == 'flag')) { + if (el2 = $.qs('option[selected]', el)) { + el2.removeAttribute('selected'); + } + if ((cookie = localStorage.getItem('4chan_flag_' + Main.board)) && + (el2 = $.qs('option[value="' + cookie + '"]', el))) { + el2.setAttribute('selected', 'selected'); + } + $.on(el, 'change', window.onBoardFlagChanged); + } + } + } + + qrForm.appendChild(row); + } + + if (!this.btn) { + this.btn = $.qs('input[type="submit"]', postForm).cloneNode(false); + this.btn.tabIndex = ''; + + if (file) { + file.parentNode.appendChild(this.btn); + } + else { + qrForm.appendChild(document.createElement('div')); + qrForm.lastElementChild.appendChild(this.btn); + } + } + + if (el = $.qs('.desktop > label > input[name="spoiler"]', postForm)) { + spoiler = document.createElement('span'); + spoiler.id = 'qrSpoiler'; + spoiler.innerHTML = ''; + file.parentNode.insertBefore(spoiler, file.nextSibling); + } + + form.appendChild(qrForm); + cnt.appendChild(form); + + qrError = document.createElement('div'); + qrError.id = 'qrError'; + cnt.appendChild(qrError); + + cnt.addEventListener('click', QR.onClick, false); + + document.body.appendChild(cnt); + + QR.startCooldown(); + + if (Main.threadClosed) { + QR.lock(); + } + + if (!window.passEnabled) { + QR.renderCaptcha(); + } + + if (!Main.hasMobileLayout) { + Draggable.set($.id('qrHeader')); + } +}; + +QR.renderCaptcha = function() { + if (window.grecaptcha) { + QR.captchaWidgetId = grecaptcha.render(QR.captchaWidgetCnt, { + sitekey: window.recaptchaKey, + theme: (Main.stylesheet === 'tomorrow' || window.dark_captcha) ? 'dark' : 'light' + }); + QR.validateCT(); + return; + } + else if (window.t_captcha) { + TCaptcha.init(QR.captchaWidgetCnt, Main.board, QR.currentTid); + TCaptcha.setErrorCb(QR.showPostError); + } +}; + +QR.resetCaptcha = function() { + if (window.grecaptcha) { + if (QR.captchaWidgetId !== null) { + grecaptcha.reset(QR.captchaWidgetId); + QR.validateCT(); + } + } + else if (window.t_captcha) { + TCaptcha.clearChallenge(); + } +}; + +QR.onPassError = function() { + var el, cnt; + + if (QR.captchaWidgetCnt) { + return; + } + + window.passEnabled = QR.noCaptcha = false; + + el = document.createElement('div'); + el.id = 'qrCaptchaContainer'; + + cnt = $.id('qrForm'); + + cnt.insertBefore(el, cnt.lastElementChild); + + QR.captchaWidgetCnt = el; + + QR.renderCaptcha(); +}; + +QR.onFileChange = function() { + var fsize, maxFilesize; + + if (this.value) { + maxFilesize = window.maxFilesize; + + if (this.files) { + fsize = this.files[0].size; + if (this.files[0].type == 'video/webm' && window.maxWebmFilesize) { + maxFilesize = window.maxWebmFilesize; + } + } + else { + fsize = 0; + } + + if (QR.fileDisabled) { + QR.showPostError('Image limit reached.', 'imagelimit', true); + } + else if (fsize > maxFilesize) { + QR.showPostError('Error: Maximum file size allowed is ' + + Math.floor(maxFilesize / 1048576) + ' MB', 'filesize', true); + } + else { + QR.hidePostError(); + } + } + else { + QR.hidePostError(); + } + + QR.painterData = null; + QR.replayBlob = null; + + QR.startCooldown(); +}; + +QR.onKeyDown = function(e) { + if (e.ctrlKey && e.keyCode == 83) { + var ta, start, end, spoiler; + + e.stopPropagation(); + e.preventDefault(); + + ta = e.target; + start = ta.selectionStart; + end = ta.selectionEnd; + + if (ta.value) { + spoiler = '[spoiler]' + ta.value.slice(start, end) + '[/spoiler]'; + ta.value = ta.value.slice(0, start) + spoiler + ta.value.slice(end); + ta.setSelectionRange(end + 19, end + 19); + } + else { + ta.value = '[spoiler][/spoiler]'; + ta.setSelectionRange(9, 9); + } + } + else if (e.keyCode == 27 && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { + QR.close(); + return; + } + + clearTimeout(QR.comCheckTimeout); + QR.comCheckTimeout = setTimeout(QR.checkCommentField, 500); +}; + +QR.checkCommentField = function() { + var byteLength; + + if (QR.comLength) { + byteLength = encodeURIComponent(QR.comField.value).split(/%..|./).length - 1; + + if (byteLength > QR.comLength) { + QR.showPostError('Error: Comment too long (' + + byteLength + '/' + QR.comLength + ').', 'length', true); + } + else { + QR.hidePostError('length'); + } + } + + if (window.sjis_tags) { + if (/\[sjis\]/.test(QR.comField.value)) { + if (!$.hasClass(QR.comField, 'sjis')) { + $.addClass(QR.comField, 'sjis'); + } + } + else { + if ($.hasClass(QR.comField, 'sjis')) { + $.removeClass(QR.comField, 'sjis'); + } + } + } +}; + +QR.close = function() { + var el, cnt = $.id('quickReply'); + + QR.comField = null; + QR.currentTid = null; + + QR.painterTime = 0; + QR.painterData = null; + QR.painterSrc = null; + QR.replayBlob = null; + QR.canvasLoading = false; + + clearInterval(QR.pulse); + + if (QR.xhr) { + QR.xhr.abort(); + QR.xhr = null; + } + + cnt.removeEventListener('click', QR.onClick, false); + + (el = $.id('qrFile')) && el.removeEventListener('change', QR.startCooldown, false); + (el = $.id('qrEmail')) && el.removeEventListener('change', QR.startCooldown, false); + $.tag('textarea', cnt)[0].removeEventListener('keydown', QR.onKeyDown, false); + + Draggable.unset($.id('qrHeader')); + + if (!QR.noCaptcha) { + QR.setCTTag(); + } + + if (window.t_captcha && TCaptcha.node === QR.captchaWidgetCnt) { + TCaptcha.destroy(); + } + + document.body.removeChild(cnt); +}; + +QR.onClick = function(e) { + var t = e.target; + + if (t.type == 'submit') { + e.preventDefault(); + QR.submit(e.shiftKey); + } + else { + switch (t.id) { + case 'qrFile': + if (e.shiftKey) { + e.preventDefault(); + QR.resetFile(); + } + break; + case 'qrDummyFile': + case 'qrDummyFileButton': + case 'qrDummyFileLabel': + e.preventDefault(); + if (e.shiftKey) { + QR.resetFile(); + } + else { + $.id('qrFile').click(); + } + break; + case 'qrClose': + QR.close(); + break; + } + } +}; + +QR.showPostError = function(msg, type, silent) { + var qrError; + + qrError = $.id('qrError'); + + if (!qrError) { + return; + } + + if (!msg) { + QR.hidePostError(); + return; + } + + qrError.innerHTML = msg; + qrError.style.display = 'block'; + + qrError.setAttribute('data-type', type || ''); + + if (!silent && (document.hidden + || document.mozHidden + || document.webkitHidden + || document.msHidden)) { + alert('Posting Error'); + } +}; + +QR.hidePostError = function(type) { + var el = $.id('qrError'); + + if (!el.hasAttribute('style')) { + return; + } + + if (!type || el.getAttribute('data-type') == type) { + el.removeAttribute('style'); + } +}; + +QR.resetFile = function() { + var file, el; + + QR.painterData = null; + QR.replayBlob = null; + + el = document.createElement('input'); + el.id = 'qrFile'; + el.type = 'file'; + el.size = '19'; + el.name = 'upfile'; + el.addEventListener('change', QR.onFileChange, false); + + file = $.id('qrFile'); + file.removeEventListener('change', QR.onFileChange, false); + + file.parentNode.replaceChild(el, file); + + QR.hidePostError('imagelimit'); + + QR.needPreuploadCaptcha = false; + + QR.startCooldown(); +}; + +QR.submit = function(force) { + var formdata; + + QR.hidePostError(); + + if (!QR.presubmitChecks(force)) { + return; + } + + QR.auto = false; + + QR.xhr = new XMLHttpRequest(); + + QR.xhr.open('POST', document.forms.qrPost.action, true); + + QR.xhr.withCredentials = true; + + QR.xhr.setRequestHeader('Accept', 'application/json'); + + QR.xhr.upload.onprogress = function(e) { + if (e.loaded >= e.total) { + QR.btn.value = '100%'; + } + else { + QR.btn.value = (0 | (e.loaded / e.total * 100)) + '%'; + } + }; + + QR.xhr.onerror = function() { + QR.xhr = null; + QR.showPostError('Connection error.'); + }; + + QR.xhr.onload = function() { + var resp, el, hasFile, ids, tid, pid, tracked; + + QR.xhr = null; + + QR.btn.value = 'Post'; + + if (this.status == 200) { + if (this.getResponseHeader("Content-Type") == 'application/json') { + try { + resp = JSON.parse(this.responseText); + } + catch (e) { + console.log('QR resp', e); + resp = { error: 'Something went wrong.' }; + } + + if (resp.error) { + if (window.passEnabled && /4chan Pass/.test(resp.error)) { + QR.onPassError(); + } + else { + QR.resetCaptcha(); + } + QR.showPostError(resp.error); + return; + } + + if (resp.pid) { + tid = resp.tid; + pid = resp.pid; + } + } + else if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) { + if (window.passEnabled && /4chan Pass/.test(resp)) { + QR.onPassError(); + } + else { + QR.resetCaptcha(); + } + QR.showPostError(resp[1]); + return; + } + else if (ids = this.responseText.match(//)) { + tid = ids[1]; + pid = ids[2]; + } + + if (pid) { + hasFile = (el = $.id('qrFile')) && el.value; + + QR.setPostTime(); + + if (Config.persistentQR) { + $.byName('com')[1].value = ''; + + if (el = $.byName('spoiler')[2]) { + el.checked = false; + } + + QR.resetCaptcha(); + + if (hasFile || QR.painterData) { + QR.resetFile(); + } + + QR.startCooldown(); + } + else { + QR.close(); + } + + if (Main.tid) { + if (Config.threadWatcher) { + ThreadWatcher.setLastRead(pid, tid); + } + QR.lastReplyId = +pid; + Parser.trackedReplies['>>' + pid] = 1; + Parser.saveTrackedReplies(tid, Parser.trackedReplies); + } + else { + tracked = Parser.getTrackedReplies(Main.board, tid) || {}; + tracked['>>' + pid] = 1; + Parser.saveTrackedReplies(tid, tracked); + } + + Parser.touchTrackedReplies(tid); + + UA.dispatchEvent('4chanQRPostSuccess', { threadId: tid, postId: pid }); + } + + if (ThreadUpdater.enabled) { + setTimeout(ThreadUpdater.forceUpdate, 500); + } + } + else { + QR.showPostError('Error: ' + this.status + ' ' + this.statusText); + } + }; + + formdata = new FormData(document.forms.qrPost); + + if (formdata.entries && formdata.delete + && (!formdata.get('upfile') || !formdata.get('upfile').size)) { + formdata.delete('upfile'); + } + + if (QR.painterData) { + QR.appendPainter(formdata); + + if (QR.replayBlob) { + formdata.append('oe_replay', QR.replayBlob, 'tegaki.tgkr'); + } + + formdata.append('oe_time', QR.painterTime); + + if (QR.painterSrc) { + formdata.append('oe_src', QR.painterSrc); + } + } + + clearInterval(QR.pulse); + + QR.btn.value = 'Sending'; + + QR.xhr.send(formdata); +}; + +QR.appendPainter = function(formdata) { + var blob; + + blob = QR.b64toBlob(QR.painterData.slice(QR.painterData.indexOf(',') + 1)); + + if (blob) { + if (blob.size > window.maxFilesize) { + QR.showPostError('Error: Maximum file size allowed is ' + + Math.floor(window.maxFilesize / 1048576) + ' MB', 'filesize', true); + + return; + } + + formdata.append('upfile', blob, 'tegaki.png'); + } +}; + +QR.b64toBlob = function(data) { + var i, bytes, ary, bary, len; + + bytes = atob(data); + len = bytes.length; + + ary = new Array(len); + + for (i = 0; i < len; ++i) { + ary[i] = bytes.charCodeAt(i); + } + + bary = new Uint8Array(ary); + + return new Blob([bary]); +}; + +QR.presubmitChecks = function(force) { + if (QR.xhr) { + QR.xhr.abort(); + QR.xhr = null; + QR.showPostError('Aborted.'); + QR.btn.value = 'Post'; + return false; + } + + if (!force && QR.cooldown) { + if (QR.auto = !QR.auto) { + QR.btn.value = QR.cooldown + 's (auto)'; + } + else { + QR.btn.value = QR.cooldown + 's'; + } + return false; + } + + if (window.isIncognito) { + let el = $.id('qrFile'); + if (el && el.value && !QR.painterData) { + QR.showPostError('Uploading files in incognito mode is not allowed.' + + '
    The File field has been cleared.'); + QR.resetFile(); + return false; + } + } + + return true; +}; + +QR.getCooldown = function(type) { + return QR.cooldowns[type]; +}; + +QR.setPostTime = function() { + return localStorage.setItem('4chan-cd-' + Main.board, Date.now()); +}; + +QR.getPostTime = function() { + return localStorage.getItem('4chan-cd-' + Main.board); +}; + +QR.removePostTime = function() { + return localStorage.removeItem('4chan-cd-' + Main.board); +}; + +QR.startCooldown = function() { + var type, el, time; + + if (QR.noCooldown || !$.id('quickReply') || QR.xhr) { + return; + } + + clearInterval(QR.pulse); + + type = ((el = $.id('qrFile')) && el.value) ? 'image' : 'reply'; + + time = QR.getPostTime(type); + + if (!time) { + QR.btn.value = 'Post'; + return; + } + + QR.timestamp = parseInt(time, 10); + + QR.activeDelay = QR.getCooldown(type); + + QR.cdElapsed = Date.now() - QR.timestamp; + + QR.cooldown = Math.ceil((QR.activeDelay - QR.cdElapsed) / 1000); + + if (QR.cooldown <= 0 || QR.cdElapsed < 0) { + QR.cooldown = false; + QR.btn.value = 'Post'; + return; + } + + QR.btn.value = QR.cooldown + 's'; + + QR.pulse = setInterval(QR.onPulse, 1000); +}; + +QR.onPulse = function() { + QR.cdElapsed = Date.now() - QR.timestamp; + QR.cooldown = Math.ceil((QR.activeDelay - QR.cdElapsed) / 1000); + if (QR.cooldown <= 0) { + clearInterval(QR.pulse); + QR.btn.value = 'Post'; + QR.cooldown = false; + if (QR.auto) { + QR.submit(); + } + } + else { + QR.btn.value = QR.cooldown + (QR.auto ? 's (auto)' : 's'); + } +}; + +/** + * Thread hiding + */ +var ThreadHiding = {}; + +ThreadHiding.init = function() { + this.threshold = 43200000; // 12 hours + + this.hidden = {}; + + this.load(); + + this.purge(); +}; + +ThreadHiding.clear = function(silent) { + var i, id, key, msg; + + this.load(); + + i = 0; + + for (id in this.hidden) { + ++i; + } + + key = '4chan-hide-t-' + Main.board; + + if (!silent) { + if (!i) { + alert("You don't have any hidden threads on /" + Main.board + '/'); + return; + } + + msg = 'This will unhide ' + i + ' thread' + (i > 1 ? 's' : '') + ' on /' + Main.board + '/'; + + if (!confirm(msg)) { + return; + } + + localStorage.removeItem(key); + } + else { + localStorage.removeItem(key); + } +}; + +ThreadHiding.isHidden = function(tid) { + return !!ThreadHiding.hidden[tid]; +}; + +ThreadHiding.toggle = function(tid) { + if (this.isHidden(tid)) { + this.show(tid); + } + else { + this.hide(tid); + } + this.save(); +}; + +ThreadHiding.show = function(tid) { + var sa, th; + + th = $.id('t' + tid); + sa = $.id('sa' + tid); + + if (Main.hasMobileLayout) { + sa.parentNode.removeChild(sa); + th.style.display = null; + $.removeClass(th.nextElementSibling, 'mobile-hr-hidden'); + } + else { + sa.removeAttribute('data-hidden'); + sa.firstChild.src = Main.icons.minus; + $.removeClass(th, 'post-hidden'); + } + + delete this.hidden[tid]; +}; + +ThreadHiding.hide = function(tid) { + var sa, th; + + th = $.id('t' + tid); + + if (Main.hasMobileLayout) { + th.style.display = 'none'; + $.addClass(th.nextElementSibling, 'mobile-hr-hidden'); + + sa = document.createElement('span'); + sa.id = 'sa' + tid; + sa.setAttribute('data-cmd', 'hide'); + sa.setAttribute('data-id', tid); + sa.textContent = 'Show Hidden Thread'; + sa.className = 'mobileHideButton button mobile-tu-show'; + th.parentNode.insertBefore(sa, th); + } + else { + if (Config.hideStubs && !$.cls('stickyIcon', th)[0]) { + th.style.display = th.nextElementSibling.style.display = 'none'; + } + else { + sa = $.id('sa' + tid); + sa.setAttribute('data-hidden', tid); + sa.firstChild.src = Main.icons.plus; + th.className += ' post-hidden'; + } + } + + this.hidden[tid] = Date.now(); +}; + +ThreadHiding.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-hide-t-' + Main.board)) { + this.hidden = JSON.parse(storage); + } +}; + +ThreadHiding.purge = function() { + var i, hasHidden, lastPurged, key; + + key = '4chan-purge-t-' + Main.board; + + lastPurged = localStorage.getItem(key); + + for (i in this.hidden) { + hasHidden = true; + break; + } + + if (!hasHidden) { + return; + } + + if (!lastPurged || lastPurged < Date.now() - this.threshold) { + $.get('//a.4cdn.org/' + Main.board + '/threads.json', + { + onload: function() { + var i, j, t, p, pages, threads, alive; + + if (this.status == 200) { + alive = {}; + pages = JSON.parse(this.responseText); + for (i = 0; p = pages[i]; ++i) { + threads = p.threads; + for (j = 0; t = threads[j]; ++j) { + if (ThreadHiding.hidden[t.no]) { + alive[t.no] = 1; + } + } + } + ThreadHiding.hidden = alive; + ThreadHiding.save(); + localStorage.setItem(key, Date.now()); + } + else { + console.log('Bad status code while purging threads'); + } + }, + onerror: function() { + console.log('Error while purging hidden threads'); + } + }); + } +}; + +ThreadHiding.save = function() { + var i; + + for (i in this.hidden) { + localStorage.setItem('4chan-hide-t-' + Main.board, + JSON.stringify(this.hidden) + ); + return; + } + localStorage.removeItem('4chan-hide-t-' + Main.board); +}; + +/** + * Reply hiding + */ +var ReplyHiding = {}; + +ReplyHiding.init = function() { + this.threshold = 7 * 86400000; + this.hidden = {}; + this.hiddenR = {}; + this.hiddenRMap = {}; + this.hasR = false; + this.load(); +}; + +ReplyHiding.isHidden = function(pid) { + var sa = $.id('sa' + pid); + + return !sa || sa.hasAttribute('data-hidden'); +}; + +ReplyHiding.toggle = function(pid) { + this.load(); + + if (this.isHidden(pid)) { + this.show(pid); + } + else { + this.hide(pid); + } + this.save(); +}; + +ReplyHiding.toggleR = function(pid) { + var i, el, post, nodes, rid, parentPid; + + this.load(); + + if (parentPid = this.hiddenRMap['>>' + pid]) { + this.showR(parentPid, parentPid); + + for (i in this.hiddenRMap) { + if (this.hiddenRMap[i] == parentPid) { + this.showR(i.slice(2)); + } + } + } + else { + this.hideR(pid, pid); + + post = $.id('m' + pid); + nodes = $.cls('postMessage'); + + for (i = 1; nodes[i] !== post; ++i) {} + + for (; el = nodes[i]; ++i) { + if (ReplyHiding.shouldToggleR(el)) { + rid = el.id.slice(1); + this.hideR(rid, pid); + } + } + } + + this.hasR = false; + + for (i in this.hiddenRMap) { + this.hasR = true; + break; + } + + this.save(); +}; + +ReplyHiding.shouldToggleR = function(el) { + var j, ql, hit, quotes; + + if (el.parentNode.hasAttribute('data-pfx')) { + return false; + } + + quotes = $.qsa('#' + el.id + ' > .quotelink', el); + + if (!quotes[0]) { + return false; + } + + hit = this.hiddenRMap[quotes[0].textContent]; + + if (quotes.length === 1 && hit) { + return hit; + } + else { + for (j = 0; ql = quotes[j]; ++j) { + if (!this.hiddenRMap[ql.textContent]) { + return false; + } + } + } + + return hit; +}; + +ReplyHiding.show = function(pid) { + $.removeClass($.id('pc' + pid), 'post-hidden'); + $.id('sa' + pid).removeAttribute('data-hidden'); + + delete ReplyHiding.hidden[pid]; +}; + +ReplyHiding.showR = function(pid, parentPid) { + $.removeClass($.id('pc' + pid), 'post-hidden'); + $.id('sa' + pid).removeAttribute('data-hidden'); + + delete ReplyHiding.hiddenRMap['>>' + pid]; + + if (parentPid) { + delete ReplyHiding.hiddenR[parentPid]; + } +}; + +ReplyHiding.hide = function(pid) { + $.addClass($.id('pc' + pid), 'post-hidden'); + $.id('sa' + pid).setAttribute('data-hidden', pid); + + ReplyHiding.hidden[pid] = Date.now(); +}; + +ReplyHiding.hideR = function(pid, parentPid) { + $.addClass($.id('pc' + pid), 'post-hidden'); + $.id('sa' + pid).setAttribute('data-hidden', pid); + + ReplyHiding.hiddenRMap['>>' + pid] = parentPid; + + if (pid === parentPid) { + ReplyHiding.hiddenR[pid] = Date.now(); + } + + ReplyHiding.hasR = true; +}; + +ReplyHiding.load = function() { + var storage; + + this.hasHiddenR = false; + + if (storage = localStorage.getItem('4chan-hide-r-' + Main.board)) { + this.hidden = JSON.parse(storage); + } + + if (storage = localStorage.getItem('4chan-hide-rr-' + Main.board)) { + this.hiddenR = JSON.parse(storage); + } +}; + +ReplyHiding.purge = function() { + var tid, now; + + now = Date.now(); + + for (tid in this.hidden) { + if (now - this.hidden[tid] > this.threshold) { + delete this.hidden[tid]; + } + } + + for (tid in this.hiddenR) { + if (now - this.hiddenR[tid] > this.threshold) { + delete this.hiddenR[tid]; + } + } + + this.save(); +}; + +ReplyHiding.save = function() { + var i, clr; + + clr = true; + + for (i in this.hidden) { + localStorage.setItem('4chan-hide-r-' + Main.board, + JSON.stringify(this.hidden) + ); + clr = false; + break; + } + + clr && localStorage.removeItem('4chan-hide-r-' + Main.board); + + clr = true; + + for (i in this.hiddenR) { + localStorage.setItem('4chan-hide-rr-' + Main.board, + JSON.stringify(this.hiddenR) + ); + clr = false; + break; + } + + clr && localStorage.removeItem('4chan-hide-rr-' + Main.board); +}; + +/** + * Thread watcher + */ +var ThreadWatcher = {}; + +ThreadWatcher.init = function() { + var cnt, jumpTo, rect, el, awt; + + this.listNode = null; + this.charLimit = 45; + this.watched = {}; + this.blacklisted = {}; + this.isRefreshing = false; + + if (Main.hasMobileLayout) { + el = document.createElement('a'); + el.href = '#'; + el.textContent = 'TW'; + el.addEventListener('click', ThreadWatcher.toggleList, false); + cnt = $.id('settingsWindowLinkMobile'); + cnt.parentNode.insertBefore(el, cnt); + cnt.parentNode.insertBefore(document.createTextNode(' '), cnt); + } + + if (location.hash && (jumpTo = location.hash.split('lr')[1])) { + if (jumpTo = $.id('pc' + jumpTo)) { + if (jumpTo.nextElementSibling) { + jumpTo = jumpTo.nextElementSibling; + if (el = $.id('p' + jumpTo.id.slice(2))) { + $.addClass(el, 'highlight'); + } + } + + rect = jumpTo.getBoundingClientRect(); + + if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) { + window.scrollBy(0, rect.top); + } + } + + if (window.history && history.replaceState) { + history.replaceState(null, '', location.href.split('#', 1)[0]); + } + } + + cnt = document.createElement('div'); + cnt.id = 'threadWatcher'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-trackpos', 'TW-position'); + + if (Main.hasMobileLayout) { + cnt.style.display = 'none'; + } + else { + if (Config['TW-position']) { + cnt.style.cssText = Config['TW-position']; + } + else { + cnt.style.left = '10px'; + cnt.style.top = '380px'; + } + + if (Config.fixedThreadWatcher) { + cnt.style.position = 'fixed'; + } + else { + cnt.style.position = ''; + } + } + + cnt.innerHTML = '
    ' + + (Main.hasMobileLayout ? ('X') : '') + + 'Thread Watcher' + + (UA.hasCORS ? ('R
    ') : '
    '); + + this.listNode = document.createElement('ul'); + this.listNode.id = 'watchList'; + + this.load(); + + if (Config.threadAutoWatcher) { + if (awt = Main.getCookie('4chan_awt')) { + Main.removeCookie('4chan_awt', '.' + $L.d(Main.board), '/' + Main.board + '/'); + + this.add(+awt, Main.board); + this.save(); + } + + if (document.forms.post) { + el = $.el('input'); + el.type = 'hidden'; + el.name = 'awt'; + el.value = '1'; + + document.forms.post.appendChild(el); + } + } + + if (Main.tid) { + this.refreshCurrent(); + } + + this.build(); + + cnt.appendChild(this.listNode); + document.body.appendChild(cnt); + cnt.addEventListener('mouseup', this.onClick, false); + Draggable.set($.id('twHeader')); + window.addEventListener('storage', this.syncStorage, false); + + if (Main.hasMobileLayout) { + if (Main.tid) { + ThreadWatcher.initMobileButtons(); + } + } + else if (!Main.tid && this.canAutoRefresh()) { + this.refresh(); + } +}; + +ThreadWatcher.toggleList = function(e) { + var el = $.id('threadWatcher'); + + e && e.preventDefault(); + + if (!Main.tid && ThreadWatcher.canAutoRefresh()) { + ThreadWatcher.refresh(); + } + + if (el.style.display == 'none') { + el.style.top = (window.pageYOffset + 30) + 'px'; + el.style.display = ''; + } + else { + el.style.display = 'none'; + } +}; + +ThreadWatcher.syncStorage = function(e) { + var key; + + if (!e.key) { + return; + } + + key = e.key.split('-'); + + if (key[0] == '4chan' && key[1] == 'watch' && !key[2] && e.newValue != e.oldValue) { + ThreadWatcher.load(); + ThreadWatcher.build(true); + } +}; + +ThreadWatcher.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-watch')) { + this.watched = JSON.parse(storage); + } + if (storage = localStorage.getItem('4chan-watch-bl')) { + this.blacklisted = JSON.parse(storage); + } +}; + +ThreadWatcher.build = function(rebuildButtons) { + var html, tuid, key, cls; + + html = ''; + + for (key in this.watched) { + tuid = key.split('-'); + html += '
  • × (' + this.watched[key][2] + ') '; + } + else { + html += (cls[0] ? ('class="' + cls.join(' ') + '"') : '') + '>'; + } + } + + html += '/' + tuid[1] + '/ - ' + this.watched[key][0] + '
  • '; + } + + if (rebuildButtons) { + ThreadWatcher.rebuildButtons(); + } + + ThreadWatcher.listNode.innerHTML = html; +}; + +ThreadWatcher.rebuildButtons = function() { + var i, buttons, key, btn; + + buttons = $.cls('wbtn'); + + for (i = 0; btn = buttons[i]; ++i) { + key = btn.getAttribute('data-id') + '-' + Main.board; + if (ThreadWatcher.watched[key]) { + if (!btn.hasAttribute('data-active')) { + btn.src = Main.icons.watched; + btn.setAttribute('data-active', '1'); + } + } + else { + if (btn.hasAttribute('data-active')) { + btn.src = Main.icons.notwatched; + btn.removeAttribute('data-active'); + } + } + } +}; + +ThreadWatcher.initMobileButtons = function() { + var el, cnt, key, ref; + + el = document.createElement('img'); + + key = Main.tid + '-' + Main.board; + + if (ThreadWatcher.watched[key]) { + el.src = Main.icons.watched; + el.setAttribute('data-active', '1'); + } + else { + el.src = Main.icons.notwatched; + } + + el.className = 'extButton wbtn wbtn-' + key; + el.setAttribute('data-cmd', 'watch'); + el.setAttribute('data-id', Main.tid); + el.alt = 'W'; + + cnt = document.createElement('span'); + cnt.className = 'mobileib button'; + + cnt.appendChild(el); + + if (ref = $.cls('navLinks')[0]) { + ref.appendChild(document.createTextNode(' ')); + ref.appendChild(cnt); + } + + if (ref = $.cls('navLinks')[3]) { + ref.appendChild(document.createTextNode(' ')); + ref.appendChild(cnt.cloneNode(true)); + } +}; + +ThreadWatcher.onClick = function(e) { + var t = e.target; + + if (t.hasAttribute('data-id')) { + ThreadWatcher.toggle( + t.getAttribute('data-id'), + t.getAttribute('data-board') + ); + } + else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) { + ThreadWatcher.refreshWithAutoWatch(); + } + else if (t.id == 'twClose') { + ThreadWatcher.toggleList(); + } +}; + +ThreadWatcher.generateLabel = function(sub, com, tid) { + var label; + + if (label = sub) { + label = label.slice(0, this.charLimit); + } + else if (label = com) { + label = label.replace(/(?:
    )+/g, ' ') + .replace(/<[^>]*?>/g, '').slice(0, this.charLimit); + } + else { + label = 'No.' + tid; + } + + return label; +}; + +ThreadWatcher.toggle = function(tid, board) { + var key; + + key = tid + '-' + (board || Main.board); + + if (this.watched[key]) { + this.blacklisted[key] = 1; + delete this.watched[key]; + } + else { + this.add(tid, board, key); + } + + this.save(); + this.load(); + this.build(true); +}; + +ThreadWatcher.add = function(tid, board) { + var key, label, sub, com, lastReply, thread; + + key = tid + '-' + (board || Main.board); + + sub = $.cls('subject', $.id('pi' + tid))[0].textContent; + com = $.id('m' + tid).innerHTML; + + label = this.generateLabel(sub, com, tid); + + if ((thread = $.id('t' + tid)).children[1]) { + lastReply = thread.lastElementChild.id.slice(2); + } + else { + lastReply = tid; + } + + this.watched[key] = [ label, lastReply, 0 ]; +}; + +ThreadWatcher.addRaw = function(post, board) { + var key, label; + + key = post.no + '-' + (board || Main.board); + + if (this.watched[key]) { + return; + } + + label = ThreadWatcher.generateLabel(post.sub, post.com, post.no); + + this.watched[key] = [ label, 0, 0 ]; +}; + +ThreadWatcher.save = function() { + var i; + + ThreadWatcher.sortByBoard(); + + localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched)); + + //StorageSync.sync('4chan-watch'); + + for (i in ThreadWatcher.blacklisted) { + localStorage.setItem('4chan-watch-bl', JSON.stringify(ThreadWatcher.blacklisted)); + //StorageSync.sync('4chan-watch-bl'); + break; + } +}; + +ThreadWatcher.sortByBoard = function() { + var i, self, key, sorted, keys; + + self = ThreadWatcher; + + sorted = {}; + keys = []; + + for (key in self.watched) { + keys.push(key); + } + + keys.sort(function(a, b) { + a = a.split('-')[1]; + b = b.split('-')[1]; + + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }); + + for (i = 0; key = keys[i]; ++i) { + sorted[key] = self.watched[key]; + } + + self.watched = sorted; +}; + +ThreadWatcher.canAutoRefresh = function() { + var time; + + if (time = localStorage.getItem('4chan-tw-timestamp')) { + return Date.now() - (+time) >= 60000; + } + + return true; +}; + +ThreadWatcher.setRefreshTimestamp = function() { + localStorage.setItem('4chan-tw-timestamp', Date.now()); + //StorageSync.sync('4chan-tw-timestamp'); +}; + +ThreadWatcher.refreshWithAutoWatch = function() { + var i, f, count, board, boards, img; + + if (!Config.filter) { + this.refresh(); + return; + } + + Filter.load(); + + boards = {}; + count = 0; + + for (i = 0; f = Filter.activeFilters[i]; ++i) { + if (!f.auto || !f.boards) { + continue; + } + for (board in f.boards) { + if (boards[board]) { + continue; + } + boards[board] = true; + ++count; + } + } + + if (!count) { + this.refresh(); + return; + } + + img = $.id('twPrune'); + img.src = Main.icons.rotate; + this.isRefreshing = true; + + this.fetchCatalogs(boards, count); +}; + +ThreadWatcher.fetchCatalogs = function(boards, count) { + var to, board, catalogs, meta; + + catalogs = {}; + meta = { count: count }; + to = 0; + + for (board in boards) { + setTimeout(ThreadWatcher.fetchCatalog, to, board, catalogs, meta); + to += 200; + } +}; + +ThreadWatcher.fetchCatalog = function(board, catalogs, meta) { + var xhr; + + xhr = new XMLHttpRequest(); + xhr.open('GET', '//a.4cdn.org/' + board + '/catalog.json'); + xhr.onload = function() { + meta.count--; + catalogs[board] = Parser.parseCatalogJSON(this.responseText); + if (!meta.count) { + ThreadWatcher.onCatalogsLoaded(catalogs); + } + }; + xhr.onerror = function() { + meta.count--; + if (!meta.count) { + ThreadWatcher.onCatalogsLoaded(catalogs); + } + }; + xhr.send(null); +}; + +ThreadWatcher.onCatalogsLoaded = function(catalogs) { + var i, j, board, page, pages, threads, thread, key, blacklisted; + + $.id('twPrune').src = Main.icons.refresh; + this.isRefreshing = false; + + blacklisted = {}; + + for (board in catalogs) { + pages = catalogs[board]; + for (i = 0; page = pages[i]; ++i) { + threads = page.threads; + for (j = 0; thread = threads[j]; ++j) { + key = thread.no + '-' + board; + if (this.blacklisted[key]) { + blacklisted[key] = 1; + continue; + } + if (Filter.match(thread, board)) { + this.addRaw(thread, board); + } + } + } + } + + this.blacklisted = blacklisted; + this.build(true); + this.refresh(); +}; + +ThreadWatcher.refresh = function() { + var i, to, key, total, img; + + if (total = $.id('watchList').children.length) { + i = to = 0; + img = $.id('twPrune'); + img.src = Main.icons.rotate; + ThreadWatcher.isRefreshing = true; + ThreadWatcher.setRefreshTimestamp(); + for (key in ThreadWatcher.watched) { + setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null); + to += 200; + } + } +}; + +ThreadWatcher.refreshCurrent = function(rebuild) { + var key, thread, lastReply; + + key = Main.tid + '-' + Main.board; + + if (this.watched[key]) { + if ((thread = $.id('t' + Main.tid)).children[1]) { + lastReply = thread.lastElementChild.id.slice(2); + } + else { + lastReply = Main.tid; + } + if (this.watched[key][1] < lastReply) { + this.watched[key][1] = lastReply; + } + + this.watched[key][2] = 0; + this.watched[key][4] = 0; + this.save(); + + if (rebuild) { + this.build(); + } + } +}; + +ThreadWatcher.setLastRead = function(pid, tid) { + var key = tid + '-' + Main.board; + + if (this.watched[key]) { + this.watched[key][1] = pid; + this.watched[key][2] = 0; + this.watched[key][4] = 0; + this.save(); + this.build(); + } +}; + +ThreadWatcher.onRefreshEnd = function(img) { + img.src = Main.icons.refresh; + this.isRefreshing = false; + this.save(); + this.load(); + this.build(); +}; + +ThreadWatcher.fetch = function(key, img) { + var tuid, xhr, li; + + li = $.id('watch-' + key); + + if (ThreadWatcher.watched[key][1] == -1) { + delete ThreadWatcher.watched[key]; + li.parentNode.removeChild(li); + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + return; + } + + tuid = key.split('-'); // tid, board + + xhr = new XMLHttpRequest(); + xhr.onload = function() { + var i, newReplies, posts, lastReply, trackedReplies, dummy, quotelinks, q, j; + if (this.status == 200) { + posts = Parser.parseThreadJSON(this.responseText); + lastReply = ThreadWatcher.watched[key][1]; + newReplies = 0; + + if (!ThreadWatcher.watched[key][4]) { + trackedReplies = Parser.getTrackedReplies(tuid[1], tuid[0]); + + if (trackedReplies) { + dummy = document.createElement('div'); + } + } + else { + trackedReplies = null; + } + + for (i = posts.length - 1; i >= 1; i--) { + if (posts[i].no <= lastReply) { + break; + } + ++newReplies; + + if (trackedReplies) { + dummy.innerHTML = posts[i].com; + quotelinks = $.cls('quotelink', dummy); + + if (!quotelinks[0]) { + continue; + } + + for (j = 0; q = quotelinks[j]; ++j) { + if (trackedReplies[q.textContent]) { + ThreadWatcher.watched[key][4] = 1; + trackedReplies = null; + break; + } + } + } + } + if (newReplies > ThreadWatcher.watched[key][2]) { + ThreadWatcher.watched[key][2] = newReplies; + } + if (posts[0].archived) { + ThreadWatcher.watched[key][3] = 1; + } + } + else if (this.status == 404) { + ThreadWatcher.watched[key][1] = -1; + } + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + }; + if (img) { + xhr.onerror = xhr.onload; + } + xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json'); + xhr.send(null); +}; + +/** + * Thread expansion + */ +var ThreadExpansion = {}; + +ThreadExpansion.init = function() { + this.enabled = UA.hasCORS; + this.fetchXhr = null; +}; + +ThreadExpansion.expandComment = function(link) { + var ids, tid, pid, abbr; + + if (!(ids = link.getAttribute('href').match(/^(?:thread\/)([0-9]+)#p([0-9]+)$/))) { + return; + } + + tid = ids[1]; + pid = ids[2]; + + abbr = link.parentNode; + abbr.textContent = 'Loading...'; + + $.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json', + { + onload: function() { + var i, msg, post, posts; + + if (this.status == 200) { + msg = $.id('m' + pid); + + posts = Parser.parseThreadJSON(this.responseText); + + if (tid == pid) { + post = posts[0]; + } + else { + for (i = posts.length - 1; i > 0; i--) { + if (posts[i].no == pid) { + post = posts[i]; + break; + } + } + } + + if (post) { + post = Parser.buildHTMLFromJSON(post, Main.board); + + msg.innerHTML = $.cls('postMessage', post)[0].innerHTML; + + if (Parser.prettify) { + Parser.parseMarkup(msg); + } + if (window.math_tags) { + Parser.parseMathOne(msg); + } + } + else { + abbr.textContent = "This post doesn't exist anymore."; + } + } + else if (this.status == 404) { + abbr.textContent = "This thread doesn't exist anymore."; + } + else { + abbr.textContent = 'Connection Error'; + console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText); + } + }, + onerror: function() { + abbr.textContent = 'Connection Error'; + console.log('ThreadExpansion: xhr failed'); + } + } + ); +}; + +ThreadExpansion.toggle = function(tid) { + var thread, msg, expmsg, summary, tmp; + + thread = $.id('t' + tid); + summary = thread.children[1]; + if (thread.hasAttribute('data-truncated')) { + msg = $.id('m' + tid); + expmsg = msg.nextSibling; + } + + if ($.hasClass(thread, 'tExpanded')) { + thread.className = thread.className.replace(' tExpanded', ' tCollapsed'); + summary.children[0].src = Main.icons.plus; + summary.children[1].style.display = 'inline'; + summary.children[2].style.display = 'none'; + if (msg) { + tmp = msg.innerHTML; + msg.innerHTML = expmsg.textContent; + expmsg.textContent = tmp; + } + } + else if ($.hasClass(thread, 'tCollapsed')) { + thread.className = thread.className.replace(' tCollapsed', ' tExpanded'); + summary.children[0].src = Main.icons.minus; + summary.children[1].style.display = 'none'; + summary.children[2].style.display = 'inline'; + if (msg) { + tmp = msg.innerHTML; + msg.innerHTML = expmsg.textContent; + expmsg.textContent = tmp; + } + } + else { + summary.children[0].src = Main.icons.rotate; + if (!ThreadExpansion.fetchXhr) { + ThreadExpansion.fetch(tid); + } + } +}; + +ThreadExpansion.fetch = function(tid) { + ThreadExpansion.fetchXhr = $.get( + '//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json', + { + onload: function() { + var i, p, n, frag, thread, tail, posts, msg, metacap, + expmsg, summary, abbr; + + ThreadExpansion.fetchXhr = null; + + thread = $.id('t' + tid); + summary = thread.children[1]; + + if (this.status == 200) { + tail = $.cls('reply', thread); + + posts = Parser.parseThreadJSON(this.responseText); + + if (!Config.revealSpoilers && posts[0].custom_spoiler) { + Parser.setCustomSpoiler(Main.board, posts[0].custom_spoiler); + } + + frag = document.createDocumentFragment(); + + if (tail[0]) { + tail = +tail[0].id.slice(1); + + for (i = 1; p = posts[i]; ++i) { + if (p.no < tail) { + n = Parser.buildHTMLFromJSON(p, Main.board); + n.className += ' rExpanded'; + frag.appendChild(n); + } + else { + break; + } + } + } + else { + for (i = 1; p = posts[i]; ++i) { + n = Parser.buildHTMLFromJSON(p, Main.board); + n.className += ' rExpanded'; + frag.appendChild(n); + } + } + + msg = $.id('m' + tid); + if ((abbr = $.cls('abbr', msg)[0]) + && /^Comment/.test(abbr.textContent)) { + thread.setAttribute('data-truncated', '1'); + expmsg = document.createElement('div'); + expmsg.style.display = 'none'; + expmsg.textContent = msg.innerHTML; + msg.parentNode.insertBefore(expmsg, msg.nextSibling); + if (metacap = $.cls('capcodeReplies', msg)[0]) { + msg.innerHTML = posts[0].com + '

    '; + msg.appendChild(metacap); + } + else { + msg.innerHTML = posts[0].com; + } + if (Parser.prettify) { + Parser.parseMarkup(msg); + } + if (window.math_tags) { + Parser.parseMathOne(msg); + } + } + + thread.insertBefore(frag, summary.nextSibling); + Parser.parseThread(tid, 1, i - 1); + + thread.className += ' tExpanded'; + summary.children[0].src = Main.icons.minus; + summary.children[1].style.display = 'none'; + summary.children[2].style.display = 'inline'; + } + else if (this.status == 404) { + summary.children[0].src = Main.icons.plus; + summary.children[0].display = 'none'; + summary.children[1].textContent = "This thread doesn't exist anymore."; + } + else { + summary.children[0].src = Main.icons.plus; + console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText); + } + }, + onerror: function() { + ThreadExpansion.fetchXhr = null; + $.id('t' + tid).children[1].children[0].src = Main.icons.plus; + console.log('ThreadExpansion: xhr failed'); + } + } + ); +}; + +/** + * Thread updater + */ +var ThreadUpdater = {}; + +ThreadUpdater.init = function() { + if (!UA.hasCORS || window.thread_archived) { + return; + } + + this.enabled = true; + + this.pageTitle = document.title; + + this.unreadCount = 0; + this.auto = this.hadAuto = false; + + this.delayId = 0; + this.delayIdHidden = 4; + this.delayRange = [ 10, 15, 20, 30, 60, 90, 120, 180, 240, 300 ]; + this.timeLeft = 0; + this.interval = null; + + this.tailSize = window.tailSize || 0; + this.lastUpdated = Date.now(); + this.lastModified = '0'; + this.lastModifiedTail = '0'; + this.lastReply = null; + + this.currentIcon = null; + this.iconPath = '//s.4cdn.org/image/'; + this.iconNode = $.qs('link[rel="shortcut icon"]', document.head); + this.iconNode.type = 'image/x-icon'; + this.defaultIcon = this.iconNode.getAttribute('href').replace(this.iconPath, ''); + + this.deletionQueue = {}; + + if (Config.updaterSound) { + this.audioEnabled = false; + this.audio = document.createElement('audio'); + this.audio.src = '//s.4cdn.org/media/beep.ogg'; + } + + this.hidden = 'hidden'; + this.visibilitychange = 'visibilitychange'; + + this.adsTTL = 900000; + this.adsReloadTs = 0; + + if (typeof document.hidden === 'undefined') { + if ('mozHidden' in document) { + this.hidden = 'mozHidden'; + this.visibilitychange = 'mozvisibilitychange'; + } + else if ('webkitHidden' in document) { + this.hidden = 'webkitHidden'; + this.visibilitychange = 'webkitvisibilitychange'; + } + else if ('msHidden' in document) { + this.hidden = 'msHidden'; + this.visibilitychange = 'msvisibilitychange'; + } + } + + this.initControls(); + + document.addEventListener('scroll', this.onScroll, false); + + if (Config.alwaysAutoUpdate || sessionStorage.getItem('4chan-auto-' + Main.tid)) { + this.start(); + } +}; + +ThreadUpdater.apiUrlFilter = null; + +ThreadUpdater.buildMobileControl = function(el, bottom) { + var wrap, cnt, ctrl, cb, label, oldBtn, btn; + + bottom = (bottom ? 'Bot' : ''); + + wrap = document.createElement('div'); + wrap.className = 'btn-row'; + + // Update button + oldBtn = el.parentNode; + + btn = oldBtn.cloneNode(true); + btn.innerHTML = ''; + + wrap.appendChild(btn); + cnt = el.parentNode.parentNode; + ctrl = document.createElement('span'); + ctrl.className = 'mobileib button'; + + // Auto checkbox + label = document.createElement('label'); + cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.setAttribute('data-cmd', 'auto'); + this['autoNode' + bottom] = cb; + label.appendChild(cb); + label.appendChild(document.createTextNode('Auto')); + ctrl.appendChild(label); + wrap.appendChild(document.createTextNode(' ')); + wrap.appendChild(ctrl); + + // Status label + label = document.createElement('div'); + label.className = 'mobile-tu-status'; + + wrap.appendChild(this['statusNode' + bottom] = label); + + cnt.appendChild(wrap); + + // Remove Update button + oldBtn.parentNode.removeChild(oldBtn); + + if (cnt = $.id('mpostform')) { + cnt.parentNode.style.marginTop = ''; + } +}; + +ThreadUpdater.buildDesktopControl = function(bottom) { + var frag, el, label, navlinks; + + bottom = (bottom ? 'Bot' : ''); + + frag = document.createDocumentFragment(); + + // Update button + frag.appendChild(document.createTextNode(' [')); + el = document.createElement('a'); + el.href = ''; + el.textContent = 'Update'; + el.setAttribute('data-cmd', 'update'); + frag.appendChild(el); + frag.appendChild(document.createTextNode(']')); + + // Auto checkbox + frag.appendChild(document.createTextNode(' [')); + label = document.createElement('label'); + el = document.createElement('input'); + el.type = 'checkbox'; + el.title = 'Fetch new replies automatically'; + el.setAttribute('data-cmd', 'auto'); + this['autoNode' + bottom] = el; + label.appendChild(el); + label.appendChild(document.createTextNode('Auto')); + frag.appendChild(label); + frag.appendChild(document.createTextNode('] ')); + + if (Config.updaterSound) { + // Sound checkbox + frag.appendChild(document.createTextNode(' [')); + label = document.createElement('label'); + el = document.createElement('input'); + el.type = 'checkbox'; + el.title = 'Play a sound on new replies to your posts'; + el.setAttribute('data-cmd', 'sound'); + this['soundNode' + bottom] = el; + label.appendChild(el); + label.appendChild(document.createTextNode('Sound')); + frag.appendChild(label); + frag.appendChild(document.createTextNode('] ')); + } + + // Status label + frag.appendChild( + this['statusNode' + bottom] = document.createElement('span') + ); + + if (bottom) { + navlinks = $.cls('navLinks' + bottom)[0]; + } + else { + navlinks = $.cls('navLinks')[1]; + } + + if (navlinks) { + navlinks.appendChild(frag); + } +}; + +ThreadUpdater.initControls = function() { + // Mobile + if (Main.hasMobileLayout) { + this.buildMobileControl($.id('refresh_top')); + this.buildMobileControl($.id('refresh_bottom'), true); + } + // Desktop + else { + this.buildDesktopControl(); + this.buildDesktopControl(true); + } +}; + +ThreadUpdater.start = function() { + if (this.dead) { + return; + } + this.auto = this.hadAuto = true; + this.autoNode.checked = this.autoNodeBot.checked = true; + this.force = this.updating = false; + if (this.hidden) { + document.addEventListener(this.visibilitychange, + this.onVisibilityChange, false); + } + this.delayId = 0; + this.timeLeft = this.delayRange[0]; + this.pulse(); + sessionStorage.setItem('4chan-auto-' + Main.tid, 1); +}; + +ThreadUpdater.stop = function(manual) { + clearTimeout(this.interval); + this.auto = this.updating = this.force = false; + this.autoNode.checked = this.autoNodeBot.checked = false; + if (this.hidden) { + document.removeEventListener(this.visibilitychange, + this.onVisibilityChange, false); + } + if (manual) { + this.setStatus(''); + this.setIcon(null); + } + sessionStorage.removeItem('4chan-auto-' + Main.tid); +}; + +ThreadUpdater.pulse = function() { + var self = ThreadUpdater; + + if (self.timeLeft === 0) { + self.update(); + } + else { + self.setStatus(self.timeLeft--); + self.interval = setTimeout(self.pulse, 1000); + } +}; + +ThreadUpdater.adjustDelay = function(postCount) +{ + if (postCount === 0) { + if (!this.force) { + if (this.delayId < this.delayRange.length - 1) { + ++this.delayId; + } + } + } + else { + this.delayId = document[this.hidden] ? this.delayIdHidden : 0; + } + this.timeLeft = this.delayRange[this.delayId]; + if (this.auto) { + this.pulse(); + } +}; + +ThreadUpdater.onVisibilityChange = function() { + var self = ThreadUpdater; + + if (document[self.hidden] && self.delayId < self.delayIdHidden) { + self.delayId = self.delayIdHidden; + } + else { + self.delayId = 0; + } + + self.timeLeft = self.delayRange[0]; + clearTimeout(self.interval); + self.pulse(); +}; + +ThreadUpdater.onScroll = function() { + if (ThreadUpdater.hadAuto && + (document.documentElement.scrollHeight + <= (Math.ceil(window.innerHeight + window.pageYOffset)) + && !document[ThreadUpdater.hidden])) { + ThreadUpdater.clearUnread(); + } +}; + +ThreadUpdater.clearUnread = function() { + if (!this.dead) { + this.setIcon(null); + } + if (this.lastReply) { + this.unreadCount = 0; + document.title = this.pageTitle; + $.removeClass(this.lastReply, 'newPostsMarker'); + this.lastReply = null; + } +}; + +ThreadUpdater.forceUpdate = function() { + ThreadUpdater.force = true; + ThreadUpdater.update(); +}; + +ThreadUpdater.toggleAuto = function() { + if (this.updating) { + return; + } + this.auto ? this.stop(true) : this.start(); +}; + +ThreadUpdater.toggleSound = function() { + this.soundNode.checked = this.soundNodeBot.checked = + this.audioEnabled = !this.audioEnabled; +}; + +ThreadUpdater.update = function(full) { + var self, isTail, url; + + self = ThreadUpdater; + + if (self.updating || self.dead) { + return; + } + + clearTimeout(self.interval); + + self.updating = true; + + self.setStatus('Updating...'); + + isTail = !full && self.checkTailUpdate(); + + url = '//a.4cdn.org/' + Main.board + '/thread/' + Main.tid + + (isTail ? '-tail' : '') + '.json'; + + if (self.apiUrlFilter) { + url = self.apiUrlFilter(url); + } + + $.get(url, + { + onload: self.onload, + onerror: self.onerror, + istail: isTail + }, + { + 'If-Modified-Since': isTail ? self.lastModifiedTail : self.lastModified + } + ); +}; + +ThreadUpdater.checkTailUpdate = function() { + var self, nodes, el, dtr, dtp; + + self = ThreadUpdater; + + if (!self.tailSize) { + return false; + } + + nodes = $.cls('replyContainer'); + + el = nodes[nodes.length - self.tailSize]; + + if (!el) { + return true; + } + + el = $.cls('dateTime', el)[0]; + + dtp = (0 | (self.lastUpdated / 1000)) - (+el.getAttribute('data-utc')); + dtr = 0 | ((Date.now() - self.lastUpdated) / 1000); + + return dtp > dtr; +}; + +ThreadUpdater.checkTailValid = function(posts) { + var op = posts[0]; + + if (!op || !op.tail_id) { + ThreadUpdater.tailSize = 0; + return false; + } + + return !!$.id('p' + op.tail_id); +}; + +ThreadUpdater.reloadAds = function() { + if (!window.Danbo || !window.reloadAdsDanbo) { + return; + } + + let self = ThreadUpdater; + + let now = Date.now(); + + if (!self.adsReloadTs) { + self.adsReloadTs = now; + return; + } + + if (now - self.adsReloadTs < self.adsTTL) { + return; + } + + self.adsReloadTs = now; + + window.reloadAdsDanbo(); +}; + +ThreadUpdater.onload = function() { + var i, state, self, nodes, thread, newposts, frag, lastrep, lastid, + op, doc, autoscroll, count, fromQR, lastRepPos; + + self = ThreadUpdater; + nodes = []; + + self.setStatus(''); + + if (this.status == 200) { + newposts = Parser.parseThreadJSON(this.responseText); + + if (this.istail) { + if (!self.checkTailValid(newposts)) { + self.updating = false; + self.update(true); + return; + } + self.lastModifiedTail = this.getResponseHeader('Last-Modified'); + } + else { + if (newposts[0].tail_size !== self.tailSize) { + self.tailSize = newposts[0].tail_size || 0; + } + self.lastModified = this.getResponseHeader('Last-Modified'); + } + + thread = $.id('t' + Main.tid); + + lastrep = thread.children[thread.childElementCount - 1]; + lastid = +lastrep.id.slice(2); + + state = !!newposts[0].archived; + if (window.thread_archived !== undefined && state != window.thread_archived) { + QR.enabled && $.id('quickReply') && QR.lock(); + Main.setThreadState('archived', state); + } + + state = !!newposts[0].closed; + if (state != Main.threadClosed) { + if (newposts[0].archived) { + state = false; + } + else if (QR.enabled && $.id('quickReply')) { + if (state) { + QR.lock(); + } + else { + QR.unlock(); + } + } + Main.setThreadState('closed', state); + } + + state = !!newposts[0].sticky; + if (state != Main.threadSticky) { + Main.setThreadState('sticky', state); + } + + state = !!newposts[0].imagelimit; + if (QR.enabled && state != QR.fileDisabled) { + QR.fileDisabled = state; + } + + if (!Config.revealSpoilers && newposts[0].custom_spoiler) { + Parser.setCustomSpoiler(Main.board, newposts[0].custom_spoiler); + } + + for (i = newposts.length - 1; i >= 0; i--) { + if (newposts[i].no <= lastid) { + break; + } + nodes.push(newposts[i]); + } + + count = nodes.length; + + if (count == 1 && QR.lastReplyId == nodes[0].no) { + fromQR = true; + QR.lastReplyId = null; + } + + if (count) { + doc = document.documentElement; + + autoscroll = ( + Config.autoScroll + && document[self.hidden] + && doc.scrollHeight == Math.ceil(window.innerHeight + window.pageYOffset) + ); + + if (window.chrome && document.activeElement) { + if (document.activeElement.href || document.activeElement.type === 'checkbox') { + document.activeElement.blur(); + } + } + + frag = document.createDocumentFragment(); + for (i = nodes.length - 1; i >= 0; i--) { + frag.appendChild(Parser.buildHTMLFromJSON(nodes[i], Main.board)); + } + thread.appendChild(frag); + + lastRepPos = lastrep.offsetTop; + + Parser.hasYouMarkers = false; + Parser.hasHighlightedPosts = false; + Parser.parseThread(thread.id.slice(1), -nodes.length); + + if (lastRepPos != lastrep.offsetTop) { + window.scrollBy(0, lastrep.offsetTop - lastRepPos); + } + + if (!fromQR) { + if (!self.force && doc.scrollHeight > window.innerHeight) { + if (!self.lastReply && lastid != Main.tid) { + (self.lastReply = lastrep.lastChild).className += ' newPostsMarker'; + } + if (Parser.hasYouMarkers) { + self.setIcon('rep'); + if (self.audioEnabled && document[self.hidden]) { + self.audio.play(); + } + } + else if (Parser.hasHighlightedPosts && self.currentIcon !== 'rep') { + self.setIcon('hl'); + } + else if (self.unreadCount === 0) { + self.setIcon('new'); + } + self.unreadCount += count; + document.title = '(' + self.unreadCount + ') ' + self.pageTitle; + } + else { + self.setStatus(count + ' new post' + (count > 1 ? 's' : '')); + } + } + + if (autoscroll) { + window.scrollTo(0, document.documentElement.scrollHeight); + } + + if (Config.threadWatcher) { + ThreadWatcher.refreshCurrent(true); + } + + if (Config.threadStats) { + op = newposts[0]; + ThreadStats.update(op.replies, op.images, op.unique_ips, op.bumplimit, op.imagelimit); + } + + if (self.force) { + self.reloadAds(); + } + + UA.dispatchEvent('4chanThreadUpdated', { count: count }); + } + else { + self.setStatus('No new posts'); + } + + if (newposts[0].archived) { + self.setError('This thread is archived'); + if (!self.dead) { + self.setIcon('dead'); + window.thread_archived = true; + self.dead = true; + self.stop(); + } + } + } + else if (this.status === 304 || this.status === 0) { + self.setStatus('No new posts'); + } + else if (this.status === 404) { + if (this.istail) { + self.updating = false; + self.update(true); + return; + } + self.setIcon('dead'); + self.setError('This thread has been pruned or deleted'); + self.dead = true; + self.stop(); + return; + } + + self.lastUpdated = Date.now(); + self.adjustDelay(nodes.length); + self.updating = self.force = false; +}; + +ThreadUpdater.onerror = function() { + var self = ThreadUpdater; + + if (UA.isOpera && !this.statusText && this.status === 0) { + self.setStatus('No new posts'); + } + else { + self.setError('Connection Error'); + } + + self.lastUpdated = Date.now(); + self.adjustDelay(0); + self.updating = self.force = false; +}; + +ThreadUpdater.setStatus = function(msg) { + this.statusNode.textContent = this.statusNodeBot.textContent = msg; +}; + +ThreadUpdater.setError = function(msg) { + this.statusNode.innerHTML + = this.statusNodeBot.innerHTML + = '' + msg + ''; +}; + +ThreadUpdater.setIcon = function(type) { + var icon; + + if (type === null) { + icon = this.defaultIcon; + } + else { + icon = this.icons[Main.type + type]; + } + + this.currentIcon = type; + this.iconNode.href = this.iconPath + icon; + document.head.appendChild(this.iconNode); +}; + +ThreadUpdater.icons = { + wsnew: 'favicon-ws-newposts.ico', + nwsnew: 'favicon-nws-newposts.ico', + wsrep: 'favicon-ws-newreplies.ico', + nwsrep: 'favicon-nws-newreplies.ico', + wsdead: 'favicon-ws-deadthread.ico', + nwsdead: 'favicon-nws-deadthread.ico', + wshl: 'favicon-ws-newfilters.ico', + nwshl: 'favicon-nws-newfilters.ico' +}; + +/** + * Thread stats + */ +var ThreadStats = {}; + +ThreadStats.init = function() { + var cnt; + + this.nodeTop = document.createElement('div'); + this.nodeTop.className = 'thread-stats'; + + if (!Main.hasMobileLayout) { + this.nodeBot = this.nodeTop.cloneNode(false); + + cnt = $.cls('navLinks'); + cnt[1] && cnt[1].appendChild(this.nodeTop); + cnt[2] && cnt[2].appendChild(this.nodeBot); + } + else { + this.nodeBot = {}; + + cnt = $.cls('navLinks'); + if (cnt[0]) { + cnt = cnt[cnt.length - 1].nextElementSibling; + cnt.parentNode.insertBefore(this.nodeTop, cnt); + } + } + + this.pageNumber = null; + this.update(null, null, null, window.bumplimit, window.imagelimit); + + if (!window.thread_archived) { + this.updatePageNumber(); + this.pageInterval = setInterval(this.updatePageNumber, 3 * 60000); + } +}; + +ThreadStats.update = function(replies, images, ips, isBumpFull, isImageFull) { + var stats; + + if (replies === null) { + replies = $.cls('replyContainer').length; + images = $.cls('fileText').length - ($.id('fT' + Main.tid) ? 1 : 0); + } + + stats = []; + + if (Main.threadSticky) { + stats.push('Sticky'); + } + + if (window.thread_archived) { + stats.push('Archived'); + } + else if (Main.threadClosed) { + stats.push('Closed'); + } + + if (isBumpFull) { + stats.push('' + replies + ''); + } + else { + stats.push('' + replies + ''); + } + + if (isImageFull) { + stats.push('' + images + ''); + } + else { + stats.push('' + images + ''); + } + + if (!window.thread_archived) { + if (window.unique_ips) { + stats.push('' + (ips || window.unique_ips) + ''); + } + stats.push('' + (this.pageNumber || '?') + ''); + } + + this.nodeTop.innerHTML = this.nodeBot.innerHTML + = stats.join(' / '); +}; + +ThreadStats.updatePageNumber = function() { + $.get('//a.4cdn.org/' + Main.board + '/threads.json', + { + onload: ThreadStats.onCatalogLoad, + onerror: ThreadStats.onCatalogError + } + ); +}; + +ThreadStats.onCatalogLoad = function() { + var self, i, j, k, n, page, post, threads, catalog, tid, nodes; + + self = ThreadStats; + + if (this.status == 200) { + tid = +Main.tid; + catalog = JSON.parse(this.responseText); + for (i = 0; page = catalog[i]; ++i) { + threads = page.threads; + for (j = 0; post = threads[j]; ++j) { + if (post.no == tid) { + nodes = $.cls('ts-page'); + for (k = 0; n = nodes[k]; ++k) { + n.textContent = page.page; + } + self.pageNumber = page.page; + return; + } + } + } + clearInterval(self.pageInterval); + } + else { + ThreadStats.onCatalogError(); + } +}; + +ThreadStats.onCatalogError = function() { + console.log('ThreadStats: couldn\'t get the catalog (' + this.status + ')'); +}; + +/** + * Filter + */ +var Filter = {}; + +Filter.init = function() { + this.entities = document.createElement('div'); + Filter.load(); +}; + +Filter.onClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'filters-add': + Filter.add(); + break; + case 'filters-save': + Filter.save(); + Filter.close(); + break; + case 'filters-close': + Filter.close(); + break; + case 'filters-palette': + Filter.openPalette(e.target); + break; + case 'filters-palette-close': + Filter.closePalette(); + break; + case 'filters-palette-clear': + Filter.clearPalette(); + break; + case 'filters-up': + Filter.moveUp(e.target.parentNode.parentNode); + break; + case 'filters-del': + Filter.remove(e.target.parentNode.parentNode); + break; + case 'filters-help-open': + Filter.openHelp(); + break; + case 'filters-help-close': + Filter.closeHelp(); + break; + } + } +}; + +Filter.onPaletteClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'palette-pick': + Filter.pickColor(e.target); + break; + case 'palette-clear': + Filter.pickColor(e.target, true); + break; + case 'palette-close': + Filter.closePalette(); + break; + } + } +}; + +Filter.match = function(post, board) { + var i, com, f, filters, hit; + + hit = false; + filters = Filter.activeFilters; + + for (i = 0; f = filters[i]; ++i) { + // boards + if (!f.boards[board]) { + continue; + } + // tripcode + if (f.type === 0) { + if (f.pattern === post.trip) { + hit = true; + break; + } + } + // name + else if (f.type === 1) { + if (f.pattern === post.name) { + hit = true; + break; + } + } + // comment + else if (f.type === 2 && post.com) { + if (com === undefined) { + this.entities.innerHTML + = post.com.replace(/
    /g, '\n').replace(/[<[^>]+>/g, ''); + com = this.entities.textContent; + } + if (f.pattern.test(com)) { + hit = true; + break; + } + } + // user id + else if (f.type === 4) { + if (f.pattern === post.id) { + hit = true; + break; + } + } + // subject + else if (f.type === 5) { + if (f.pattern.test(post.sub)) { + hit = true; + break; + } + } + // filename + else if (f.type === 6) { + if (f.pattern.test(post.filename)) { + hit = true; + break; + } + } + } + + return hit; +}; + +Filter.exec = function(cnt, pi, msg, tid) { + var i, el, trip, name, com, uid, sub, fname, f, filters, hit, currentBoard; + + if (Parser.trackedReplies && Parser.trackedReplies['>>' + pi.id.slice(2)]) { + return false; + } + + currentBoard = Main.board; + filters = Filter.activeFilters; + hit = false; + + for (i = 0; f = filters[i]; ++i) { + // boards + if (f.boards && !f.boards[currentBoard]) { + continue; + } + // tripcode + if (f.type === 0) { + if ((trip !== undefined || (trip = pi.getElementsByClassName('postertrip')[0]) + ) && f.pattern == trip.textContent) { + hit = true; + break; + } + } + // name + else if (f.type === 1) { + if ((name || (name = pi.getElementsByClassName('name')[0])) + && f.pattern == name.textContent) { + hit = true; + break; + } + } + // comment + else if (f.type === 2) { + if (com === undefined) { + this.entities.innerHTML + = msg.innerHTML.replace(/
    /g, '\n').replace(/[<[^>]+>/g, ''); + com = this.entities.textContent; + } + if (f.pattern.test(com)) { + hit = true; + break; + } + } + // user id + else if (f.type === 4) { + if ((uid || + ((uid = pi.getElementsByClassName('posteruid')[0]) + && (uid = uid.firstElementChild.textContent) + ) + ) && f.pattern == uid) { + hit = true; + break; + } + } + // subject + else if (!Main.tid && f.type === 5) { + if ((sub || + ((sub = pi.getElementsByClassName('subject')[0]) + && (sub = sub.textContent) + ) + ) && f.pattern.test(sub)) { + hit = true; + break; + } + } + // filename + else if (f.type === 6) { + if (fname === undefined) { + if ((fname = pi.parentNode.getElementsByClassName('fileText')[0])) { + fname = fname.firstElementChild.textContent; + } + else { + fname = ''; + } + } + if (f.pattern.test(fname)) { + hit = true; + break; + } + } + } + + if (hit) { + if (f.hide) { + if (tid && Config.hideStubs && !$.cls('stickyIcon', cnt)[0]) { + cnt.style.display = cnt.nextElementSibling.style.display = 'none'; + } + else { + cnt.className += ' post-hidden'; + el = document.createElement('span'); + if (!tid) { + el.textContent = '[View]'; + el.setAttribute('data-filtered', '1'); + el.setAttribute('data-cmd', 'unfilter'); + } + else { + el.innerHTML = '[View]'; + } + el.className = 'filter-preview'; + pi.appendChild(el); + } + return true; + } + else { + cnt.className += ' filter-hl'; + cnt.style.boxShadow = '-3px 0 ' + f.color; + Parser.hasHighlightedPosts = true; + } + } + return false; +}; + +Filter.unfilter = function(t) { + var cnt = t.parentNode.parentNode; + + QuotePreview.remove(); + $.removeClass(cnt, 'post-hidden'); + t.parentNode.removeChild(t); +}; + +Filter.load = function() { + var i, j, f, rawFilters, rawPattern, fid, regexEscape, regexType, + wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard, + tmp, boards, pattern, match; + + this.activeFilters = []; + + if (!(rawFilters = localStorage.getItem('4chan-filters'))) { + return; + } + + rawFilters = JSON.parse(rawFilters); + + regexEscape = new RegExp('(\\' + + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\') + + ')', 'g'); + regexType = /^\/(.*)\/(i?)$/; + wordSepS = '(?=.*\\b'; + wordSepE = '\\b)'; + regexWildcard = /\\\*/g; + replaceWildcard = '[^\\s]*'; + + try { + for (fid = 0; f = rawFilters[fid]; ++fid) { + if (f.active && f.pattern !== '') { + // Boards + if (f.boards) { + tmp = f.boards.split(/[^a-z0-9]+/i); + boards = {}; + for (i = 0; j = tmp[i]; ++i) { + boards[j] = true; + } + } + else { + boards = false; + } + + rawPattern = f.pattern; + // Name, Tripcode or ID, string comparison + if (!f.type || f.type == 1 || f.type == 4) { + pattern = rawPattern; + } + // /RegExp/ + else if (match = rawPattern.match(regexType)) { + pattern = new RegExp(match[1], match[2]); + } + // "Exact match" + else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') { + pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); + } + // Full words, AND operator + else { + words = rawPattern.split(' '); + pattern = ''; + for (i = 0, j = words.length; i < j; ++i) { + inner = words[i] + .replace(regexEscape, '\\$1') + .replace(regexWildcard, replaceWildcard); + pattern += wordSepS + inner + wordSepE; + } + pattern = new RegExp('^' + pattern, 'im'); + } + //console.log('Resulting pattern: ' + pattern); + this.activeFilters.push({ + type: f.type, + pattern: pattern, + boards: boards, + color: f.color, + hide: f.hide, + auto: f.auto + }); + } + } + } + catch (e) { + alert('There was an error processing one of the filters: ' + + e + ' in: ' + rawPattern); + } +}; + +Filter.addSelection = function() { + var text, type, node, sel = UA.getSelection(true); + + if (Filter.open() === false) { + return; + } + + if (typeof sel == 'string') { + text = sel.trim(); + } + else { + node = sel.anchorNode.parentNode; + text = sel.toString().trim(); + + if ($.hasClass(node, 'name')) { + type = 1; + } + else if ($.hasClass(node, 'postertrip')) { + type = 0; + } + else if ($.hasClass(node, 'subject')) { + type = 5; + } + else if ($.hasClass(node, 'posteruid') || $.hasClass(node, 'hand')) { + type = 4; + } + else if ($.hasClass(node, 'fileText')) { + type = 6; + } + else { + type = 2; + } + } + + Filter.add(text, type); +}; + +Filter.openHelp = function() { + var cnt; + + if ($.id('filtersHelp')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'filtersHelp'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'filters-help-close'); + cnt.innerHTML = '\ +
    Filters & Highlights Help\ +Close
    \ +

    Tripcode, Name and ID filters:

    \ +
    • Those use simple string comparison.
    • \ +
    • Type them exactly as they appear on 4chan, including the exclamation mark for tripcode filters.
    • \ +
    • Example: !Ep8pui8Vw2
    \ +

    Comment, Subject and E-mail filters:

    \ +
    • Matching whole words:
    • \ +
    • feel — will match "feel" but not "feeling". This search is case-insensitive.
    \ +
    • AND operator:
    • \ +
    • feel girlfriend — will match "feel" AND "girlfriend" in any order.
    \ +
    • Exact match:
    • \ +
    • "that feel when" — place double quotes around the pattern to search for an exact string
    \ +
    • Wildcards:
    • \ +
    • feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc…
    • \ +
    • idolm*ster — this can match "idolmaster" or "idolm@ster", etc…
    \ +
    • Regular expressions:
    • \ +
    • /feel when no (girl|boy)friend/i
    • \ +
    • /^(?!.*touhou).*$/i — NOT operator.
    • \ +
    • /^>/ — comments starting with a quote.
    • \ +
    • /^$/ — comments with no text.
    \ +

    Colors:

    \ +
    • The color field can accept any valid CSS color:
    • \ +
    • red, #0f0, #00ff00, rgba( 34, 12, 64, 0.3), etc…
    \ +

    Boards:

    \ +
    • A space separated list of boards on which the filter will be active. Leave blank to apply to all boards.
    \ +

    Auto-watching:

    \ +
    • Enabling the "Auto" option will automatically add matched threads to the Thread Watcher when it is manually refreshed. This only works when the "Boards" field is not empty, and searches catalog JSON for the selected boards(s).
    \ +

    Shortcut:

    \ +
    • If you have Keyboard shortcuts enabled, pressing F will add the selected text to your filters.
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); +}; + +Filter.closeHelp = function() { + var cnt; + + if (cnt = $.id('filtersHelp')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Filter.open = function() { + var i, f, cnt, rawFilters, filterList; + + if ($.id('filtersMenu')) { + return false; + } + + cnt = document.createElement('div'); + cnt.id = 'filtersMenu'; + cnt.className = 'UIPanel'; + cnt.style.display = 'none'; + cnt.setAttribute('data-cmd', 'filters-close'); + cnt.innerHTML = '\ +
    Filters & Highlights\ +HelpClose
    \ +\ +\ +\ +\ +\ +\ +\ +\ +\ +\ +
    OnPatternBoardsTypeColorAutoHideDel
    \ +\ +\ +
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); + + filterList = $.id('filter-list'); + + if (rawFilters = localStorage.getItem('4chan-filters')) { + rawFilters = JSON.parse(rawFilters); + for (i = 0; f = rawFilters[i]; ++i) { + filterList.appendChild(this.buildEntry(f, i)); + } + } + + cnt.style.display = ''; +}; + +Filter.close = function() { + var cnt; + + if (cnt = $.id('filtersMenu')) { + this.closePalette(); + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Filter.moveUp = function(el) { + var prev; + + if (prev = el.previousElementSibling) { + el.parentNode.insertBefore(el, prev); + } +}; + +Filter.add = function(pattern, type, boards) { + var filter, id, el; + + filter = { + active: true, + type: type || 0, + pattern: pattern || '', + boards: boards || '', + color: '', + auto: false, + hide: false + }; + + id = this.getNextFilterId(); + el = this.buildEntry(filter, id); + + $.id('filter-list').appendChild(el); + $.cls('fPattern', el)[0].focus(); +}; + +Filter.remove = function(tr) { + $.id('filter-list').removeChild(tr); +}; + +Filter.save = function() { + var i, rawFilters, entries, tr, f, color, type; + + rawFilters = []; + entries = $.id('filter-list').children; + + for (i = 0; tr = entries[i]; ++i) { + type = tr.children[4].firstChild; + + f = { + active: tr.children[1].firstChild.checked, + pattern: tr.children[2].firstChild.value, + boards: tr.children[3].firstChild.value, + type: +type.options[type.selectedIndex].value, + auto: tr.children[6].firstChild.checked, + hide: tr.children[7].firstChild.checked + }; + + color = tr.children[5].firstChild; + + if (!color.hasAttribute('data-nocolor')) { + f.color = color.style.backgroundColor; + } + + rawFilters.push(f); + } + + if (rawFilters[0]) { + localStorage.setItem('4chan-filters', JSON.stringify(rawFilters)); + } + else { + localStorage.removeItem('4chan-filters'); + } +}; + +Filter.getNextFilterId = function() { + var i, j, max, entries = $.id('filter-list').children; + + if (!entries.length) { + return 0; + } + else { + max = 0; + for (i = 0; j = entries[i]; ++i) { + j = +j.id.slice(7); + if (j > max) { + max = j; + } + } + return max + 1; + } +}; + +Filter.buildEntry = function(filter, id) { + var tr, html, sel; + + tr = document.createElement('tr'); + tr.id = 'filter-' + id; + + html = ''; + + // Move up + html += ''; + + // On + html += '' : '>'); + + // Pattern + html += ''; + + // Boards + html += ''; + + // FIXME + if (filter.type === 3) { + filter.type = 4; + } + + // Type + sel = [ '', '', '', '', '', '', '' ]; + sel[filter.type] = ' selected="selected"'; + + html += ''; + + // Color + html += ''; + } + html += ''; + + // Auto + html += '' : '>'); + + // Hide + html += '' : '>'); + + // Del + html += '×'; + + tr.innerHTML = html; + + return tr; +}; + +Filter.buildPalette = function(id) { + var i, j, cnt, html, colors, rowCount, colCount; + + colors = [ + ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00'], + ['#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB'], + ['#00A550', '#007FFF', '#AF0A0F', '#B5BD68'] + ]; + + rowCount = colors.length; + colCount = colors[0].length; + + html = '
    '; + + for (i = 0; i < rowCount; ++i) { + html += ''; + for (j = 0; j < colCount; ++j) { + html += ''; + } + html += ''; + } + + html += '
    Custom\ +
    \ +
    \ +[Close]\ +[Clear]
    '; + + cnt = document.createElement('div'); + cnt.id = 'filter-palette'; + cnt.setAttribute('data-target', id); + cnt.setAttribute('data-cmd', 'palette-close'); + cnt.className = 'UIMenu'; + cnt.innerHTML = html; + + return cnt; +}; + +Filter.openPalette = function(target) { + var el, pos, id, picker; + + Filter.closePalette(); + + pos = target.getBoundingClientRect(); + id = target.parentNode.parentNode.id.slice(7); + + el = Filter.buildPalette(id); + document.body.appendChild(el); + + $.id('filter-palette').addEventListener('click', Filter.onPaletteClick, false); + $.id('palette-custom-input').addEventListener('keyup', Filter.setCustomColor, false); + + picker = el.firstElementChild; + picker.style.cssText = 'top:' + pos.top + 'px;left:' + + (pos.left - picker.clientWidth - 10) + 'px;'; +}; + +Filter.closePalette = function() { + var el; + + if (el = $.id('filter-palette')) { + $.id('filter-palette').removeEventListener('click', Filter.onPaletteClick, false); + $.id('palette-custom-input').removeEventListener('keyup', Filter.setCustomColor, false); + el.parentNode.removeChild(el); + } +}; + +Filter.pickColor = function(el, clear) { + var id, target; + + id = $.id('filter-palette').getAttribute('data-target'); + target = $.id('filter-' + id); + + if (!target) { + return; + } + + target = $.cls('colorbox', target)[0]; + + if (clear === true) { + target.setAttribute('data-nocolor', '1'); + target.innerHTML = '∕'; + target.style.background = ''; + } + else { + target.removeAttribute('data-nocolor'); + target.innerHTML = ''; + target.style.background = el.style.backgroundColor; + } + + Filter.closePalette(); +}; + +Filter.setCustomColor = function() { + var input, box; + + input = $.id('palette-custom-input'); + box = $.id('palette-custom-ok'); + + box.style.backgroundColor = input.value; +}; + +/** + * ID colors + */ +var IDColor = { + css: 'padding: 0 5px; border-radius: 6px; font-size: 0.8em;', + ids: {} +}; + +IDColor.init = function() { + var style; + + if (window.user_ids) { + this.enabled = true; + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = '.posteruid .hand {' + this.css + '}'; + document.head.appendChild(style); + } +}; + +IDColor.compute = function(str) { + var rgb, hash; + + rgb = []; + hash = $.hash(str); + + rgb[0] = (hash >> 24) & 0xFF; + rgb[1] = (hash >> 16) & 0xFF; + rgb[2] = (hash >> 8) & 0xFF; + rgb[3] = ((rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114)) > 125; + + this.ids[str] = rgb; + + return rgb; +}; + +IDColor.apply = function(uid) { + var rgb; + + rgb = IDColor.ids[uid.textContent] || IDColor.compute(uid.textContent); + uid.style.cssText = '\ + background-color: rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ');\ + color: ' + (rgb[3] ? 'black;' : 'white;'); +}; + +IDColor.applyRemote = function(uid) { + this.apply(uid); + uid.style.cssText += this.css; +}; + +/** + * SWF embed + */ +var SWFEmbed = {}; + +SWFEmbed.init = function() { + if (Main.tid) { + this.processThread(); + } + else if (!Main.hasMobileLayout) { + this.processIndex(); + } +}; + +SWFEmbed.processThread = function() { + var fileText, el; + + fileText = $.id('fT' + Main.tid); + + if (!fileText) { + return; + } + + el = document.createElement('a'); + el.href = 'javascript:;'; + el.textContent = 'Embed'; + el.addEventListener('click', SWFEmbed.toggleThread, false); + + fileText.appendChild(document.createTextNode('-[')); + fileText.appendChild(el); + fileText.appendChild(document.createTextNode(']')); +}; + +SWFEmbed.processIndex = function() { + var i, tr, el, cnt, nodes, srcIndex, src; + + srcIndex = 2; + + cnt = $.cls('postblock')[0]; + + if (!cnt) { + return; + } + + tr = cnt.parentNode; + + el = document.createElement('td'); + el.className = 'postblock'; + tr.insertBefore(el, tr.children[srcIndex].nextElementSibling); + + cnt = $.cls('flashListing')[0]; + + if (!cnt) { + return; + } + + nodes = $.tag('tr', cnt); + + for (i = 1; tr = nodes[i]; ++i) { + src = tr.children[srcIndex].firstElementChild; + el = document.createElement('td'); + el.innerHTML = '[Embed]'; + el.firstElementChild.addEventListener('click', SWFEmbed.embedIndex, false); + tr.insertBefore(el, tr.children[srcIndex].nextElementSibling); + } +}; + +SWFEmbed.toggleThread = function(e) { + var cnt, link, el, post, maxWidth, ratio, width, height; + + if (cnt = $.id('swf-embed')) { + cnt.parentNode.removeChild(cnt); + e.target.textContent = 'Embed'; + return; + } + + link = $.tag('a', e.target.parentNode)[0]; + + maxWidth = document.documentElement.clientWidth - (!Main.hasMobileLayout ? 100 : 30); + + width = +link.getAttribute('data-width'); + height = +link.getAttribute('data-height'); + + if (width > maxWidth) { + ratio = width / height; + width = maxWidth; + height = Math.round(maxWidth / ratio); + } + + cnt = document.createElement('div'); + cnt.id = 'swf-embed'; + + el = SWFEmbed.getFrameNode(link.href, width, height); + + cnt.appendChild(el); + + post = $.id('m' + Main.tid); + post.insertBefore(cnt, post.firstChild); + + $.cls('thread')[0].scrollIntoView(true); + + e.target.textContent = 'Remove'; +}; + +SWFEmbed.embedIndex = function(e) { + var el, cnt, header, icon, backdrop, width, height, cntWidth, cntHeight, + maxWidth, maxHeight, docWidth, docHeight, margins, headerHeight, fileName, + ratio; + + e.preventDefault(); + + margins = 10; + headerHeight = 20; + + el = e.target.parentNode.parentNode.children[2].firstElementChild; + + fileName = el.getAttribute('title') || el.textContent; + + cntWidth = width = +el.getAttribute('data-width'); + cntHeight = height = +el.getAttribute('data-height'); + + docWidth = document.documentElement.clientWidth; + docHeight = document.documentElement.clientHeight; + + maxWidth = docWidth - margins; + maxHeight = docHeight - margins - headerHeight; + + ratio = width / height; + + if (cntWidth > maxWidth) { + cntWidth = maxWidth; + cntHeight = Math.round(maxWidth / ratio); + } + + if (cntHeight > maxHeight) { + cntHeight = maxHeight; + cntWidth = Math.round(maxHeight * ratio); + } + + el = SWFEmbed.getFrameNode(e.target.href, cntWidth, cntHeight); + + cnt = document.createElement('div'); + cnt.style.position = 'fixed'; + cnt.style.width = cntWidth + 'px'; + cnt.style.height = cntHeight + 'px'; + cnt.style.top = '50%'; + cnt.style.left = '50%'; + cnt.style.marginTop = (-cntHeight / 2 - headerHeight / 2) + 'px'; + cnt.style.marginLeft = (-cntWidth / 2) + 'px'; + cnt.style.background = 'white'; + + header = document.createElement('div'); + header.id = 'swf-embed-header'; + header.className = 'postblock'; + header.textContent = fileName + ', ' + width + 'x' + height; + + icon = document.createElement('img'); + icon.id = 'swf-embed-close'; + icon.className = 'pointer'; + icon.src = Main.icons.cross; + + header.appendChild(icon); + + cnt.appendChild(header); + cnt.appendChild(el); + + backdrop = document.createElement('div'); + backdrop.id = 'swf-embed'; + backdrop.style.cssText = 'width: 100%; height: 100%; position: fixed;\ + top: 0; left: 0; background: rgba(128, 128, 128, 0.5)'; + + backdrop.appendChild(cnt); + backdrop.addEventListener('click', SWFEmbed.onBackdropClick, false); + + document.body.appendChild(backdrop); +}; + +SWFEmbed.getFrameNode = function(file_url, width, height) { + var el, filename; + + filename = file_url.replace(/^https:\/\/i\.4cdn\.org\/f\//, ''); + + el = document.createElement('iframe'); + + el.setAttribute('allow', 'autoplay; fullscreen'); + el.setAttribute('sandbox', 'allow-scripts allow-same-origin'); + el.setAttribute('scrolling', 'no'); + el.setAttribute('frameborder', '0'); + el.setAttribute('width', +width); + el.setAttribute('height', +height); + + el.src = `//s.4cdn.org/media/flash/embed.html?4#${+width},${+height},${filename},1`; + + return el; +}; + +SWFEmbed.onBackdropClick = function(e) { + var backdrop = $.id('swf-embed'); + + if (e.target === backdrop || e.target.id == 'swf-embed-close') { + backdrop.removeEventListener('click', SWFEmbed.onBackdropClick, false); + backdrop.parentNode.removeChild(backdrop); + } +}; + +/** + * Linkify + */ +var Linkify = { + init: function() { + this.probeRe = /(?:^|[^\B"])https?:\/\/[-.a-z0-9]+\.[a-z]{2,4}/; + this.linkRe = /(^|[^\B"])(https?:\/\/[-.a-z0-9\u200b]+\.[a-z\u200b]{2,15}(?:\/[^\s<>]*)?)/ig; + this.punct = /[:!?,.'"]+$/g; + this.derefer = '//sys.' + $L.d(Main.board) + '/derefer?url='; + }, + + exec: function(el) { + if (!this.probeRe.test(el.innerHTML)) { + return; + } + + el.innerHTML = el.innerHTML + .replace(//g, '\u200b') + .replace(this.linkRe, this.funk) + .replace(/\u200b/g, ''); + }, + + funk: function(match, pfx, url, o, str) { + var m, mm, end, len, sfx; + + m = o + match.length; + + if (str.slice(m, m + 4) === '') { + return match; + } + + end = len = url.length; + + if (m = url.match(Linkify.punct)) { + end -= m[0].length; + } + + if (m = url.match(/\)+$/g)) { + mm = m[0].length; + if (m = url.match(/\(/g)) { + mm = mm - m.length; + if (mm > 0) { + end -= mm; + } + } + else { + end -= mm; + } + } + + if (end < len) { + sfx = url.slice(end); + url = url.slice(0, end); + } + else { + sfx = ''; + } + + return pfx + '' + + url + '' + sfx; + } +}; + +/** + * Media + */ +var Media = {}; + +Media.init = function() { + this.matchSC = /(?:soundcloud\.com|snd\.sc)\/[^\s<]+(?:)?[^\s<]*/g; + this.matchYT = /(?:youtube\.com\/watch\?[^\s]*?v=|youtu\.be\/)[^\s<]+(?:)?[^\s<]*(?:)?[^\s<]*(?:)?[^\s<]*/g; + this.toggleYT = /(?:v=|\.be\/)([a-zA-Z0-9_-]{11})/; + this.timeYT = /[\?#&]t=([ms0-9]+)/; + + this.map = { + yt: this.toggleYouTube, + sc: this.toggleSoundCloud, + }; +}; + +Media.parseSoundCloud = function(msg) { + msg.innerHTML = msg.innerHTML.replace(this.matchSC, this.replaceSoundCloud); +}; + +Media.replaceSoundCloud = function(link, o, str) { + var pfx; + + if (Config.linkify) { + if (str[o + link.length - 1] === '"') { + return link; + } + else { + pfx = link + ''; + } + } + else { + pfx = '' + link + ''; + } + + return pfx + ' [Embed]'; +}; + +Media.toggleSoundCloud = function(node) { + var xhr, url; + + if (node.textContent == 'Remove') { + node.parentNode.removeChild(node.nextElementSibling); + node.textContent = 'Embed'; + } + else if (node.textContent == 'Embed') { + url = node.previousElementSibling.textContent; + + xhr = new XMLHttpRequest(); + xhr.open('GET', '//soundcloud.com/oembed?show_artwork=false&' + + 'maxwidth=500px&show_comments=false&format=json&url=' + + 'http://' + url.replace(/^https?:\/\//i, '')); + xhr.onload = function() { + var el; + + if (this.status == 200 || this.status == 304) { + el = document.createElement('div'); + el.className = 'media-embed'; + el.innerHTML = JSON.parse(this.responseText).html; + node.parentNode.insertBefore(el, node.nextElementSibling); + node.textContent = 'Remove'; + } + else { + node.textContent = 'Error'; + console.log('SoundCloud Error (HTTP ' + this.status + ')'); + } + }; + node.textContent = 'Loading...'; + xhr.send(null); + } +}; + +Media.parseYouTube = function(msg) { + msg.innerHTML = msg.innerHTML.replace(this.matchYT, this.replaceYouTube); +}; + +Media.replaceYouTube = function(link, o, str) { + var pfx; + + if (Config.linkify) { + if (str[o + link.length - 1] === '"') { + return link; + } + else { + pfx = link + ''; + } + } + else { + pfx = '' + link + ''; + } + + return pfx + ' [' + + (!Main.hasMobileLayout ? 'Embed' : 'Open') + ']'; +}; + +Media.showYTPreview = function(link) { + var cnt, img, vid, aabb, x, y, tw, th, pad; + + tw = 320; th = 180; pad = 5; + + aabb = link.getBoundingClientRect(); + + vid = link.previousElementSibling.textContent.match(this.toggleYT)[1]; + + if (aabb.right + tw + pad > $.docEl.clientWidth) { + x = aabb.left - tw - pad; + } + else { + x = aabb.right + pad; + } + + y = aabb.top - th / 2 + aabb.height / 2; + + img = document.createElement('img'); + img.width = tw; + img.height = th; + img.alt = ''; + img.src = '//i1.ytimg.com/vi/' + encodeURIComponent(vid) + '/mqdefault.jpg'; + + cnt = document.createElement('div'); + cnt.id = 'yt-preview'; + cnt.className = 'reply'; + cnt.style.left = (x + window.pageXOffset) + 'px'; + cnt.style.top = (y + window.pageYOffset) + 'px'; + + cnt.appendChild(img); + + document.body.appendChild(cnt); +}; + +Media.removeYTPreview = function() { + var el; + + if (el = $.id('yt-preview')) { + document.body.removeChild(el); + } +}; + +Media.toggleYouTube = function(node) { + var vid, time, el, url; + + if (node.textContent == 'Remove') { + node.parentNode.removeChild(node.nextElementSibling); + node.textContent = 'Embed'; + } + else { + url = node.previousElementSibling.textContent; + vid = url.match(this.toggleYT); + time = url.match(this.timeYT); + + if (vid && (vid = vid[1])) { + vid = encodeURIComponent(vid); + + if (time && (time = time[1])) { + vid += '?start=' + encodeURIComponent(time); + } + + if (Main.hasMobileLayout) { + window.open('//www.youtube.com/watch?v=' + vid); + return; + } + + el = document.createElement('div'); + el.className = 'media-embed'; + el.innerHTML = ''; + + node.parentNode.insertBefore(el, node.nextElementSibling); + + node.textContent = 'Remove'; + } + else { + node.textContent = 'Error'; + } + } +}; + +Media.toggleEmbed = function(node) { + var fn, type = node.getAttribute('data-type'); + + if (type && (fn = Media.map[type])) { + fn.call(this, node); + } +}; + +/** + * Sticky nav auto-hide + */ +var StickyNav = { + thres: 5, + pos: 0, + timeout: null, + el: null, + + init: function() { + this.el = Config.classicNav ? $.id('boardNavDesktop') : $.id('boardNavMobile'); + $.addClass(this.el, 'autohide-nav'); + window.addEventListener('scroll', this.onScroll, false); + }, + + onScroll: function() { + clearTimeout(StickyNav.timeout); + StickyNav.timeout = setTimeout(StickyNav.checkScroll, 50); + }, + + checkScroll: function() { + var thisPos; + + thisPos = window.pageYOffset; + + if (Math.abs(StickyNav.pos - thisPos) <= StickyNav.thres) { + return; + } + + if (thisPos < StickyNav.pos) { + StickyNav.el.style.top = ''; + } + else { + StickyNav.el.style.top = '-' + StickyNav.el.offsetHeight + 'px'; + } + + StickyNav.pos = thisPos; + } +}; + +/** + * Custom CSS + */ +var CustomCSS = {}; + +CustomCSS.init = function() { + var style, css; + if (css = localStorage.getItem('4chan-css')) { + style = document.createElement('style'); + style.id = 'customCSS'; + style.setAttribute('type', 'text/css'); + style.textContent = css; + document.head.appendChild(style); + } +}; + +CustomCSS.open = function() { + var cnt, ta, data; + + if ($.id('customCSSMenu')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'customCSSMenu'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'css-close'); + cnt.innerHTML = '\ +
    Custom CSS\ +Close
    \ +\ +
    \ +
    '; + + document.body.appendChild(cnt); + + cnt.addEventListener('click', this.onClick, false); + + ta = $.id('customCSSBox'); + + if (data = localStorage.getItem('4chan-css')) { + ta.textContent = data; + } + + ta.focus(); +}; + +CustomCSS.save = function() { + var ta, style; + + if (ta = $.id('customCSSBox')) { + localStorage.setItem('4chan-css', ta.value); + if (Config.customCSS && (style = $.id('customCSS'))) { + document.head.removeChild(style); + CustomCSS.init(); + } + } +}; + +CustomCSS.close = function() { + var cnt; + + if (cnt = $.id('customCSSMenu')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +CustomCSS.onClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'css-close': + CustomCSS.close(); + break; + case 'css-save': + CustomCSS.save(); + CustomCSS.close(); + break; + } + } +}; + +/** + * Keyboard shortcuts + */ +var Keybinds = { + enabled: false +}; + +Keybinds.init = function() { + this.enabled = true; + + this.map = { + // A + 65: function() { + if (ThreadUpdater.enabled) ThreadUpdater.toggleAuto(); + }, + // F + 70: function() { + if (Config.filter) { + Filter.addSelection(); + } + }, + // Q + 81: function() { + if (QR.enabled && Main.tid) { + QR.quotePost(Main.tid); + } + }, + // R + 82: function() { + if (ThreadUpdater.enabled) ThreadUpdater.forceUpdate(); + }, + // W + 87: function() { + if (Config.threadWatcher && Main.tid) ThreadWatcher.toggle(Main.tid); + }, + // B + 66: function() { + var el; + (el = $.cls('prev')[0]) && (el = $.tag('form', el)[0]) && el.submit(); + }, + // C + 67: function() { + location.href = '/' + Main.board + '/catalog'; + }, + // N + 78: function() { + var el; + (el = $.cls('next')[0]) && (el = $.tag('form', el)[0]) && el.submit(); + }, + // I + 73: function() { + location.href = '/' + Main.board + '/'; + } + }; + + document.addEventListener('keydown', this.resolve, false); +}; + +Keybinds.resolve = function(e) { + var bind, el = e.target; + + if (!Keybinds.enabled || el.nodeName == 'TEXTAREA' || el.nodeName == 'INPUT') { + return; + } + + bind = Keybinds.map[e.keyCode]; + + if (bind && !e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + bind(); + } +}; + +Keybinds.open = function() { + var cnt; + + if ($.id('keybindsHelp')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'keybindsHelp'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'keybinds-close'); + cnt.innerHTML = '\ +
    Keyboard Shortcuts\ +Close
    \ +
      \ +
    • Global
    • \ +
    • A — Toggle auto-updater
    • \ +
    • Q — Open Quick Reply
    • \ +
    • R — Update thread
    • \ +
    • W — Watch/Unwatch thread
    • \ +
    • B — Previous page
    • \ +
    • N — Next page
    • \ +
    • I — Return to index
    • \ +
    • C — Open catalog
    • \ +
    • F — Filter selected text
    • \ +
      \ +
    • Quick Reply (always enabled)
    • \ +
    • Ctrl + Click the post number — Quote without linking
    • \ +
    • Ctrl + S — Spoiler tags
    • \ +
    • Esc — Close the Quick Reply
    • \ +
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); +}; + +Keybinds.close = function() { + var cnt; + + if (cnt = $.id('keybindsHelp')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Keybinds.onClick = function(e) { + var cmd; + + if ((cmd = e.target.getAttribute('data-cmd')) && cmd == 'keybinds-close') { + Keybinds.close(); + } +}; + +var Del = { + deletePost: function(pid, file_only) { + var params; + + if (!confirm('Delete ' + (file_only ? 'file' : 'post') + '?')) { + return; + } + + params = { + mode: window.thread_archived ? 'arcdel' : 'usrdel' + }; + + params[pid] = 'delete'; + + if (file_only) { + params['onlyimgdel'] = 'on'; + } + + $.xhr('POST', 'https://sys.' + $L.d(Main.board) + '/' + Main.board + '/imgboard.php', + { + onload: Del.onPostDeleted, + onerror: Del.onError, + withCredentials: true, + pid: pid, + file_only: file_only + }, + params + ); + }, + + onPostDeleted: function() { + var el; + + if (!this.file_only) { + el = $.id('pc' + this.pid); + } + else if (el = $.id('f' + this.pid)) { + el = $.tag('img', el)[0]; + if (!el.hasAttribute('data-md5')) { + return; + } + } + + if (!el) { + return; + } + + if (/Updating index|Can't find the post/.test(this.responseText)) { + if (!$.hasClass(el, 'deleted')) { + $.addClass(el, 'deleted'); + } + } + else { + Del.onError(); + } + }, + + onError: function() { + Feedback.error('Something went wrong.'); + } +}; + +/** + * Reporting + */ +var Report = { + init: function() { + window.addEventListener('message', Report.onMessage, false); + } +}; + +Report.onMessage = function(e) { + var id; + + if (e.origin === ('https://sys.' + $L.d(Main.board)) && /^done-report/.test(e.data)) { + id = e.data.split('-')[2]; + + if (Config.threadHiding && $.id('t' + id)) { + if (!ThreadHiding.isHidden(id)) { + ThreadHiding.hide(id); + ThreadHiding.save(); + } + + return; + } + + if ($.id('p' + id)) { + if (!ReplyHiding.isHidden(id)) { + ReplyHiding.hide(id); + ReplyHiding.save(); + } + + return; + } + } +}; + +Report.open = function(pid, board) { + var height, altc; + + if (QR.noCaptcha) { + height = 205; + altc = ''; + } + else { + height = 510; + altc = ''; + } + + window.open('https://sys.' + $L.d(Main.board) + '/' + + (board || Main.board) + '/imgboard.php?mode=report&no=' + pid + altc, Date.now(), + "toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height=" + height); +}; + +/** + * Custom Menu + */ +var CustomMenu = {}; + +CustomMenu.initCtrl = function() { + var el, cnt; + + el = document.createElement('span'); + el.className = 'custom-menu-ctrl'; + el.innerHTML = '[Edit]'; + + if (Config.dropDownNav && !Config.classicNav && !Main.hasMobileLayout) { + cnt = $.id('boardSelectMobile').parentNode; + cnt.insertBefore(el, cnt.lastChild); + } + else { + cnt = $.cls('boardList'); + cnt[0] && cnt[0].appendChild(el); + cnt[1] && cnt[1].appendChild(el.cloneNode(true)); + } +}; +/* +CustomMenu.showNWSBoards = function() { + var i, el, nodes, len; + + nodes = $.cls('nwsb'); + len = nodes.length; + + for (i = len - 1; el = nodes[i]; i--) { + $.removeClass(el, 'nwsb'); + } +}; +*/ +CustomMenu.reset = function() { + var i, el, full, custom, navs; + + full = $.cls('boardList'); + custom = $.cls('customBoardList'); + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.removeEventListener('click', CustomMenu.reset, false); + } + + for (i = custom.length - 1; el = custom[i]; i--) { + full[i].style.display = null; + el.parentNode.removeChild(el); + } +}; + +CustomMenu.apply = function(str) { + var i, el, cntBottom, board, navs, boardList, cnt; + + if (!str) { + if (Config.dropDownNav && !Config.classicNav && !Main.hasMobileLayout) { + if (el = $.cls('customBoardList')[0]) { + el.parentNode.removeChild(el); + } + } + return; + } + + boardList = str.split(/[^0-9a-z]/i); + + cnt = document.createElement('span'); + cnt.className = 'customBoardList'; + + for (i = 0; board = boardList[i]; ++i) { + if (i) { + cnt.appendChild(document.createTextNode(' / ')); + } + else { + cnt.appendChild(document.createTextNode('[')); + } + el = document.createElement('a'); + el.textContent = board; + el.href = '//boards.' + $L.d(board) + '/' + board + '/'; + cnt.appendChild(el); + } + + cnt.appendChild(document.createTextNode(']')); + + if (Config.dropDownNav && !Config.classicNav && !Main.hasMobileLayout) { + if (el = $.cls('customBoardList')[0]) { + el.parentNode.removeChild(el); + } + navs = $.id('boardSelectMobile'); + navs && navs.parentNode.insertBefore(cnt, navs.nextSibling); + } + else { + cnt.appendChild(document.createTextNode(' [')); + el = document.createElement('a'); + el.textContent = '…'; + el.title = 'Show all'; + el.className = 'show-all-boards pointer'; + cnt.appendChild(el); + cnt.appendChild(document.createTextNode('] ')); + + cntBottom = cnt.cloneNode(true); + + navs = $.cls('boardList'); + + for (i = 0; el = navs[i]; ++i) { + el.style.display = 'none'; + el.parentNode.insertBefore(i ? cntBottom : cnt, el); + } + + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.addEventListener('click', CustomMenu.reset, false); + } + } +}; + +CustomMenu.onClick = function(e) { + var t; + + if ((t = e.target) == document) { + return; + } + + if (t.hasAttribute('data-close')) { + CustomMenu.closeEditor(); + } + else if (t.hasAttribute('data-save')) { + CustomMenu.save($.id('customMenu').hasAttribute('data-standalone')); + } +}; + +CustomMenu.showEditor = function(standalone) { + var cnt; + + cnt = document.createElement('div'); + cnt.id = 'customMenu'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-close', '1'); + + if (standalone === true) { + cnt.setAttribute('data-standalone', '1'); + } + + cnt.innerHTML = '\ +
    Custom Board List\ +Close
    \ +\ +
    '; + + document.body.appendChild(cnt); + + if (Config.customMenuList) { + $.id('customMenuBox').value = Config.customMenuList; + } + + cnt.addEventListener('click', CustomMenu.onClick, false); +}; + +CustomMenu.closeEditor = function() { + var el; + + if (el = $.id('customMenu')) { + el.removeEventListener('click', CustomMenu.onClick, false); + document.body.removeChild(el); + } +}; + +CustomMenu.save = function(standalone) { + var input; + + if (input = $.id('customMenuBox')) { + Config.customMenuList = input.value; + + if (standalone === true) { + CustomMenu.apply(Config.customMenuList); + Config.customMenu = true; + Config.save(); + } + } + + CustomMenu.closeEditor(); +}; + +/** + * Draggable helper + */ +var Draggable = { + el: null, + key: null, + scrollX: null, + scrollY: null, + dx: null, dy: null, right: null, bottom: null, offsetTop: null, + + set: function(handle) { + handle.addEventListener('mousedown', Draggable.startDrag, false); + }, + + unset: function(handle) { + handle.removeEventListener('mousedown', Draggable.startDrag, false); + }, + + startDrag: function(e) { + var self, doc, offs; + + if (this.parentNode.hasAttribute('data-shiftkey') && !e.shiftKey) { + return; + } + + e.preventDefault(); + + self = Draggable; + doc = document.documentElement; + + self.el = this.parentNode; + + self.key = self.el.getAttribute('data-trackpos'); + offs = self.el.getBoundingClientRect(); + self.dx = e.clientX - offs.left; + self.dy = e.clientY - offs.top; + self.right = doc.clientWidth - offs.width; + self.bottom = doc.clientHeight - offs.height; + + if (getComputedStyle(self.el, null).position != 'fixed') { + self.scrollX = window.pageXOffset; + self.scrollY = window.pageYOffset; + } + else { + self.scrollX = self.scrollY = 0; + } + + self.offsetTop = Main.getDocTopOffset(); + + document.addEventListener('mouseup', self.endDrag, false); + document.addEventListener('mousemove', self.onDrag, false); + }, + + endDrag: function() { + document.removeEventListener('mouseup', Draggable.endDrag, false); + document.removeEventListener('mousemove', Draggable.onDrag, false); + if (Draggable.key) { + Config[Draggable.key] = Draggable.el.style.cssText; + Config.save(); + } + delete Draggable.el; + }, + + onDrag: function(e) { + var left, top, style; + + left = e.clientX - Draggable.dx + Draggable.scrollX; + top = e.clientY - Draggable.dy + Draggable.scrollY; + style = Draggable.el.style; + if (left < 1) { + style.left = '0'; + style.right = ''; + } + else if (Draggable.right < left) { + style.left = ''; + style.right = '0'; + } + else { + style.left = (left / document.documentElement.clientWidth * 100) + '%'; + style.right = ''; + } + if (top <= Draggable.offsetTop) { + style.top = Draggable.offsetTop + 'px'; + style.bottom = ''; + } + else if (Draggable.bottom < top && + Draggable.el.clientHeight < document.documentElement.clientHeight) { + style.bottom = '0'; + style.top = ''; + } + else { + style.top = (top / document.documentElement.clientHeight * 100) + '%'; + style.bottom = ''; + } + } +}; + +/** + * User Agent + */ +var UA = {}; + +UA.init = function() { + document.head = document.head || $.tag('head')[0]; + + this.isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; + + this.hasCORS = 'withCredentials' in new XMLHttpRequest(); + + this.hasFormData = 'FormData' in window; +}; + +UA.dispatchEvent = function(name, detail) { + var e = document.createEvent('Event'); + e.initEvent(name, false, false); + if (detail) { + e.detail = detail; + } + document.dispatchEvent(e); +}; + +UA.getSelection = function(raw) { + var sel; + + if (UA.isOpera && typeof (sel = document.getSelection()) == 'string') {} + else { + sel = window.getSelection(); + + if (!raw) { + sel = sel.toString(); + } + } + + return sel; +}; + +/** + * Config + */ +var Config = { + quotePreview: true, + backlinks: true, + quickReply: true, + threadUpdater: true, + threadHiding: true, + + alwaysAutoUpdate: false, + topPageNav: false, + threadWatcher: false, + threadAutoWatcher: false, + imageExpansion: true, + fitToScreenExpansion: false, + threadExpansion: true, + alwaysDepage: false, + localTime: true, + stickyNav: false, + keyBinds: false, + inlineQuotes: false, + //showNWSBoards: false, + + filter: false, + revealSpoilers: false, + imageHover: false, + threadStats: true, + IDColor: true, + noPictures: false, + embedYouTube: true, + embedSoundCloud: false, + updaterSound: false, + + customCSS: false, + autoScroll: false, + hideStubs: false, + compactThreads: false, + centeredThreads: false, + dropDownNav: false, + autoHideNav: false, + classicNav: false, + fixedThreadWatcher: false, + persistentQR: false, + forceHTTPS: false, + darkTheme: false, + linkify: false, + unmuteWebm: false, + + disableAll: false +}; + +var ConfigMobile = { + embedYouTube: false, + compactThreads: false, + linkify: true, +}; + +Config.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-settings')) { + storage = JSON.parse(storage); + $.extend(Config, storage); + + if (Main.getCookie('https') === '1') { + Config.forceHTTPS = true; + } + else { + Config.forceHTTPS = false; + } + } + else { + Main.firstRun = true; + } +}; + +Config.loadFromURL = function() { + var cmd, data; + + cmd = location.href.split('=', 2); + + if (/#cfg$/.test(cmd[0])) { + try { + data = JSON.parse(decodeURIComponent(cmd[1])); + + history.replaceState(null, '', location.href.split('#', 1)[0]); + + $.extend(Config, JSON.parse(data.settings)); + + Config.save(); + + if (data.filters) { + localStorage.setItem('4chan-filters', data.filters); + } + + if (data.css) { + localStorage.setItem('4chan-css', data.css); + } + + if (data.catalogFilters) { + localStorage.setItem('catalog-filters', data.catalogFilters); + } + + if (data.catalogSettings) { + localStorage.setItem('catalog-settings', data.catalogSettings); + } + + return true; + } + catch (e) { + console.log(e); + } + } + + return false; +}; + +Config.toURL = function() { + var data, cfg = {}; + + cfg.settings = localStorage.getItem('4chan-settings'); + + if (data = localStorage.getItem('4chan-filters')) { + cfg.filters = data; + } + + if (data = localStorage.getItem('4chan-css')) { + cfg.css = data; + } + + if (data = localStorage.getItem('catalog-filters')) { + cfg.catalogFilters = data; + } + + if (data = localStorage.getItem('catalog-settings')) { + cfg.catalogSettings = data; + } + + return encodeURIComponent(JSON.stringify(cfg)); +}; + +Config.save = function(old) { + localStorage.setItem('4chan-settings', JSON.stringify(Config)); + + //StorageSync.sync('4chan-settings'); + + if (!old) { + return; + } + + if (Config.forceHTTPS) { + Main.setCookie('https', 1); + } + else { + Main.removeCookie('https'); + } + + if (old.darkTheme != Config.darkTheme) { + if (Config.darkTheme) { + Main.setCookie('nws_style', 'Tomorrow', '.' + $L.d(Main.board)); + Main.setCookie('ws_style', 'Tomorrow', '.' + $L.d(Main.board)); + } + else { + Main.removeCookie('nws_style', '.' + $L.d(Main.board)); + Main.removeCookie('ws_style', '.' + $L.d(Main.board)); + } + } +}; + +/** + * Settings menu + */ +var SettingsMenu = {}; + +// [ Name, Subtitle, available on mobile?, is sub-option?, is mobile only? ] +SettingsMenu.options = { + 'Quotes & Replying': { + quotePreview: [ 'Quote preview', 'Show post when mousing over post links', true ], + backlinks: [ 'Backlinks', 'Show who has replied to a post', true ], + inlineQuotes: [ 'Inline quote links', 'Clicking quote links will inline expand the quoted post, Shift-click to bypass inlining' ], + quickReply: [ 'Quick Reply', 'Quickly respond to a post by clicking its post number', true ], + persistentQR: [ 'Persistent Quick Reply', 'Keep Quick Reply window open after posting' ], + }, + 'Monitoring': { + threadUpdater: [ 'Thread updater', 'Append new posts to bottom of thread without refreshing the page', true ], + alwaysAutoUpdate:[ 'Auto-update by default', 'Always auto-update threads', true ], + threadWatcher: [ 'Thread Watcher', 'Keep track of threads you\'re watching and see when they receive new posts', true ], + threadAutoWatcher: [ 'Automatically watch threads you create', '', true, true ], + autoScroll: [ 'Auto-scroll with auto-updated posts', 'Automatically scroll the page as new posts are added' ], + updaterSound: [ 'Sound notification', 'Play a sound when somebody replies to your post(s)' ], + fixedThreadWatcher: [ 'Pin Thread Watcher to the page', 'Thread Watcher will scroll with you' ], + threadStats: [ 'Thread statistics', 'Display post and image counts on the right of the page, italics signify bump/image limit has been met', true ] + }, + 'Filters & Post Hiding': { + filter: [ 'Filter and highlight specific threads/posts [Edit]', 'Enable pattern-based filters' ], + threadHiding: [ 'Thread hiding [Clear History]', 'Hide entire threads by clicking the minus button', true ], + hideStubs: [ 'Hide thread stubs', "Don't display stubs of hidden threads" ] + }, + 'Navigation': { + threadExpansion: [ 'Thread expansion', 'Expand threads inline on board indexes', true ], + dropDownNav: [ 'Use persistent drop-down navigation bar', '' ], + classicNav: [ 'Use traditional board list', '', false, true ], + autoHideNav: [ 'Auto-hide on scroll', '', false, true ], + customMenu: [ 'Custom board list [Edit]', 'Only show selected boards in top and bottom board lists' ], + //showNWSBoards: [ 'Show all boards', 'Show all boards in top and bottom board lists on 4channel.org', true], + alwaysDepage: [ 'Always use infinite scroll', 'Enable infinite scroll by default, so reaching the bottom of the board index will load subsequent pages', true ], + topPageNav: [ 'Page navigation at top of page', 'Show the page switcher at the top of the page, hold Shift and drag to move' ], + stickyNav: [ 'Navigation arrows', 'Show top and bottom navigation arrows, hold Shift and drag to move' ], + keyBinds: [ 'Use keyboard shortcuts [Show]', 'Enable handy keyboard shortcuts for common actions' ] + }, + 'Images & Media': { + imageExpansion: [ 'Image expansion', 'Enable inline image expansion, limited to browser width', true ], + fitToScreenExpansion: [ 'Fit expanded images to screen', 'Limit expanded images to both browser width and height' ], + imageHover: [ 'Image hover', 'Mouse over images to view full size, limited to browser size' ], + imageHoverBg: [ 'Set a background color for transparent images', '', false, true ], + revealSpoilers: [ "Don't spoiler images", 'Show image thumbnail and original filename instead of spoiler placeholders', true ], + unmuteWebm: [ 'Un-mute WebM audio', 'Un-mute sound automatically for WebM playback', true ], + noPictures: [ 'Hide thumbnails', 'Don\'t display thumbnails while browsing', true ], + embedYouTube: [ 'Embed YouTube links', 'Embed YouTube player into replies' ], + embedSoundCloud: [ 'Embed SoundCloud links', 'Embed SoundCloud player into replies' ], + }, + 'Miscellaneous': { + linkify: [ 'Linkify URLs', 'Make user-posted links clickable', true ], + darkTheme: [ 'Use a dark theme', 'Use the Tomorrow theme for nighttime browsing', true, false, true ], + customCSS: [ 'Custom CSS [Edit]', 'Include your own CSS rules', true ], + IDColor: [ 'Color user IDs', 'Assign unique colors to user IDs on boards that use them', true ], + compactThreads: [ 'Force long posts to wrap', 'Long posts will wrap at 75% browser width' ], + centeredThreads: [ 'Center threads', 'Align threads to the center of page', false ], + localTime: [ 'Convert dates to local time', 'Convert 4chan server time (US Eastern Time) to your local time', true ], + forceHTTPS: [ 'Always use HTTPS', 'Rewrite 4chan URLs to always use HTTPS', true ] + } +}; + +SettingsMenu.save = function() { + var i, options, el, key, old; + + old = {}; + $.extend(old, Config); + + options = $.id('settingsMenu').getElementsByClassName('menuOption'); + + for (i = 0; el = options[i]; ++i) { + key = el.getAttribute('data-option'); + Config[key] = el.type == 'checkbox' ? el.checked : el.value; + } + + Config.save(old); + + UA.dispatchEvent('4chanSettingsSaved'); + + SettingsMenu.close(); + location.href = location.href.replace(/#.+$/, ''); +}; + +SettingsMenu.toggle = function() { + if ($.id('settingsMenu')) { + SettingsMenu.close(); + } + else { + SettingsMenu.open(); + } +}; + +SettingsMenu.open = function() { + var i, cat, categories, key, html, cnt, opts, mobileOpts, el; + + if (Main.firstRun) { + if (el = $.id('settingsTip')) { + el.parentNode.removeChild(el); + } + if (el = $.id('settingsTipBottom')) { + el.parentNode.removeChild(el); + } + Config.save(); + } + + cnt = document.createElement('div'); + cnt.id = 'settingsMenu'; + cnt.className = 'UIPanel'; + + html = '
    Settings' + + 'Close' + + '
      '; + + html += ''; + + if (Main.hasMobileLayout) { + categories = {}; + for (cat in SettingsMenu.options) { + mobileOpts = {}; + opts = SettingsMenu.options[cat]; + for (key in opts) { + if (opts[key][2]) { + mobileOpts[key] = opts[key]; + } + } + for (i in mobileOpts) { + categories[cat] = mobileOpts; + break; + } + } + } + else { + categories = SettingsMenu.options; + } + + for (cat in categories) { + opts = categories[cat]; + html += '
      • ' + + '' + + '' + + cat + '
        • '; + for (key in opts) { + // Mobile layout only? + if (opts[key][4] && !Main.hasMobileLayout) { + continue; + } + html += '' : '>') + + '' + + (opts[key][1] !== false ? '
        • ' : '">') + opts[key][1] : '') + + '
        • '; + } + html += '
      '; + } + + html += '
    • ' + + '
    ' + + '
    ' + + '
    '; + + cnt.innerHTML = html; + cnt.addEventListener('click', SettingsMenu.onClick, false); + document.body.appendChild(cnt); + + if (Main.firstRun) { + SettingsMenu.expandAll(); + } + + (el = $.cls('menuOption', cnt)[0]) && el.focus(); +}; + +SettingsMenu.showExport = function() { + var cnt, str, el; + + if ($.id('exportSettings')) { + return; + } + + str = location.href.replace(location.hash, '').replace(/^http:/, 'https:') + '#cfg=' + Config.toURL(); + + cnt = document.createElement('div'); + cnt.id = 'exportSettings'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'export-close'); + cnt.innerHTML = '\ +
    Export Settings\ +Close
    \ +

    Copy and save the URL below, and visit it from another \ +browser or computer to restore your extension and catalog settings.

    \ +

    \ +

    \ +

    Alternatively, you can drag the link below into your \ +bookmarks bar and click it to restore.

    \ +

    [Restore 4chan Settings]

    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onExportClick, false); + el = $.cls('export-field', cnt)[0]; + el.focus(); + el.select(); +}; + +SettingsMenu.closeExport = function() { + var cnt; + + if (cnt = $.id('exportSettings')) { + cnt.removeEventListener('click', this.onExportClick, false); + document.body.removeChild(cnt); + } +}; + +SettingsMenu.onExportClick = function(e) { + if (e.target.id == 'exportSettings') { + e.preventDefault(); + e.stopPropagation(); + SettingsMenu.closeExport(); + } +}; + +SettingsMenu.expandAll = function() { + var i, el, nodes = $.cls('settings-expand'); + + for (i = 0; el = nodes[i]; ++i) { + el.src = Main.icons.minus; + el.parentNode.nextElementSibling.style.display = 'block'; + } +}; + +SettingsMenu.toggleCat = function(t) { + var icon, disp, el = t.parentNode.nextElementSibling; + + if (!el.style.display) { + disp = 'block'; + icon = 'minus'; + } + else { + disp = ''; + icon = 'plus'; + } + + el.style.display = disp; + t.parentNode.firstElementChild.src = Main.icons[icon]; +}; + +SettingsMenu.onClick = function(e) { + var el, t; + + t = e.target; + + if ($.hasClass(t, 'settings-expand')) { + SettingsMenu.toggleCat(t); + } + else if (t.getAttribute('data-cmd') == 'settings-exp-all') { + e.preventDefault(); + SettingsMenu.expandAll(); + } + else if (t.id == 'settingsMenu' && (el = $.id('settingsMenu'))) { + e.preventDefault(); + SettingsMenu.close(el); + } +}; + +SettingsMenu.close = function(el) { + if (el = (el || $.id('settingsMenu'))) { + el.removeEventListener('click', SettingsMenu.onClick, false); + document.body.removeChild(el); + } +}; + +/** + * Feedback + */ +var Feedback = { + messageTimeout: null, + + showMessage: function(msg, type, timeout, onClick) { + var el; + + Feedback.hideMessage(); + + el = document.createElement('div'); + el.id = 'feedback'; + el.title = 'Dismiss'; + el.innerHTML = ''; + + $.on(el, 'click', onClick || Feedback.hideMessage); + + document.body.appendChild(el); + + if (timeout) { + Feedback.messageTimeout = setTimeout(Feedback.hideMessage, timeout); + } + }, + + hideMessage: function() { + var el = $.id('feedback'); + + if (el) { + if (Feedback.messageTimeout) { + clearTimeout(Feedback.messageTimeout); + Feedback.messageTimeout = null; + } + + $.off(el, 'click', Feedback.hideMessage); + + document.body.removeChild(el); + } + }, + + error: function(msg, timeout) { + if (timeout === undefined) { + timeout = 5000; + } + + Feedback.showMessage(msg || 'Something went wrong', 'error', timeout); + }, + + notify: function(msg, timeout) { + if (timeout === undefined) { + timeout = 3000; + } + + Feedback.showMessage(msg, 'notify', timeout); + } +}; + +/** + * Main + */ +var Main = {}; + +Main.addTooltip = function(link, message, id) { + var el, pos; + + el = document.createElement('div'); + el.className = 'click-me'; + if (id) { + el.id = id; + } + el.innerHTML = message || 'Change your settings'; + link.parentNode.appendChild(el); + + pos = (link.offsetWidth - el.offsetWidth + link.offsetLeft - el.offsetLeft) / 2; + el.style.marginLeft = pos + 'px'; + + return el; +}; + +Main.getDocTopOffset = function() { + if (Config.dropDownNav && !Config.autoHideNav) { + return $.id( + Config.classicNav ? 'boardNavDesktop' : 'boardNavMobile' + ).offsetHeight; + } + else { + return 0; + } +}; + +Main.init = function() { + var params; + + document.addEventListener('DOMContentLoaded', Main.run, false); + + Main.now = Date.now(); + + Main.is_4channel = location.host === 'boards.4channel.org'; + + UA.init(); + + Config.load(); + + if (Config.forceHTTPS && location.protocol != 'https:') { + location.href = location.href.replace(/^http:/, 'https:'); + return; + } + + if (Main.firstRun && Config.loadFromURL()) { + Main.firstRun = false; + } + + if (Main.stylesheet = Main.getCookie(style_group)) { + Main.stylesheet = Main.stylesheet.toLowerCase().replace(/ /g, '_'); + } + else { + Main.stylesheet = + style_group == 'nws_style' ? 'yotsuba_new' : 'yotsuba_b_new'; + } + + QR.noCaptcha = QR.noCaptcha || window.passEnabled; + + Main.initIcons(); + + Main.addCSS(); + + Main.type = style_group.split('_')[0]; + + params = location.pathname.split(/\//); + Main.board = params[1]; + Main.page = params[2]; + Main.tid = params[3]; + + UA.dispatchEvent('4chanMainInit'); +}; + +Main.initPersistentNav = function() { + var el, top, bottom; + + top = $.id('boardNavDesktop'); + bottom = $.id('boardNavDesktopFoot'); + + if (Config.classicNav) { + el = document.createElement('div'); + el.className = 'pageJump'; + el.innerHTML = '' + + 'Settings' + + 'Home
    '; + + top.appendChild(el); + + $.id('settingsWindowLinkClassic') + .addEventListener('click', SettingsMenu.toggle, false); + + $.addClass(top, 'persistentNav'); + } + else { + top.style.display = 'none'; + $.removeClass($.id('boardNavMobile'), 'mobile'); + } + + bottom.style.display = 'none'; + + $.addClass(document.body, 'hasDropDownNav'); +}; + +Main.checkMobileLayout = function() { + var mobile, desktop; + + if (window.matchMedia) { + return window.matchMedia('(max-width: 480px)').matches + && localStorage.getItem('4chan_never_show_mobile') != 'true'; + } + + mobile = $.id('boardNavMobile'); + desktop = $.id('boardNavDesktop'); + + return mobile && desktop && mobile.offsetWidth > 0 && desktop.offsetWidth === 0; +}; + +Main.disableDarkTheme = function() { + Config.darkTheme = false; + localStorage.setItem('4chan-settings', JSON.stringify(Config)); +}; + +Main.run = function() { + var el, thread; + + document.removeEventListener('DOMContentLoaded', Main.run, false); + + document.addEventListener('click', Main.onclick, false); + + $.id('settingsWindowLink').addEventListener('click', SettingsMenu.toggle, false); + $.id('settingsWindowLinkBot').addEventListener('click', SettingsMenu.toggle, false); + $.id('settingsWindowLinkMobile').addEventListener('click', SettingsMenu.toggle, false); + + Main.isOekakiBoard = Main.board === 'i'; + + Main.hasMobileLayout = Main.checkMobileLayout(); + Main.isMobileDevice = /Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent); + + Search.init(); + + if (Config.disableAll) { + return; + } + + Report.init(); + + if (Main.hasMobileLayout) { + $.extend(Config, ConfigMobile); + } + else { + if (el = $.id('bottomReportBtn')) { + el.style.display = 'none'; + } + + if (Main.isMobileDevice) { + $.addClass(document.body, 'isMobileDevice'); + } + } + /* + if (Main.is_4channel && Config.showNWSBoards) { + CustomMenu.showNWSBoards(); + } + */ + if (Config.linkify) { + Linkify.init(); + } + + if (Config.IDColor) { + IDColor.init(); + } + + if (Config.customCSS) { + CustomCSS.init(); + } + + if (Config.keyBinds) { + Keybinds.init(); + } + + if (Main.firstRun && Main.isMobileDevice) { + Config.topPageNav = false; + Config.dropDownNav = true; + } + + if (Config.dropDownNav && !Main.hasMobileLayout) { + Main.initPersistentNav(); + } + + $.addClass(document.body, Main.stylesheet); + $.addClass(document.body, Main.type); + + if (Config.darkTheme) { + $.addClass(document.body, 'm-dark'); + if (!Main.hasMobileLayout) { + $.cls('stylechanger')[0].addEventListener('change', Main.disableDarkTheme, false); + } + } + + if (Config.compactThreads) { + $.addClass(document.body, 'compact'); + } + else if (Config.centeredThreads) { + $.addClass(document.body, 'centeredThreads'); + } + + if (Config.noPictures) { + $.addClass(document.body, 'noPictures'); + } + + if (Config.customMenu) { + CustomMenu.apply(Config.customMenuList); + } + + CustomMenu.initCtrl(); + + if (Config.quotePreview || Config.imageHover|| Config.filter) { + thread = $.id('delform') || $.id('arc-list'); + thread.addEventListener('mouseover', Main.onThreadMouseOver, false); + thread.addEventListener('mouseout', Main.onThreadMouseOut, false); + } + + if (Config.stickyNav) { + Main.setStickyNav(); + } + + if (!Main.hasMobileLayout) { + Main.initGlobalMessage(); + if (Config.autoHideNav) { + StickyNav.init(); + } + } + else { + StickyNav.init(); + } + + if (Config.threadExpansion) { + ThreadExpansion.init(); + } + + if (Config.filter) { + Filter.init(); + } + + if (Config.threadWatcher) { + ThreadWatcher.init(); + } + + if (Main.hasMobileLayout || Config.embedSoundCloud || Config.embedYouTube) { + Media.init(); + } + + ReplyHiding.init(); + + if (Config.quotePreview) { + QuotePreview.init(); + } + + Parser.init(); + + if (Main.tid) { + Main.threadClosed = !document.forms.post || !!$.cls('closedIcon')[0]; + Main.threadSticky = !!$.cls('stickyIcon', $.id('pi' + Main.tid))[0]; + + if (Config.threadStats) { + ThreadStats.init(); + } + + Parser.parseThread(Main.tid); + + if (Config.threadUpdater) { + ThreadUpdater.init(); + } + } + else { + if (!Main.page) { + Depager.init(); + } + + if (Config.topPageNav) { + Main.setPageNav(); + } + if (Config.threadHiding) { + ThreadHiding.init(); + Parser.parseBoard(); + } + else { + Parser.parseBoard(); + } + } + + if (Main.board === 'f') { + SWFEmbed.init(); + } + + if (Config.quickReply) { + QR.init(); + } + + ReplyHiding.purge(); + + if (Config.alwaysDepage && !Main.hasMobileLayout) { + if ($.docEl.scrollHeight <= $.docEl.clientHeight) { + Depager.depage(); + } + } +}; + +Main.isThreadClosed = function(tid) { + var el; + return window.thread_archived || ((el = $.id('pi' + tid)) && $.cls('closedIcon', el)[0]); +}; + +Main.setThreadState = function(state, mode) { + var cnt, el, ref, cap; + + cap = state.charAt(0).toUpperCase() + state.slice(1); + + if (mode) { + cnt = $.cls('postNum', $.id('pi' + Main.tid))[0]; + el = document.createElement('img'); + el.className = state + 'Icon retina'; + el.title = cap; + el.src = Main.icons2[state]; + if (state == 'sticky' && (ref = $.cls('closedIcon', cnt)[0])) { + cnt.insertBefore(el, ref); + cnt.insertBefore(document.createTextNode(' '), ref); + } + else { + cnt.appendChild(document.createTextNode(' ')); + cnt.appendChild(el); + } + } + else { + if (el = $.cls(state + 'Icon', $.id('pi' + Main.tid))[0]) { + el.parentNode.removeChild(el.previousSibling); + el.parentNode.removeChild(el); + } + } + + Main['thread' + cap] = mode; +}; + +Main.icons = { + up: 'arrow_up.png', + down: 'arrow_down.png', + right: 'arrow_right.png', + download: 'arrow_down2.png', + refresh: 'refresh.png', + cross: 'cross.png', + gis: 'gis.png', + iqdb: 'iqdb.png', + minus: 'post_expand_minus.png', + plus: 'post_expand_plus.png', + rotate: 'post_expand_rotate.gif', + quote: 'quote.png', + report: 'report.png', + notwatched: 'watch_thread_off.png', + watched: 'watch_thread_on.png', + help: 'question.png' +}; + +Main.icons2 = { + archived: 'archived.gif', + closed: 'closed.gif', + sticky: 'sticky.gif', + trash: 'trash.gif' +}, + +Main.initIcons = function() { + var key, paths, url; + + paths = { + yotsuba_new: 'futaba/', + futaba_new: 'futaba/', + yotsuba_b_new: 'burichan/', + burichan_new: 'burichan/', + tomorrow: 'tomorrow/', + photon: 'photon/' + }; + + url = '//s.4cdn.org/image/'; + + if (window.devicePixelRatio >= 2) { + for (key in Main.icons) { + Main.icons[key] = Main.icons[key].replace('.', '@2x.'); + } + for (key in Main.icons2) { + Main.icons2[key] = Main.icons2[key].replace('.', '@2x.'); + } + } + + for (key in Main.icons2) { + Main.icons2[key] = url + Main.icons2[key]; + } + + url += 'buttons/' + paths[Main.stylesheet]; + for (key in Main.icons) { + Main.icons[key] = url + Main.icons[key]; + } +}; + +Main.setPageNav = function() { + var el, cnt; + + cnt = document.createElement('div'); + cnt.setAttribute('data-shiftkey', '1'); + cnt.setAttribute('data-trackpos', 'TN-position'); + cnt.className = 'topPageNav'; + + if (Config['TN-position']) { + cnt.style.cssText = Config['TN-position']; + } + else { + cnt.style.left = '10px'; + cnt.style.top = '50px'; + } + + el = $.cls('pagelist')[0]; + + if (!el) { + return; + } + + el = el.cloneNode(true); + cnt.appendChild(el); + Draggable.set(el); + document.body.appendChild(cnt); +}; + +Main.getWebmVolume = function() { + let vol = parseFloat(localStorage.getItem('4chan-volume')); + + if (!isNaN(vol)) { + return vol; + } + else { + return 0.5; + } +}; + +Main.getWebmVolumeChangeCb = function() { + let t; + + return (e) => { + clearTimeout(t); + t = setTimeout(() => { localStorage.setItem('4chan-volume', e.target.volume); }, 200); + }; +}; + +Main.initGlobalMessage = function() { + var msg, btn, thisTs, oldTs; + + if ((msg = $.id('globalMessage')) && msg.textContent) { + msg.nextElementSibling.style.clear = 'both'; + + btn = document.createElement('img'); + btn.id = 'toggleMsgBtn'; + btn.className = 'extButton'; + btn.setAttribute('data-cmd', 'toggleMsg'); + btn.alt = 'Toggle'; + btn.title = 'Toggle announcement'; + + oldTs = localStorage.getItem('4chan-global-msg'); + thisTs = msg.getAttribute('data-utc'); + + if (oldTs && thisTs <= oldTs) { + msg.style.display = 'none'; + btn.style.opacity = '0.5'; + btn.src = Main.icons.plus; + } + else { + btn.src = Main.icons.minus; + } + + msg.parentNode.insertBefore(btn, msg); + } +}; + +Main.toggleGlobalMessage = function() { + var msg, btn; + + msg = $.id('globalMessage'); + btn = $.id('toggleMsgBtn'); + if (msg.style.display == 'none') { + msg.style.display = ''; + btn.src = Main.icons.minus; + btn.style.opacity = '1'; + localStorage.removeItem('4chan-global-msg'); + } + else { + msg.style.display = 'none'; + btn.src = Main.icons.plus; + btn.style.opacity = '0.5'; + localStorage.setItem('4chan-global-msg', msg.getAttribute('data-utc')); + } + + //StorageSync.sync('4chan-global-msg'); +}; + +Main.setStickyNav = function() { + var cnt, hdr; + + cnt = document.createElement('div'); + cnt.id = 'stickyNav'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-shiftkey', '1'); + cnt.setAttribute('data-trackpos', 'SN-position'); + + if (Config['SN-position']) { + cnt.style.cssText = Config['SN-position']; + } + else { + cnt.style.right = '10px'; + cnt.style.top = '50px'; + } + + hdr = document.createElement('div'); + hdr.innerHTML = '▲' + + '▼'; + Draggable.set(hdr); + + cnt.appendChild(hdr); + document.body.appendChild(cnt); +}; + +Main.getCookie = function(name) { + var i, c, ca, key; + + key = name + "="; + ca = document.cookie.split(';'); + + for (i = 0; c = ca[i]; ++i) { + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(key) === 0) { + return decodeURIComponent(c.substring(key.length, c.length)); + } + } + return null; +}; + +Main.setCookie = function(name, value, domain) { + var date = new Date(); + + date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000)); + + if (!domain) { + domain = location.host; + } + + document.cookie = name + '=' + value + + '; expires=' + date.toGMTString() + + '; path=/; domain=' + domain; +}; + +Main.removeCookie = function(name, domain, path) { + if (!domain) { + domain = location.host; + } + + if (!path) { + path = '/'; + } + + document.cookie = name + '=' + + '; expires=Thu, 01 Jan 1970 00:00:01 GMT;' + + '; path=' + path + '; domain=' + domain; +}; + +Main.onclick = function(e) { + var t, cmd, tid, id; + + if ((t = e.target) == document) { + return; + } + + if (cmd = t.getAttribute('data-cmd')) { + id = t.getAttribute('data-id'); + switch (cmd) { + case 'update': + e.preventDefault(); + ThreadUpdater.forceUpdate(); + break; + case 'post-menu': + e.preventDefault(); + PostMenu.open(t); + break; + case 'auto': + ThreadUpdater.toggleAuto(); + break; + case 'totop': + case 'tobottom': + if (!e.shiftKey) { + location.href = '#' + cmd.slice(2); + } + break; + case 'hide': + ThreadHiding.toggle(id); + break; + case 'watch': + ThreadWatcher.toggle(id); + break; + case 'hide-r': + if (t.hasAttribute('data-recurse')) { + ReplyHiding.toggleR(id); + } + else { + ReplyHiding.toggle(id); + } + break; + case 'expand': + ThreadExpansion.toggle(id); + break; + case 'open-qr': + e.preventDefault(); + QR.show(Main.tid); + $.tag('textarea', document.forms.qrPost)[0].focus(); + break; + case 'qr-painter-draw': + QR.openPainter(); + break; + case 'qr-painter-clear': + QR.onPainterCancel(); + break; + case 'qr-painter-edit': + e.preventDefault(); + QR.onOpenInPainterClick(t); + break; + case 'unfilter': + Filter.unfilter(t); + break; + case 'depage': + e.preventDefault(); + Depager.toggle(); + break; + case 'report': + Report.open(id, t.getAttribute('data-board')); + break; + case 'filter-sel': + e.preventDefault(); + Filter.addSelection(); + break; + case 'embed': + Media.toggleEmbed(t); + break; + case 'sound': + ThreadUpdater.toggleSound(); + break; + case 'toggleMsg': + Main.toggleGlobalMessage(); + break; + case 'settings-toggle': + SettingsMenu.toggle(); + break; + case 'settings-save': + SettingsMenu.save(); + break; + case 'keybinds-open': + Keybinds.open(); + break; + case 'filters-open': + Filter.open(); + break; + case 'thread-hiding-clear': + ThreadHiding.clear(); + break; + case 'css-open': + CustomCSS.open(); + break; + case 'settings-export': + SettingsMenu.showExport(); + break; + case 'export-close': + SettingsMenu.closeExport(); + break; + case 'custom-menu-edit': + e.preventDefault(); + CustomMenu.showEditor($.hasClass(t.parentNode, 'custom-menu-ctrl')); + break; + case 'del-post': + case 'del-file': + e.preventDefault(); + Del.deletePost(id, cmd === 'del-file'); + break; + case 'open-tex-preview': + QR.openTeXPreview(); + break; + case 'close-tex-preview': + QR.closeTeXPreview(); + break; + } + } + else if (!Config.disableAll) { + if (QR.enabled && t.title == 'Reply to this post') { + e.preventDefault(); + tid = Main.tid || t.previousElementSibling.getAttribute('href').split('#')[0].split('/').slice(-1)[0]; + QR.quotePost(tid, !e.ctrlKey && t.textContent); + } + else if (Config.imageExpansion && e.which == 1 && t.parentNode + && $.hasClass(t.parentNode, 'fileThumb') + && t.parentNode.nodeName == 'A' + && !$.hasClass(t.parentNode, 'deleted') + && !$.hasClass(t, 'mFileInfo')) { + + if (ImageExpansion.toggle(t)) { + e.preventDefault(); + } + + return; + } + else if (Config.inlineQuotes && e.which == 1 && $.hasClass(t, 'quotelink') && Main.page !== 'archive') { + if (!e.shiftKey) { + QuoteInline.toggle(t, e); + } + else { + e.preventDefault(); + window.location = t.href; + } + } + else if (Config.threadExpansion && t.parentNode && $.hasClass(t.parentNode, 'abbr')) { + e.preventDefault(); + ThreadExpansion.expandComment(t); + } + else if (Main.isMobileDevice && Config.quotePreview + && $.hasClass(t, 'quotelink') + && t.getAttribute('href').match(QuotePreview.regex)) { + e.preventDefault(); + } + else if ($.hasClass(t, 'mFileInfo')) { + e.preventDefault(); + e.stopPropagation(); + } + } + + if (Main.hasMobileLayout && (Config.disableAll || !Config.imageExpansion)) { + if (t.parentNode && t.parentNode.hasAttribute('data-m')) { + ImageExpansion.setMobileSrc(t.parentNode); + } + } +}; + +Main.onThreadMouseOver = function(e) { + var t = e.target; + + if (Config.quotePreview + && $.hasClass(t, 'quotelink') + && !$.hasClass(t, 'deadlink') + && !$.hasClass(t, 'linkfade')) { + QuotePreview.resolve(e.target); + } + else if (Config.imageHover && ( + (t.hasAttribute('data-md5') && !$.hasClass(t.parentNode, 'deleted')) + || + (t.href && !$.hasClass(t.parentNode, 'fileText') && /(i\.4cdn|is\.4chan)\.org\/[a-z0-9]+\/[0-9]+\.(gif|jpg|png|webm)$/.test(t.href)) + ) + ) { + ImageHover.show(t); + } + else if ($.hasClass(t, 'dateTime')) { + Parser.onDateMouseOver(t); + } + else if ($.hasClass(t, 'hand')) { + Parser.onUIDMouseOver(t); + } + else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) { + Media.showYTPreview(t); + } + else if (Config.filter && t.hasAttribute('data-filtered')) { + QuotePreview.show(t, + t.href ? t.parentNode.parentNode.parentNode : t.parentNode.parentNode); + } +}; + +Main.onThreadMouseOut = function(e) { + var t = e.target; + + if (Config.quotePreview && $.hasClass(t, 'quotelink')) { + QuotePreview.remove(t); + } + else if (Config.imageHover && + (t.hasAttribute('data-md5') + || (t.href && !$.hasClass(t.parentNode, 'fileText') && /(i\.4cdn|is\.4chan)\.org\/[a-z0-9]+\/[0-9]+\.(gif|jpg|png|webm)$/.test(t.href)) + ) + ) { + ImageHover.hide(); + } + else if ($.hasClass(t, 'dateTime') || $.hasClass(t, 'hand')) { + Parser.onTipMouseOut(t); + } + else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) { + Media.removeYTPreview(); + } + else if (Config.filter && t.hasAttribute('data-filtered')) { + QuotePreview.remove(t); + } +}; + +Main.linkToThread = function(tid, board, post) { + return '//' + location.host + '/' + + (board || Main.board) + '/thread/' + + tid + (post > 0 ? ('#p' + post) : ''); +}; + +Main.addCSS = function() { + var style, css = '\ +body.hasDropDownNav {\ + margin-top: 45px;\ +}\ +.extButton.threadHideButton {\ + float: left;\ + margin-right: 5px;\ + margin-top: -1px;\ +}\ +.extButton.replyHideButton {\ + margin-top: 1px;\ +}\ +div.op > span .postHideButtonCollapsed {\ + margin-right: 1px;\ +}\ +.dropDownNav #boardNavMobile, {\ + display: block !important;\ +}\ +.extPanel {\ + border: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.tomorrow .extPanel {\ + border: 1px solid #111;\ +}\ +.extButton,\ +img.pointer {\ + width: 18px;\ + height: 18px;\ +}\ +.extControls {\ + display: inline;\ + margin-left: 5px;\ +}\ +.extButton {\ + cursor: pointer;\ + margin-bottom: -4px;\ +}\ +.trashIcon {\ + width: 16px;\ + height: 16px;\ + margin-bottom: -2px;\ + margin-left: 5px;\ +}\ +.threadUpdateStatus {\ + margin-left: 0.5ex;\ +}\ +.futaba_new .stub,\ +.burichan_new .stub {\ + line-height: 1;\ + padding-bottom: 1px;\ +}\ +.stub .extControls,\ +.stub .wbtn,\ +.stub input {\ + display: none;\ +}\ +.stub .threadHideButton {\ + float: none;\ + margin-right: 2px;\ +}\ +.right {\ + float: right;\ +}\ +.center {\ + display: block;\ + margin: auto;\ +}\ +.pointer {\ + cursor: pointer;\ +}\ +.drag {\ + cursor: move !important;\ + user-select: none !important;\ + -moz-user-select: none !important;\ + -webkit-user-select: none !important;\ +}\ +#quickReply {\ + display: block;\ + position: fixed;\ + padding: 2px;\ + font-size: 10pt;\ +}\ +#qrepHeader,\ +#qrHeader {\ + font-size: 10pt;\ + text-align: center;\ + margin-bottom: 1px;\ + padding: 0;\ + height: 18px;\ + line-height: 18px;\ +}\ +#qrHeader .left { float: left; margin-left: 3px; }\ +#qrepClose,\ +#qrClose {\ + float: right;\ +}\ +#qrCaptchaContainer { width: 300px; background-color: #eee; overflow: hidden; margin-bottom: 3px }\ +.tomorrow #qrCaptchaContainer.t-qr-root { background-color: #323232; }\ +.tomorrow #qrCaptchaContainer #t-cnt { filter: invert(85%); }\ +#qrForm > div {\ + clear: both;\ +}\ +#quickReply input[type="text"],\ +#quickReply textarea,\ +#quickReply #recaptcha_response_field {\ + border: 1px solid #aaa;\ + font-family: arial,helvetica,sans-serif;\ + font-size: 10pt;\ + outline: medium none;\ + width: 296px;\ + padding: 2px;\ + margin: 0 0 1px 0;\ +}\ +.tomorrow #quickReply input[type="text"],\ +.tomorrow #quickReply textarea,\ +.tomorrow #quickReply #recaptcha_response_field {\ + border: 1px solid #515151;\ + background-color: #282a2e;\ + color: #c5c8c6;\ +}\ +.tomorrow #quickReply input[type="text"]:focus,\ +.tomorrow #quickReply textarea:focus {\ + border: 1px solid #757575;\ +}\ +#quickReply textarea {\ + min-width: 296px;\ + float: left;\ +}\ +.tomorrow #quickReply input::placeholder {\ + color: 919191 !important;\ +}\ +#quickReply input[type="submit"] {\ + width: 75px;\ + margin: 0;\ + float: right;\ +}\ +#quickReply #qrCapField {\ + display: block;\ + margin-top: 1px;\ +}\ +#quickReply input.presubmit {\ + margin-right: 1px;\ + width: 212px;\ + float: left;\ +}\ +#qrFile {\ + width: 130px;\ + margin-right: 5px;\ +}\ +.yotsuba_new #qrFile {\ + color:black;\ +}\ +#qrSpoiler {\ + display: inline;\ +}\ +#qrError {\ + width: 292px;\ + display: none;\ + font-family: monospace;\ + background-color: #E62020;\ + font-size: 12px;\ + color: white;\ + padding: 3px 5px;\ + text-shadow: 0 1px rgba(0, 0, 0, 0.20);\ + clear: both;\ +}\ +#qrError a:hover,\ +#qrError a {\ + color: white !important;\ + text-decoration: underline;\ +}\ +#twHeader {\ + font-weight: bold;\ + text-align: center;\ + height: 17px;\ +}\ +.futaba_new #twHeader,\ +.burichan_new #twHeader {\ + line-height: 1;\ +}\ +#twPrune {\ + margin-left: 3px;\ + margin-top: -1px;\ +}\ +#twClose {\ + float: left;\ + margin-top: -1px;\ +}\ +#threadWatcher {\ + max-width: 265px;\ + display: block;\ + position: absolute;\ + padding: 3px;\ +}\ +#watchList {\ + margin: 0;\ + padding: 0;\ + user-select: none;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ +}\ +#watchList li:first-child {\ + margin-top: 3px;\ + padding-top: 2px;\ + border-top: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.photon #watchList li:first-child {\ + border-top: 1px solid #ccc;\ +}\ +.yotsuba_new #watchList li:first-child {\ + border-top: 1px solid #d9bfb7;\ +}\ +.yotsuba_b_new #watchList li:first-child {\ + border-top: 1px solid #b7c5d9;\ +}\ +.tomorrow #watchList li:first-child {\ + border-top: 1px solid #111;\ +}\ +#watchList a {\ + text-decoration: none;\ +}\ +#watchList li {\ + overflow: hidden;\ + white-space: nowrap;\ + text-overflow: ellipsis;\ +}\ +div.post div.image-expanded {\ + display: table;\ +}\ +div.op div.file .image-expanded-anti {\ + margin-left: -3px;\ +}\ +#quote-preview {\ + display: block;\ + position: absolute;\ + top: 0;\ + padding: 3px 6px 6px 3px;\ + margin: 0;\ +}\ +#quote-preview .dateTime {\ + white-space: nowrap;\ +}\ +#quote-preview.reveal-spoilers s {\ + background-color: #aaa !important;\ + color: inherit !important;\ + text-decoration: none !important;\ +}\ +#quote-preview.reveal-spoilers s a {\ + background: transparent !important;\ + text-decoration: underline;\ +}\ +.yotsuba_b_new #quote-preview.reveal-spoilers s a,\ +.burichan_new #quote-preview.reveal-spoilers s a {\ + color: #D00 !important;\ +}\ +.yotsuba_new #quote-preview.reveal-spoilers s a,\ +.futaba_new #quote-preview.reveal-spoilers s a {\ + color: #000080 !important;\ +}\ +.tomorrow #quote-preview.reveal-spoilers s { color: #000 !important; }\ +.tomorrow #quote-preview.reveal-spoilers s a { color: #5F89AC !important; }\ +.photon #quote-preview.reveal-spoilers s a {\ + color: #FF6600 !important;\ +}\ +.yotsuba_new #quote-preview.highlight,\ +.yotsuba_b_new #quote-preview.highlight {\ + border-width: 1px 2px 2px 1px !important;\ + border-style: solid !important;\ +}\ +.yotsuba_new #quote-preview.highlight {\ + border-color: #D99F91 !important;\ +}\ +.yotsuba_b_new #quote-preview.highlight {\ + border-color: #BA9DBF !important;\ +}\ +.yotsuba_b_new .highlight-anti,\ +.burichan_new .highlight-anti {\ + border-width: 1px !important;\ + background-color: #bfa6ba !important;\ +}\ +.yotsuba_new .highlight-anti,\ +.futaba_new .highlight-anti {\ + background-color: #e8a690 !important;\ +}\ +.tomorrow .highlight-anti {\ + background-color: #111 !important;\ + border-color: #111;\ +}\ +.photon .highlight-anti {\ + background-color: #bbb !important;\ +}\ +.op.inlined {\ + display: block;\ +}\ +#quote-preview .inlined,\ +#quote-preview .postMenuBtn,\ +#quote-preview .extButton,\ +#quote-preview .extControls {\ + display: none;\ +}\ +.hasNewReplies { font-weight: bold; }\ +.hasYouReplies { font-style: italic; }\ +.archivelink {\ + opacity: 0.5;\ +}\ +.deadlink {\ + text-decoration: line-through !important;\ +}\ +div.backlink {\ + font-size: 0.8em !important;\ + display: inline;\ + padding: 0;\ + padding-left: 5px;\ +}\ +.backlink.mobile {\ + padding: 3px 5px;\ + display: block;\ + clear: both !important;\ + line-height: 2;\ +}\ +.op .backlink.mobile,\ +#quote-preview .backlink.mobile {\ + display: none !important;\ +}\ +.backlink.mobile .quoteLink {\ + padding-right: 2px;\ +}\ +.backlink span {\ + padding: 0;\ +}\ +.burichan_new .backlink a,\ +.yotsuba_b_new .backlink a {\ + color: #34345C !important;\ +}\ +.burichan_new .backlink a:hover,\ +.yotsuba_b_new .backlink a:hover {\ + color: #dd0000 !important;\ +}\ +.expbtn {\ + margin-right: 3px;\ + margin-left: 2px;\ +}\ +.tCollapsed .rExpanded {\ + display: none;\ +}\ +#stickyNav {\ + position: fixed;\ + font-size: 0;\ +}\ +#stickyNav img {\ + vertical-align: middle;\ +}\ +.tu-error {\ + color: red;\ +}\ +.topPageNav {\ + position: absolute;\ +}\ +.yotsuba_b_new .topPageNav {\ + border-top: 1px solid rgba(255, 255, 255, 0.25);\ + border-left: 1px solid rgba(255, 255, 255, 0.25);\ +}\ +.newPostsMarker:not(#quote-preview) {\ + box-shadow: 0 3px red;\ +}\ +#toggleMsgBtn {\ + float: left;\ + margin-bottom: 6px;\ +}\ +.panelHeader {\ + font-weight: bold;\ + font-size: 16px;\ + text-align: center;\ + margin-bottom: 5px;\ + margin-top: 5px;\ + padding-bottom: 5px;\ + border-bottom: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.yotsuba_new .panelHeader {\ + border-bottom: 1px solid #d9bfb7;\ +}\ +.yotsuba_b_new .panelHeader {\ + border-bottom: 1px solid #b7c5d9;\ +}\ +.tomorrow .panelHeader {\ + border-bottom: 1px solid #111;\ +}\ +.panelHeader .panelCtrl {\ + position: absolute;\ + right: 5px;\ + top: 5px;\ +}\ +.UIMenu,\ +.UIPanel {\ + position: fixed;\ + width: 100%;\ + height: 100%;\ + top: 0;\ + left: 0;\ + z-index: 100002;\ +}\ +.UIPanel {\ + line-height: 14px;\ + font-size: 14px;\ + background-color: rgba(0, 0, 0, 0.25);\ +}\ +.UIPanel:after {\ + display: inline-block;\ + height: 100%;\ + vertical-align: middle;\ + content: "";\ +}\ +.UIPanel > div {\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + display: inline-block;\ + height: auto;\ + max-height: 100%;\ + position: relative;\ + width: 400px;\ + left: 50%;\ + margin-left: -200px;\ + overflow: auto;\ + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\ + vertical-align: middle;\ +}\ +#settingsMenu > div {\ + top: 25px;;\ + vertical-align: top;\ + max-height: 85%;\ +}\ +.extPanel input[type="text"],\ +.extPanel textarea {\ + border: 1px solid #AAA;\ + outline: none;\ +}\ +.UIPanel .center {\ + margin-bottom: 5px;\ +}\ +.UIPanel button {\ + display: inline-block;\ + margin-right: 5px;\ +}\ +.UIPanel code {\ + background-color: #eee;\ + color: #000000;\ + padding: 1px 4px;\ + font-size: 12px;\ +}\ +.UIPanel ul {\ + list-style: none;\ + padding: 0;\ + margin: 0 0 10px;\ +}\ +.UIPanel .export-field {\ + width: 385px;\ +}\ +#settingsMenu label input {\ + margin-right: 5px;\ +}\ +.tomorrow #settingsMenu ul {\ + border-bottom: 1px solid #282a2e;\ +}\ +.settings-off {\ + padding-left: 3px;\ +}\ +.settings-cat-lbl {\ + font-weight: bold;\ + margin: 10px 0 5px;\ + padding-left: 5px;\ +}\ +.settings-cat-lbl img {\ + vertical-align: text-bottom;\ + margin-right: 5px;\ + cursor: pointer;\ + width: 18px;\ + height: 18px;\ +}\ +.settings-tip {\ + font-size: 0.85em;\ + margin: 2px 0 5px 0;\ + padding-left: 23px;\ +}\ +#settings-exp-all {\ + padding-left: 7px;\ + text-align: center;\ +}\ +#settingsMenu .settings-cat {\ + display: none;\ + margin-left: 3px;\ +}\ +#tex-preview-cnt .extPanel { width: 600px; margin-left: -300px; }\ +#tex-preview-cnt textarea,\ +#customCSSMenu textarea {\ + display: block;\ + max-width: 100%;\ + min-width: 100%;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + height: 200px;\ + margin: 0 0 5px;\ + font-family: monospace;\ +}\ +#tex-preview-cnt textarea { height: 75px; }\ +#output-tex-preview {\ + min-height: 75px;\ + white-space: pre;\ + padding: 0 3px;\ + -moz-box-sizing: border-box; box-sizing: border-box;\ +}\ +#tex-protip { font-size: 11px; margin: 5px 0; text-align: center; }\ +a.tex-logo sub { pointer-events: none; }\ +#customCSSMenu .right,\ +#settingsMenu .right {\ + margin-top: 2px;\ +}\ +#settingsMenu label {\ + display: inline-block;\ + user-select: none;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ +}\ +#filtersHelp > div {\ + width: 600px;\ + left: 50%;\ + margin-left: -300px;\ +}\ +#filtersHelp h4 {\ + font-size: 15px;\ + margin: 20px 0 0 10px;\ +}\ +#filtersHelp h4:before {\ + content: "»";\ + margin-right: 3px;\ +}\ +#filtersHelp ul {\ + padding: 0;\ + margin: 10px;\ +}\ +#filtersHelp li {\ + padding: 3px 0;\ + list-style: none;\ +}\ +#filtersMenu table {\ + width: 100%;\ +}\ +#filtersMenu th {\ + font-size: 12px;\ +}\ +#filtersMenu tbody {\ + text-align: center;\ +}\ +#filtersMenu select,\ +#filtersMenu .fPattern,\ +#filtersMenu .fBoards,\ +#palette-custom-input {\ + padding: 1px;\ + font-size: 11px;\ +}\ +#filtersMenu select {\ + width: 75px;\ +}\ +#filtersMenu tfoot td {\ + padding-top: 10px;\ +}\ +#keybindsHelp li {\ + padding: 3px 5px;\ +}\ +.fPattern {\ + width: 110px;\ +}\ +.fBoards {\ + width: 25px;\ +}\ +.fColor {\ + width: 60px;\ +}\ +.fDel {\ + font-size: 16px;\ +}\ +.filter-preview {\ + cursor: pointer;\ + margin-left: 3px;\ +}\ +#quote-preview iframe,\ +#quote-preview .filter-preview {\ + display: none;\ +}\ +.post-hidden .extButton,\ +.post-hidden:not(#quote-preview) .postInfo {\ + opacity: 0.5;\ +}\ +.post-hidden:not(.thread) .postInfo {\ + padding-left: 5px;\ +}\ +.post-hidden:not(#quote-preview) input,\ +.post-hidden:not(#quote-preview) .replyContainer,\ +.post-hidden:not(#quote-preview) .summary,\ +.post-hidden:not(#quote-preview) .op .file,\ +.post-hidden:not(#quote-preview) .file,\ +.post-hidden .wbtn,\ +.post-hidden .postNum span,\ +.post-hidden:not(#quote-preview) .backlink,\ +div.post-hidden:not(#quote-preview) div.file,\ +div.post-hidden:not(#quote-preview) blockquote.postMessage {\ + display: none;\ +}\ +.click-me {\ + border-radius: 5px;\ + margin-top: 5px;\ + padding: 2px 5px;\ + position: absolute;\ + font-weight: bold;\ + z-index: 2;\ + white-space: nowrap;\ +}\ +.yotsuba_new .click-me,\ +.futaba_new .click-me {\ + color: #800000;\ + background-color: #F0E0D6;\ + border: 2px solid #D9BFB7;\ +}\ +.yotsuba_b_new .click-me,\ +.burichan_new .click-me {\ + color: #000;\ + background-color: #D6DAF0;\ + border: 2px solid #B7C5D9;\ +}\ +.tomorrow .click-me {\ + color: #C5C8C6;\ + background-color: #282A2E;\ + border: 2px solid #111;\ +}\ +.photon .click-me {\ + color: #333;\ + background-color: #ddd;\ + border: 2px solid #ccc;\ +}\ +.click-me:before {\ + content: "";\ + border-width: 0 6px 6px;\ + border-style: solid;\ + left: 50%;\ + margin-left: -6px;\ + position: absolute;\ + width: 0;\ + height: 0;\ + top: -6px;\ +}\ +.yotsuba_new .click-me:before,\ +.futaba_new .click-me:before {\ + border-color: #D9BFB7 transparent;\ +}\ +.yotsuba_b_new .click-me:before,\ +.burichan_new .click-me:before {\ + border-color: #B7C5D9 transparent;\ +}\ +.tomorrow .click-me:before {\ + border-color: #111 transparent;\ +}\ +.photon .click-me:before {\ + border-color: #ccc transparent;\ +}\ +.click-me:after {\ + content: "";\ + border-width: 0 4px 4px;\ + top: -4px;\ + display: block;\ + left: 50%;\ + margin-left: -4px;\ + position: absolute;\ + width: 0;\ + height: 0;\ +}\ +.yotsuba_new .click-me:after,\ +.futaba_new .click-me:after {\ + border-color: #F0E0D6 transparent;\ + border-style: solid;\ +}\ +.yotsuba_b_new .click-me:after,\ +.burichan_new .click-me:after {\ + border-color: #D6DAF0 transparent;\ + border-style: solid;\ +}\ +.tomorrow .click-me:after {\ + border-color: #282A2E transparent;\ + border-style: solid;\ +}\ +.photon .click-me:after {\ + border-color: #DDD transparent;\ + border-style: solid;\ +}\ +#image-hover {\ + position: fixed;\ + max-width: 100%;\ + max-height: 100%;\ + top: 0px;\ + right: 0px;\ + z-index: 9002;\ +}\ +.thread-stats {\ + float: right;\ + margin-right: 5px;\ + cursor: default;\ +}\ +.compact .thread {\ + max-width: 75%;\ +}\ +.dotted {\ + text-decoration: none;\ + border-bottom: 1px dashed;\ +}\ +.linkfade {\ + opacity: 0.5;\ +}\ +#quote-preview .linkfade {\ + opacity: 1.0;\ +}\ +kbd {\ + background-color: #f7f7f7;\ + color: black;\ + border: 1px solid #ccc;\ + border-radius: 3px 3px 3px 3px;\ + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset;\ + font-family: monospace;\ + font-size: 11px;\ + line-height: 1.4;\ + padding: 0 5px;\ +}\ +.deleted {\ + opacity: 0.66;\ +}\ +div.collapseWebm { text-align: center; margin-top: 10px; }\ +.noPictures a.fileThumb img:not(.expanded-thumb) {\ + opacity: 0;\ +}\ +.noPictures.futaba_new a.fileThumb,\ +.noPictures.yotsuba_new a.fileThumb {\ + border: 1px solid #800;\ +}\ +.noPictures.burichan_new a.fileThumb,\ +.noPictures.yotsuba_b_new a.fileThumb {\ + border: 1px solid #34345C;\ +}\ +.noPictures.tomorrow a.fileThumb:not(.expanded-thumb) {\ + border: 1px solid #C5C8C6;\ +}\ +.noPictures.photon a.fileThumb:not(.expanded-thumb) {\ + border: 1px solid #004A99;\ +}\ +.spinner {\ + margin-top: 2px;\ + padding: 3px;\ + display: table;\ +}\ +#settings-presets {\ + position: relative;\ + top: -1px;\ +}\ +#colorpicker { \ + position: fixed;\ + text-align: center;\ +}\ +.colorbox {\ + font-size: 10px;\ + width: 16px;\ + height: 16px;\ + line-height: 17px;\ + display: inline-block;\ + text-align: center;\ + background-color: #fff;\ + border: 1px solid #aaa;\ + text-decoration: none;\ + color: #000;\ + cursor: pointer;\ + vertical-align: top;\ +}\ +#palette-custom-input {\ + vertical-align: top;\ + width: 45px;\ + margin-right: 2px;\ +}\ +#qrDummyFile {\ + float: left;\ + margin-right: 5px;\ + width: 220px;\ + cursor: default;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ + -ms-user-select: none;\ + user-select: none;\ + white-space: nowrap;\ + text-overflow: ellipsis;\ + overflow: hidden;\ +}\ +#qrDummyFileLabel {\ + margin-left: 3px;\ +}\ +.depageNumber {\ + position: absolute;\ + right: 5px;\ +}\ +.depagerEnabled .depagelink {\ + font-weight: bold;\ +}\ +.depagerEnabled strong {\ + font-weight: normal;\ +}\ +.depagelink {\ + display: inline-block;\ + padding: 4px 0;\ + cursor: pointer;\ + text-decoration: none;\ +}\ +.burichan_new .depagelink,\ +.futaba_new .depagelink {\ + text-decoration: underline;\ +}\ +#customMenuBox {\ + margin: 0 auto 5px auto;\ + width: 385px;\ + display: block;\ +}\ +.preview-summary {\ + display: block;\ +}\ +#swf-embed-header {\ + padding: 0 0 0 3px;\ + font-weight: normal;\ + height: 20px;\ + line-height: 20px;\ +}\ +.yotsuba_new #swf-embed-header,\ +.yotsuba_b_new #swf-embed-header {\ + height: 18px;\ + line-height: 18px;\ +}\ +#swf-embed-close {\ + position: absolute;\ + right: 0;\ + top: 1px;\ +}\ +#qr-painter-ctrl { text-align: center; }\ +#qr-painter-ctrl label { margin-right: 4px; }\ +.open-qr-wrap {\ + text-align: center;\ + width: 200px;\ + position: absolute;\ + margin-left: 50%;\ + left: -100px;\ +}\ +.postMenuBtn {\ + margin-left: 5px;\ + text-decoration: none;\ + line-height: 1em;\ + display: inline-block;\ + -webkit-transition: -webkit-transform 0.1s;\ + -moz-transition: -moz-transform 0.1s;\ + transition: transform 0.1s;\ + width: 1em;\ + height: 1em;\ + text-align: center;\ + outline: none;\ + opacity: 0.8;\ +}\ +.postMenuBtn:hover{\ + opacity: 1;\ +}\ +.yotsuba_new .postMenuBtn,\ +.futaba_new .postMenuBtn {\ + color: #000080;\ +}\ +.tomorrow .postMenuBtn {\ + color: #5F89AC;\ +}\ +.tomorrow .postMenuBtn:hover {\ + color: #81a2be;\ +}\ +.photon .postMenuBtn {\ + color: #FF6600;\ +}\ +.photon .postMenuBtn:hover {\ + color: #FF3300;\ +}\ +.menuOpen {\ + -webkit-transform: rotate(90deg);\ + -moz-transform: rotate(90deg);\ + -ms-transform: rotate(90deg);\ + transform: rotate(90deg);\ +}\ +.settings-sub label:before {\ + border-bottom: 1px solid;\ + border-left: 1px solid;\ + content: " ";\ + display: inline-block;\ + height: 8px;\ + margin-bottom: 5px;\ + width: 8px;\ +}\ +.settings-sub {\ + margin-left: 25px;\ +}\ +.settings-tip.settings-sub {\ + padding-left: 32px;\ +}\ +.centeredThreads .opContainer {\ + display: block;\ +}\ +.centeredThreads .postContainer {\ + margin: auto;\ + width: 75%;\ +}\ +.centeredThreads .sideArrows {\ + display: none;\ +}\ +.centre-exp {\ + width: auto !important;\ + clear: both;\ +}\ +.centeredThreads .summary {\ + margin-left: 12.5%;\ + display: block;\ +}\ +.centre-exp div.op{\ + display: table;\ +}\ +#yt-preview { position: absolute; }\ +#yt-preview img { display: block; }\ +.autohide-nav { transition: top 0.2s ease-in-out }\ +#feedback {\ + position: fixed;\ + top: 10px;\ + text-align: center;\ + width: 100%;\ + z-index: 9999;\ +}\ +.feedback-notify,\ +.feedback-error {\ + border-radius: 5px;\ + cursor: pointer;\ + color: #fff;\ + padding: 3px 6px;\ + font-size: 16px;\ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);\ + text-shadow: 0 1px rgba(0, 0, 0, 0.2);\ +}\ +.feedback-error { background-color: #C41E3A; }\ +.feedback-notify { background-color: #00A550; }\ +.boardSelect .custom-menu-ctrl, .boardSelect .customBoardList { margin-left: 10px; }\ +.boardSelect .custom-menu-ctrl { display: none; }\ +.boardSelect:hover .custom-menu-ctrl { display: inline; }\ +.persistentNav .boardList a, .persistentNav .customBoardList a, #boardNavMobile .boardSelect a { text-decoration: none; }\ +@media only screen and (max-width: 480px) {\ +.thread-stats { float: none; text-align: center; }\ +.ts-replies:before { content: "Replies: "; }\ +.ts-images:before { content: "Images: "; }\ +.ts-ips:before { content: "Posters: "; }\ +.ts-page:before { content: "Page: "; }\ +#threadWatcher {\ + max-width: none;\ + padding: 3px 0;\ + left: 0;\ + width: 100%;\ + border-left: none;\ + border-right: none;\ +}\ +#watchList {\ + padding: 0 10px;\ +}\ +div.post div.postInfoM span.nameBlock { clear: none }\ +.btn-row {\ + margin-top: 5px;\ +}\ +.image-expanded .mFileName {\ + display: block;\ + margin-bottom: 2px;\ +}\ +.mFileName { display: none }\ +.mobile-report {\ + float: right;\ + font-size: 11px;\ + margin-bottom: 3px;\ + margin-left: 10px;\ +}\ +.mobile-report:after {\ + content: "]";\ +}\ +.mobile-report:before {\ + content: "[";\ +}\ +.nws .mobile-report:after {\ + color: #800000;\ +}\ +.nws .mobile-report:before {\ + color: #800000;\ +}\ +.ws .mobile-report {\ + color: #34345C;\ +}\ +.nws .mobile-report {\ + color:#0000EE;\ +}\ +.reply .mobile-report {\ + margin-right:5px;\ +}\ +.postLink .mobileHideButton {\ + margin-right: 3px;\ +}\ +.board .mobile-hr-hidden {\ + margin-top: 10px !important;\ +}\ +.board > .mobileHideButton {\ + margin-top: -20px !important;\ +}\ +.board > .mobileHideButton:first-child {\ + margin-top: 10px !important;\ +}\ +.extButton.threadHideButton {\ + float: none;\ + margin: 0;\ + margin-bottom: 5px;\ +}\ +.mobile-post-hidden {\ + display: none;\ +}\ +#toggleMsgBtn {\ + display: none;\ +}\ +.mobile-tu-status {\ + height: 20px;\ + line-height: 20px;\ +}\ +.mobile-tu-show {\ + width: 150px;\ + margin: auto;\ + display: block;\ + text-align: center;\ +}\ +.button input {\ + margin: 0 3px 0 0;\ + position: relative;\ + top: -2px;\ + border-radius: 0;\ + height: 10px;\ + width: 10px;\ +}\ +.UIPanel > div {\ + width: 320px;\ + margin-left: -160px;\ +}\ +.UIPanel .export-field {\ + width: 300px;\ +}\ +.yotsuba_new #quote-preview.highlight,\ +#quote-preview {\ + border-width: 1px !important;\ +}\ +.yotsuba_new #quote-preview.highlight {\ + border-color: #D9BFB7 !important;\ +}\ +#quickReply input[type="text"],\ +#quickReply textarea,\ +.extPanel input[type="text"],\ +.extPanel textarea {\ + font-size: 16px;\ +}\ +#quickReply {\ + position: absolute;\ + left: 50%;\ + margin-left: -154px;\ +}\ +.m-dark .button {\ + background-color: rgb(27,28,30);\ + background-image: url("//s.4cdn.org/image/buttonfade-dark.png");\ + background-repeat: repeat-x;\ + border: 1px solid #282A2E;\ +}\ +.depaged-ad { margin-top: -25px; margin-bottom: -25px; }\ +.depageNumber { font-size: 10px; margin-top: -21px; }\ +.m-dark a, .m-dark div#absbot a { color: #81A2BE !important; }\ +.m-dark a:hover { color: #5F89AC !important; }\ +.m-dark .button a, .m-dark .button:hover, .m-dark .button { color: #707070 !important; }\ +.m-dark #boardNavMobile { background-color: #1D1F21; border-bottom: 2px solid #282A2E; }\ +body.m-dark { background: #1D1F21 none; color: #C5C8C6; }\ +.m-dark #globalToggle {\ + background-color: #FFADAD;\ + background-image: url("//s.4cdn.org/image/buttonfade-red.png");\ + border: 1px solid #C45858;\ + color: #880000 !important;\ +}\ +.m-dark .boardTitle { color: #C5C8C6; }\ +.m-dark .mobile-report { color: #81a2be !important; }\ +.m-dark .mobile-report:after,\ +.m-dark .mobile-report:before { color: #1d1f21 !important; }\ +.m-dark hr, .m-dark div.board > hr { border-top: 1px solid #282A2E; }\ +.m-dark div.opContainer,\ +.m-dark div.reply { background-color: #282A2E; }\ +.m-dark .preview { background-color: #282A2E; border: 1px solid #333 !important; }\ +.m-dark div.post div.postInfoM { background-color: #212326; border-bottom: 1px solid #2D2F33; }\ +.m-dark div.postLink,\ +.m-dark .backlink.mobile { background-color: #212326; border-top: 1px solid #2D2F33; }\ +.m-dark div.post div.postInfoM span.dateTime,\ +.m-dark div.postLink span.info,\ +.m-dark div.post div.postInfoM span.dateTime a { color: #707070 !important; }\ +.m-dark span.subject { color: #B294BB !important; }\ +.m-dark .highlightPost:not(.op) { background: #3A171C !important; }\ +.m-dark .reply:target, .m-dark .reply.highlight { background: #1D1D21 !important; padding: 2px; }\ +.m-dark .reply:target, .m-dark .reply.highlight { background: #1D1D21 !important; padding: 2px; }\ +}'; + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = css; + document.head.appendChild(style); +}; + +Main.init(); diff --git a/js/extension.js b/js/extension.js new file mode 100644 index 0000000..40fd710 --- /dev/null +++ b/js/extension.js @@ -0,0 +1,11227 @@ +/******************************** + * * + * 4chan Extension * + * * + ********************************/ + +/** + * Helpers + */ +var $ = {}; + +$.id = function(id) { + return document.getElementById(id); +}; + +$.cls = function(klass, root) { + return (root || document).getElementsByClassName(klass); +}; + +$.byName = function(name) { + return document.getElementsByName(name); +}; + +$.tag = function(tag, root) { + return (root || document).getElementsByTagName(tag); +}; + +$.el = function(tag) { + return document.createElement(tag); +}; + +$.qs = function(sel, root) { + return (root || document).querySelector(sel); +}; + +$.qsa = function(selector, root) { + return (root || document).querySelectorAll(selector); +}; + +$.extend = function(destination, source) { + for (var key in source) { + destination[key] = source[key]; + } +}; + +$.on = function(n, e, h) { + n.addEventListener(e, h, false); +}; + +$.off = function(n, e, h) { + n.removeEventListener(e, h, false); +}; + +if (!document.documentElement.classList) { + $.hasClass = function(el, klass) { + return (' ' + el.className + ' ').indexOf(' ' + klass + ' ') != -1; + }; + + $.addClass = function(el, klass) { + el.className = (el.className === '') ? klass : el.className + ' ' + klass; + }; + + $.removeClass = function(el, klass) { + el.className = (' ' + el.className + ' ').replace(' ' + klass + ' ', ''); + }; +} +else { + $.hasClass = function(el, klass) { + return el.classList.contains(klass); + }; + + $.addClass = function(el, klass) { + el.classList.add(klass); + }; + + $.removeClass = function(el, klass) { + el.classList.remove(klass); + }; +} + +$.get = function(url, callbacks, headers) { + var key, xhr; + + xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + if (callbacks) { + for (key in callbacks) { + xhr[key] = callbacks[key]; + } + } + if (headers) { + for (key in headers) { + xhr.setRequestHeader(key, headers[key]); + } + } + xhr.send(null); + return xhr; +}; + +$.xhr = function(method, url, callbacks, data) { + var key, xhr, form; + + xhr = new XMLHttpRequest(); + + xhr.open(method, url, true); + + if (callbacks) { + for (key in callbacks) { + xhr[key] = callbacks[key]; + } + } + + if (data) { + form = new FormData(); + for (key in data) { + form.append(key, data[key]); + } + data = form; + } + else { + data = null; + } + + xhr.send(data); + + return xhr; +}; + +$.fit = function(w, h, maxW, maxH) { + var r, outW, outH; + + r = w / h; + + if (w > maxW) { + outW = maxW; + outH = Math.round(outW / r); + + if (outH > maxH) { + outH = maxH; + outW = Math.round(outH * r); + } + } + else if (h > maxH) { + outH = maxH; + outW = Math.round(outH * r); + + if (outW > maxW) { + outW = maxW; + outH = Math.round(outW / r); + } + } + else { + outW = w; + outH = h; + } + + return [outW, outH]; +}; + +$.ago = function(timestamp) { + var delta, count, head, tail; + + delta = Date.now() / 1000 - timestamp; + + if (delta < 1) { + return 'moments ago'; + } + + if (delta < 60) { + return (0 | delta) + ' seconds ago'; + } + + if (delta < 3600) { + count = 0 | (delta / 60); + + if (count > 1) { + return count + ' minutes ago'; + } + else { + return 'one minute ago'; + } + } + + if (delta < 86400) { + count = 0 | (delta / 3600); + + if (count > 1) { + head = count + ' hours'; + } + else { + head = 'one hour'; + } + + tail = 0 | (delta / 60 - count * 60); + + if (tail > 1) { + head += ' and ' + tail + ' minutes'; + } + + return head + ' ago'; + } + + count = 0 | (delta / 86400); + + if (count > 1) { + head = count + ' days'; + } + else { + head = 'one day'; + } + + tail = 0 | (delta / 3600 - count * 24); + + if (tail > 1) { + head += ' and ' + tail + ' hours'; + } + + return head + ' ago'; +}; + +$.hash = function(str) { + var i, j, msg = 0; + for (i = 0, j = str.length; i < j; ++i) { + msg = ((msg << 5) - msg) + str.charCodeAt(i); + } + return msg; +}; + +$.prettySeconds = function(fs) { + var m, s; + + m = Math.floor(fs / 60); + s = Math.round(fs - m * 60); + + return [ m, s ]; +}; + +$.docEl = document.documentElement; + +$.cache = {}; + +/** + * Parser + */ +var Parser = { + tipTimeout: null +}; + +Parser.init = function() { + var o, a, h, m, tail, staticPath; + + if (Config.filter || Config.linkify || Config.embedSoundCloud + || Config.embedYouTube || Main.hasMobileLayout) { + this.needMsg = true; + } + + staticPath = '//s.4cdn.org/image/'; + + tail = window.devicePixelRatio >= 2 ? '@2x.gif' : '.gif'; + + this.icons = { + admin: staticPath + 'adminicon' + tail, + founder: staticPath + 'foundericon' + tail, + mod: staticPath + 'modicon' + tail, + dev: staticPath + 'developericon' + tail, + manager: staticPath + 'managericon' + tail, + del: staticPath + 'filedeleted-res' + tail + }; + + this.prettify = typeof prettyPrint == 'function'; + + this.customSpoiler = {}; + + if (Config.localTime) { + if (o = (new Date()).getTimezoneOffset()) { + a = Math.abs(o); + h = (0 | (a / 60)); + + this.utcOffset = 'Timezone: UTC' + (o < 0 ? '+' : '-') + + h + ((m = a - h * 60) ? (':' + m) : ''); + } + else { + this.utcOffset = 'Timezone: UTC'; + } + + this.weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + } + + if (Main.tid) { + this.trackedReplies = this.getTrackedReplies(Main.board, Main.tid); + + if (this.trackedReplies) { + this.touchTrackedReplies(Main.tid); + } + else { + this.trackedReplies = {}; + } + + this.pruneTrackedReplies(); + } + + this.postMenuIcon = Main.hasMobileLayout ? '...' : '▶'; +}; + +Parser.getTrackedReplies = function(board, tid) { + var tracked = null; + + if (tracked = localStorage.getItem('4chan-track-' + board + '-' + tid)) { + tracked = JSON.parse(tracked); + } + + return tracked; +}; + +Parser.saveTrackedReplies = function(tid, replies) { + var key = '4chan-track-' + Main.board + '-' + tid; + + localStorage.setItem(key, JSON.stringify(replies)); + + //StorageSync.sync(key); +}; + +Parser.touchTrackedReplies = function(tid) { + var tracked, key; + + key = '4chan-track-' + Main.board + '-ts'; + + if (tracked = localStorage.getItem(key)) { + tracked = JSON.parse(tracked); + } + else { + tracked = {}; + } + + tracked[tid] = 0 | (Date.now() / 1000); + localStorage.setItem(key, JSON.stringify(tracked)); +}; + +Parser.pruneTrackedReplies = function() { + var tid, tracked, now, thres, ttl, pfx, flag; + + pfx = '4chan-track-' + Main.board + '-'; + + if (tracked = localStorage.getItem(pfx + 'ts')) { + ttl = 259200; + now = 0 | (Date.now() / 1000); + thres = now - ttl; + + flag = false; + + tracked = JSON.parse(tracked); + + if (Main.tid && tracked[Main.tid]) { + tracked[Main.tid] = now; + flag = true; + } + + for (tid in tracked) { + if (tracked[tid] <= thres) { + flag = true; + delete tracked[tid]; + localStorage.removeItem(pfx + tid); + //StorageSync.queue.push(pfx + tid); + } + } + + if (flag) { + localStorage.removeItem(pfx + 'ts'); + + for (tid in tracked) { + localStorage.setItem(pfx + 'ts', JSON.stringify(tracked)); + break; + } + + //StorageSync.queue.push(pfx + 'ts'); + } + + //StorageSync.send(); + } +}; + +Parser.parseThreadJSON = function(data) { + var thread; + + try { + thread = JSON.parse(data).posts; + } + catch (e) { + console.log(e); + thread = []; + } + + return thread; +}; + +Parser.parseCatalogJSON = function(data) { + var catalog; + + try { + catalog = JSON.parse(data); + } + catch (e) { + console.log(e); + catalog = []; + } + + return catalog; +}; + +Parser.setCustomSpoiler = function(board, val) { + var s; + if (!this.customSpoiler[board] && (val = parseInt(val))) { + if (board == Main.board && (s = $.cls('imgspoiler')[0])) { + this.customSpoiler[board] = + s.firstChild.src.match(/spoiler(-[a-z0-9]+)\.png$/)[1]; + } + else { + this.customSpoiler[board] = '-' + board + + (Math.floor(Math.random() * val) + 1); + } + } +}; + +Parser.buildPost = function(thread, board, pid) { + var i, j, uid, el = null; + + for (i = 0; j = thread[i]; ++i) { + if (j.no != pid) { + continue; + } + + if (!Config.revealSpoilers && thread[0].custom_spoiler) { + Parser.setCustomSpoiler(board, thread[0].custom_spoiler); + } + + el = Parser.buildHTMLFromJSON(j, board, false, true).lastElementChild; + + if (Config.IDColor && (uid = $.cls('posteruid', el)[Main.hasMobileLayout ? 0 : 1])) { + IDColor.applyRemote(uid.firstElementChild); + } + } + + return el; +}; + +Parser.decodeSpecialChars = function(str) { + return str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>'); +}; + +Parser.encodeSpecialChars = function(str) { + return str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +}; + +Parser.onDateMouseOver = function(el) { + if (Parser.tipTimeout) { + clearTimeout(Parser.tipTimeout); + Parser.tipTimeout = null; + } + + Parser.tipTimeout = setTimeout(Tip.show, 500, el, $.ago(+el.getAttribute('data-utc'))); +}; + +Parser.onTipMouseOut = function() { + if (Parser.tipTimeout) { + clearTimeout(Parser.tipTimeout); + Parser.tipTimeout = null; + } +}; + +Parser.onUIDMouseOver = function(el) { + var p; + + if (!$.hasClass(el.parentNode, 'posteruid')) { + return; + } + + if (!Main.tid) { + p = el.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode; + + if (!$.hasClass(p, 'tExpanded')) { + return; + } + } + + if (Parser.tipTimeout) { + clearTimeout(Parser.tipTimeout); + Parser.tipTimeout = null; + } + + Parser.tipTimeout = setTimeout(Parser.showUIDCount, 500, el, el.textContent); +}; + +Parser.showUIDCount = function(t, uid) { + var i, el, nodes, count, msg; + + count = 0; + nodes = $.qsa('.postInfo .hand'); + + for (i = 0; el = nodes[i]; ++i) { + if (el.textContent === uid) { + ++count; + } + } + + msg = count + ' post' + (count != 1 ? 's' : '') + ' by this ID'; + + Tip.show(t, msg); +}; + +Parser.buildHTMLFromJSON = function(data, board, standalone, fromQuote) { + var + container = document.createElement('div'), + isOP = false, + + userId, + fileDims = '', + imgSrc = '', + fileInfo = '', + fileHtml = '', + fileThumb, + filePath, + fileName, + fileSpoilerTip = '"', + size = '', + fileClass = '', + shortFile = '', + longFile = '', + tripcode = '', + capcodeStart = '', + capcodeClass = '', + capcode = '', + flag, + highlight = '', + emailStart = '', + emailEnd = '', + name, mName, + subject, + noLink, + quoteLink, + replySpan = '', + noFilename, + decodedFilename, + mobileLink = '', + postType = 'reply', + summary = '', + postCountStr, + resto, + capcode_replies = '', + threadIcons = '', + boardTag = '', + needFileTip = false, + + i, q, href, quotes, tmp, + + imgDir; + /* + if (board !== 'f') { + if (data.no % 3 > 2) { + imgDir = '//is.4chan.org/' + board; + } + else { + imgDir = '//is2.4chan.org/' + board; + } + } + else {*/ + imgDir = '//i.4cdn.org/' + board; + //} + + if (!data.resto) { + isOP = true; + + if (standalone) { + if (data.replies > 0) { + tmp = data.replies + ' Repl' + (data.replies > 1 ? 'ies' : 'y'); + if (data.images > 0) { + tmp += ' / ' + data.images + ' Image' + (data.images > 1 ? 's' : ''); + } + } + else { + tmp = ''; + } + + mobileLink = ''; + postType = 'op'; + replySpan = '  [Reply]'; + } + + if (board != Main.board) { + boardTag = '/' + board + '/ '; + } + + resto = data.no; + } + else { + resto = data.resto; + } + + + if (!Main.tid || board != Main.board) { + noLink = '//boards.' + $L.d(board) + '/' + board + '/thread/' + resto + '#p' + data.no; + quoteLink = '//boards.' + $L.d(board) + '/' + board + '/thread/' + resto + '#q' + data.no; + } + else { + noLink = '#p' + data.no; + quoteLink = 'javascript:quote(\'' + data.no + '\')'; + } + + if (!data.capcode && data.id) { + userId = ' (ID: ' + + data.id + ') '; + } + else { + userId = ''; + } + + switch (data.capcode) { + case 'admin_highlight': + highlight = ' highlightPost'; + /* falls through */ + case 'admin': + capcodeStart = ' ## Admin'; + capcodeClass = ' capcodeAdmin'; + + capcode = ' '; + break; + case 'mod': + capcodeStart = ' ## Mod'; + capcodeClass = ' capcodeMod'; + + capcode = ' '; + break; + case 'developer': + capcodeStart = ' ## Developer'; + capcodeClass = ' capcodeDeveloper'; + + capcode = ' '; + break; + case 'manager': + capcodeStart = ' ## Manager'; + capcodeClass = ' capcodeManager'; + + capcode = ' '; + break; + case 'founder': + capcodeStart = ' ## Founder'; + capcodeClass = ' capcodeAdmin'; + + capcode = ' '; + break; + case 'verified': + capcodeStart = ' ## Verified'; + capcodeClass = ' capcodeVerified'; + + capcode = ''; + break; + } + + if (data.email) { + emailStart = ''; + emailEnd = ''; + } + + if (data.flag_name) { + flag = ' '; + } + else if (data.country_name) { + flag = ' '; + } + else { + flag = ''; + } + + if (data.filedeleted) { + fileHtml = '
    File deleted.
    '; + } + else if (data.ext) { + decodedFilename = Parser.decodeSpecialChars(data.filename); + + shortFile = longFile = data.filename + data.ext; + + if (decodedFilename.length > (isOP ? 40 : 30)) { + shortFile = Parser.encodeSpecialChars( + decodedFilename.slice(0, isOP ? 35 : 25) + ) + '(...)' + data.ext; + + needFileTip = true; + } + + if (!data.tn_w && !data.tn_h && data.ext == '.gif') { + data.tn_w = data.w; + data.tn_h = data.h; + } + if (data.fsize >= 1048576) { + size = ((0 | (data.fsize / 1048576 * 100 + 0.5)) / 100) + ' M'; + } + else if (data.fsize > 1024) { + size = (0 | (data.fsize / 1024 + 0.5)) + ' K'; + } + else { + size = data.fsize + ' '; + } + + if (data.spoiler) { + if (!Config.revealSpoilers) { + fileName = 'Spoiler Image'; + fileSpoilerTip = '" title="' + longFile + '"'; + fileClass = ' imgspoiler'; + + fileThumb = '//s.4cdn.org/image/spoiler' + + (Parser.customSpoiler[board] || '') + '.png'; + data.tn_w = 100; + data.tn_h = 100; + + noFilename = true; + } + else { + fileName = shortFile; + } + } + else { + fileName = shortFile; + } + + if (!fileThumb) { + fileThumb = '//i.4cdn.org/' + board + '/' + data.tim + 's.jpg'; + } + + fileDims = data.ext == '.pdf' ? 'PDF' : data.w + 'x' + data.h; + + if (board != 'f') { + filePath = imgDir + '/' + data.tim + data.ext; + + imgSrc = '' + size + 'B' + + '
    ' + + size + 'B ' + data.ext.slice(1).toUpperCase() + + '
    '; + + fileInfo = '
    ' + + fileName + ' (' + size + 'B, ' + fileDims + ')
    '; + } + else { + filePath = imgDir + '/' + data.filename + data.ext; + + fileDims += ', ' + data.tag; + + fileInfo = '
    File: ' + + data.filename + '.swf (' + size + 'B, ' + fileDims + ')
    '; + } + + fileHtml = '
    ' + + fileInfo + imgSrc + '
    '; + } + + if (data.trip) { + tripcode = ' ' + data.trip + ''; + } + + name = data.name || ''; + + if (Main.hasMobileLayout && name.length > 30) { + mName = '' + + Parser.truncate(name, 30) + '(...) '; + } + else { + mName = '' + name + ' '; + } + + if (isOP) { + if (data.capcode_replies) { + capcode_replies = Parser.buildCapcodeReplies(data.capcode_replies, board, data.no); + } + + if (fromQuote && data.replies) { + postCountStr = data.replies + ' repl' + (data.replies > 1 ? 'ies' : 'y'); + + if (data.images) { + postCountStr += ' and ' + data.images + ' image' + + (data.images > 1 ? 's' : ''); + } + + summary = '' + postCountStr + '.'; + } + + if (data.sticky) { + threadIcons += 'Sticky '; + } + + if (data.closed) { + if (data.archived) { + threadIcons += 'Archived '; + } + else { + threadIcons += 'Closed '; + } + } + + if (data.sub === undefined) { + subject = ' '; + } + else if (Main.hasMobileLayout && data.sub.length > 30) { + subject = '' + + Parser.truncate(data.sub, 30) + '(...) '; + } + else { + subject = '' + data.sub + ' '; + } + } + else { + subject = ''; + } + + container.className = 'postContainer ' + postType + 'Container'; + container.id = 'pc' + data.no; + + if (data.xa24) { + container.className += ' p-xa24-' + data.xa24; + } + + container.innerHTML = + (isOP ? '' : '
    >>
    ') + + '
    ' + + '' + + (isOP ? fileHtml : '') + + '' + + (isOP ? '' : fileHtml) + + '
    ' + + (data.com || '') + capcode_replies + summary + '
    ' + + '
    ' + mobileLink; + + if (!Main.tid || board != Main.board) { + quotes = container.getElementsByClassName('quotelink'); + for (i = 0; q = quotes[i]; ++i) { + href = q.getAttribute('href'); + if (href.charAt(0) != '/') { + q.href = '//boards.' + $L.d(board) + '/' + board + '/thread/' + resto + href; + } + } + } + + return container; +}; + +Parser.truncate = function(str, len) { + str = str.replace(',', ','); + str = Parser.decodeSpecialChars(str); + str = str.slice(0, len); + str = Parser.encodeSpecialChars(str); + return str; +}; + +Parser.buildCapcodeReplies = function(replies, board, tid) { + var i, capcode, id, html, map, post_ids, prelink, pretext; + + map = { + admin: 'Administrator', + mod: 'Moderator', + developer: 'Developer', + manager: 'Manager' + }; + + if (board != Main.board) { + prelink = '/' + board + '/thread/'; + pretext = '>>>/' + board + '/'; + } + else { + prelink = ''; + pretext = '>>'; + } + + html = '

    '; + + for (capcode in replies) { + html += '' + map[capcode] + ' Replies: '; + + post_ids = replies[capcode]; + + for (i = 0; id = post_ids[i]; ++i) { + html += '' + pretext + id + ' '; + } + } + + return html + ''; +}; + +Parser.parseBoard = function() { + var i, threads = document.getElementsByClassName('thread'); + + for (i = 0; threads[i]; ++i) { + Parser.parseThread(threads[i].id.slice(1)); + } +}; + +Parser.parseThread = function(tid, offset, limit) { + var i, j, thread, posts, pi, el, frag, summary, omitted, key, filtered, cnt; + + thread = $.id('t' + tid); + posts = thread.getElementsByClassName('post'); + + if (!offset) { + pi = document.getElementById('pi' + tid); + + if (!Main.tid) { + if (Config.filter) { + filtered = Filter.exec( + thread, + pi, + document.getElementById('m' + tid), + tid + ); + } + + if (Config.threadHiding && !filtered) { + if (!Main.hasMobileLayout) { + el = document.createElement('span'); + el.innerHTML = 'H'; + posts[0].insertBefore(el, posts[0].firstChild); + el.id = 'sa' + tid; + } + if (ThreadHiding.hidden[tid]) { + ThreadHiding.hidden[tid] = Main.now; + ThreadHiding.hide(tid); + } + } + + if (ThreadExpansion.enabled + && (summary = $.cls('summary', thread)[0])) { + frag = document.createDocumentFragment(); + + omitted = summary.cloneNode(true); + omitted.className = ''; + summary.textContent = ''; + + el = document.createElement('img'); + el.className = 'extButton expbtn'; + el.title = 'Expand thread'; + el.alt = '+'; + el.setAttribute('data-cmd', 'expand'); + el.setAttribute('data-id', tid); + el.src = Main.icons.plus; + frag.appendChild(el); + + frag.appendChild(omitted); + + el = document.createElement('span'); + el.style.display = 'none'; + el.textContent = 'Showing all replies.'; + frag.appendChild(el); + + summary.appendChild(frag); + } + } + + if (Main.tid && Config.threadWatcher) { + el = document.createElement('img'); + + if (ThreadWatcher.watched[key = tid + '-' + Main.board]) { + el.src = Main.icons.watched; + el.setAttribute('data-active', '1'); + } + else { + el.src = Main.icons.notwatched; + } + + el.className = 'extButton wbtn wbtn-' + key; + el.setAttribute('data-cmd', 'watch'); + el.setAttribute('data-id', tid); + el.alt = 'W'; + el.title = 'Add to watch list'; + + cnt = $.cls('navLinks'); + + for (i = 1; i < 3 && (j = cnt[i]); ++i) { + frag = document.createDocumentFragment(); + frag.appendChild(document.createTextNode('[')); + frag.appendChild(el.cloneNode(true)); + frag.appendChild(document.createTextNode('] ')); + j.insertBefore(frag, j.firstChild); + } + } + } + + j = offset ? offset < 0 ? posts.length + offset : offset : 0; + limit = limit ? j + limit : posts.length; + + if (Main.isMobileDevice && Config.quotePreview) { + for (i = j; i < limit; ++i) { + Parser.parseMobileQuotelinks(posts[i]); + } + } + + if (Parser.trackedReplies) { + for (i = j; i < limit; ++i) { + Parser.parseTrackedReplies(posts[i]); + } + } + + for (i = j; i < limit; ++i) { + Parser.parsePost(posts[i].id.slice(1), tid); + } + + if (offset) { + if (Parser.prettify) { + for (i = j; i < limit; ++i) { + Parser.parseMarkup(posts[i]); + } + } + if (window.math_tags) { + if (window.MathJax) { + for (i = j; i < limit; ++i) { + if (Parser.postHasMath(posts[i])) { + window.cleanWbr(posts[i]); + } + MathJax.Hub.Queue(['Typeset', MathJax.Hub, posts[i]]); + } + } + else { + for (i = j; i < limit; ++i) { + if (Parser.postHasMath(posts[i])) { + window.loadMathJax(); + } + } + } + } + } + + UA.dispatchEvent('4chanParsingDone', { threadId: tid, offset: j, limit: limit }); +}; + +Parser.postHasMath = function(el) { + return /\[(?:eqn|math)\]/.test(el.innerHTML); +}; + +Parser.parseMathOne = function(node) { + if (window.MathJax) { + MathJax.Hub.Queue(['Typeset', MathJax.Hub, node]); + } + else if (Parser.postHasMath(node)) { + window.loadMathJax(); + } +}; + +Parser.parseTrackedReplies = function(post) { + var i, link, quotelinks; + + quotelinks = $.cls('quotelink', post); + + for (i = 0; link = quotelinks[i]; ++i) { + if (Parser.trackedReplies[link.textContent]) { + link.className += ' ql-tracked'; + link.textContent += ' (You)'; + Parser.hasYouMarkers = true; + } + } +}; + +Parser.parseMobileQuotelinks = function(post) { + var i, link, quotelinks, t, el; + + quotelinks = $.cls('quotelink', post); + + for (i = 0; link = quotelinks[i]; ++i) { + t = link.getAttribute('href').match(/(?:\/([a-z0-9]+)\/thread\/)?([0-9]+)?#p([0-9]+)$/); + + if (!t) { + continue; + } + + el = document.createElement('a'); + el.href = link.href; + el.textContent = ' #'; + el.className = 'quoteLink'; + + link.parentNode.insertBefore(el, link.nextSibling); + } +}; + +Parser.parseMarkup = function(post) { + var i, pre, el; + + if ((pre = post.getElementsByClassName('prettyprint'))[0]) { + for (i = 0; el = pre[i]; ++i) { + el.innerHTML = window.prettyPrintOne(el.innerHTML); + } + } +}; + +Parser.revealImageSpoiler = function(fileThumb) { + var img, isOP, filename, finfo, txt; + + img = fileThumb.firstElementChild; + fileThumb.removeChild(img); + img.removeAttribute('style'); + isOP = $.hasClass(fileThumb.parentNode.parentNode, 'op'); + img.style.maxWidth = img.style.maxHeight = isOP ? '250px' : '125px'; + img.src = '//i.4cdn.org' + + (fileThumb.pathname.replace(/\/([0-9]+).+$/, '/$1s.jpg')); + + filename = fileThumb.previousElementSibling; + finfo = filename.title.split('.'); + + if (finfo[0].length > (isOP ? 40 : 30)) { + txt = finfo[0].slice(0, isOP ? 35 : 25) + '(...)' + finfo[1]; + } + else { + txt = filename.title; + filename.removeAttribute('title'); + } + + filename.firstElementChild.innerHTML = txt; + fileThumb.insertBefore(img, fileThumb.firstElementChild); +}; + +Parser.parsePost = function(pid, tid) { + var hasMobileLayout, cnt, el, pi, file, msg, filtered, uid; + + hasMobileLayout = Main.hasMobileLayout; + + if (!tid) { + pi = pid.getElementsByClassName('postInfo')[0]; + pid = pi.id.slice(2); + } + else { + pi = document.getElementById('pi' + pid); + } + + if (Parser.needMsg) { + msg = document.getElementById('m' + pid); + } + + el = document.createElement('a'); + el.href = '#'; + el.className = 'postMenuBtn'; + el.title = 'Post menu'; + el.setAttribute('data-cmd', 'post-menu'); + el.textContent = Parser.postMenuIcon; + + if (hasMobileLayout) { + cnt = document.getElementById('pim' + pid); + cnt.insertBefore(el, cnt.firstElementChild); + } + else { + pi.appendChild(el); + } + + if (tid) { + if (pid != tid) { + if (Config.filter) { + filtered = Filter.exec(pi.parentNode, pi, msg); + } + + if (!filtered && ReplyHiding.hidden[pid]) { + ReplyHiding.hidden[pid] = Main.now; + ReplyHiding.hide(pid); + } + /* + if (ReplyHiding.hiddenR[pid]) { + ReplyHiding.hideR(pid, pid); + } + else if (ReplyHiding.hasR) { + if (ppid = ReplyHiding.shouldToggleR(msg)) { + ReplyHiding.hideR(pid, ppid); + } + } + */ + } + + if (Config.backlinks) { + Parser.parseBacklinks(pid, tid); + } + + if (Main.isOekakiBoard && Main.tid) { + Parser.addOekakiEditLink(pid, tid); + } + } + + if (IDColor.enabled && (uid = $.cls('posteruid', pi.parentNode)[hasMobileLayout ? 0 : 1])) { + IDColor.apply(uid.firstElementChild); + } + + if (Config.linkify) { + Linkify.exec(msg); + } + + if (Config.embedSoundCloud) { + Media.parseSoundCloud(msg); + } + + if (Config.embedYouTube || hasMobileLayout) { + Media.parseYouTube(msg); + } + + if (Config.revealSpoilers + && (file = document.getElementById('f' + pid)) + && (file = file.children[1]) + ) { + if ($.hasClass(file, 'imgspoiler')) { + Parser.revealImageSpoiler(file); + } + } + + if (Config.localTime) { + if (hasMobileLayout) { + el = pi.parentNode.getElementsByClassName('dateTime')[0]; + el.firstChild.nodeValue + = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)) + ' '; + } + else { + el = pi.getElementsByClassName('dateTime')[0]; + //el.title = this.utcOffset; + el.textContent + = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)); + } + } +}; + +Parser.addOekakiEditLink = function(pid, tid) { + var cnt, el, a; + + cnt = $.id('fT' + pid); + + if (!cnt) { + return; + } + + a = cnt.firstElementChild; + + if (!a.href || !/\.(png|jpg)$/.test(a.href)) { + return; + } + + el = $.el('small'); + el.innerHTML = ' Edit'; + cnt.appendChild(el); +}; + +Parser.getLocaleDate = function(date) { + return ('0' + (1 + date.getMonth())).slice(-2) + '/' + + ('0' + date.getDate()).slice(-2) + '/' + + ('0' + date.getFullYear()).slice(-2) + '(' + + this.weekdays[date.getDay()] + ')' + + ('0' + date.getHours()).slice(-2) + ':' + + ('0' + date.getMinutes()).slice(-2) + ':' + + ('0' + date.getSeconds()).slice(-2); +}; + +Parser.parseBacklinks = function(pid, tid) { + var i, j, msg, backlinks, linklist, ids, target, bl, el, href; + + msg = document.getElementById('m' + pid); + + if (!(backlinks = msg.getElementsByClassName('quotelink'))) { + return; + } + + linklist = {}; + + for (i = 0; j = backlinks[i]; ++i) { + // [tid, pid] + ids = j.getAttribute('href').split('#p'); + + if (!ids[1]) { + continue; + } + + if (ids[1] == tid) { + j.textContent += ' (OP)'; + } + + if (!(target = document.getElementById('pi' + ids[1]))) { + if (Main.tid && j.textContent.charAt(2) != '>' ) { + j.textContent += ' →'; + } + continue; + } + + // Already processed? + if (linklist[ids[1]]) { + continue; + } + + linklist[ids[1]] = true; + + // Backlink node + bl = document.createElement('span'); + + if (!Main.tid) { + href = 'thread/' + tid + '#p' + pid; + } + else { + href = '#p' + pid; + } + + if (!Main.hasMobileLayout) { + bl.innerHTML = '>>' + pid + ' '; + } + else { + bl.innerHTML = '>>' + pid + + ' # '; + } + + // Backlinks container + if (!(el = document.getElementById('bl_' + ids[1]))) { + el = document.createElement('div'); + el.id = 'bl_' + ids[1]; + el.className = 'backlink'; + + if (Main.hasMobileLayout) { + el.className = 'backlink mobile'; + target = document.getElementById('p' + ids[1]); + } + + target.appendChild(el); + } + + el.appendChild(bl); + } +}; + +Parser.buildSummary = function(tid, oRep, oImg) { + var el; + + if (oRep) { + oRep = oRep + ' repl' + (oRep > 1 ? 'ies' : 'y'); + } + else { + return null; + } + + if (oImg) { + oImg = ' and ' + oImg + ' image' + (oImg > 1 ? 's' : ''); + } + else { + oImg = ''; + } + + el = document.createElement('span'); + el.className = 'summary desktop'; + el.innerHTML = oRep + oImg + + ' omitted. Click here to view.'; + + return el; +}; + +var OgvCtrl = { + ogv: null, + cnt: null, + ctrl: {}, + + seeking: false, + visible: false, + tick: null, + + attach: function(ogv) { + this.detach(); + ogv.parentNode.appendChild(this.cnt); + $.on(ogv, 'mouseup', this.toggleCtrl); + this.ogv = ogv; + }, + + detach: function() { + if (!this.ogv) { + return; + } + this.ogv.stop(); + $.off(this.ogv, 'mouseup', this.toggleCtrl); + this.ctrl.play.classList.remove('ogv-toggled'); + this.ctrl.mute.classList.remove('ogv-toggled'); + this.hideCtrl(); + this.ogv = null; + this.seeking = false; + this.cnt.remove(); + }, + + init: function() { + if (this.cnt) { + return; + } + + let cnt = $.el('div'); + cnt.className = 'ogv-ctrl'; + + let el = $.el('div'); + el.className = 'ogv-btn'; + el.innerHTML = ''; + $.on(el, 'click', this.togglePlay, false); + this.ctrl.play = el; + cnt.appendChild(el); + + el = $.el('input'); + el.className = 'ogv-seek'; + el.type = 'range'; + el.min = 0; + el.value = 0; + el.max = 100; + el.step = 0.1; + $.on(el, 'change', this.onSeek, false); + $.on(el, 'mousedown', this.toggleSeek, false); + $.on(el, 'mouseup', this.toggleSeek, false); + this.ctrl.seek = el; + cnt.appendChild(el); + + el = $.el('div'); + el.className = 'ogv-ts'; + el.textContent = '0:00 / 0:00'; + this.ctrl.ts = el; + cnt.appendChild(el); + + el = $.el('div'); + el.className = 'ogv-btn'; + el.innerHTML = ''; + $.on(el, 'click', this.toggleMute, false); + this.ctrl.mute = el; + cnt.appendChild(el); + + el = $.el('input'); + el.className = 'ogv-vol'; + el.type = 'range'; + el.min = 0; + el.value = 50; + el.step = 0.1; + el.max = 100; + $.on(el, 'input', this.onVolInput, false); + this.ctrl.vol = el; + cnt.appendChild(el); + + el = $.el('div'); + el.className = 'ogv-btn'; + el.innerHTML = ''; + $.on(el, 'click', this.toggleFullscreen, false); + this.ctrl.fs = el; + cnt.appendChild(el); + + this.cnt = cnt; + }, + + onPlayEnd: function() { + if (OgvCtrl.ogv.seekable.length) { + OgvCtrl.ogv.currentTime = 0; + } + else { + OgvCtrl.ogv.stop(); + } + OgvCtrl.ogv.play(); + }, + + toggleCtrl: function() { + if (OgvCtrl.visible) { + OgvCtrl.hideCtrl(); + } + else { + OgvCtrl.cnt.style.display = 'flex'; + OgvCtrl.setTickTimeout(); + OgvCtrl.updateTimes(); + OgvCtrl.visible = true; + } + }, + + hideCtrl: function() { + OgvCtrl.cnt.style.display = 'none'; + OgvCtrl.clearTickTimeout(); + OgvCtrl.visible = false; + }, + + toggleSeek: function() { + OgvCtrl.seeking = !OgvCtrl.seeking; + }, + + seekTick: function() { + OgvCtrl.setTickTimeout(); + OgvCtrl.updateTimes(); + }, + + setTickTimeout: function() { + OgvCtrl.tick = setTimeout(OgvCtrl.seekTick, 500); + }, + + clearTickTimeout: function() { + clearTimeout(OgvCtrl.tick); + OgvCtrl.tick = null; + }, + + updateTimes: function() { + if (!OgvCtrl.ogv.duration) { + return; + } + + if (!OgvCtrl.seeking) { + OgvCtrl.ctrl.seek.value = ((OgvCtrl.ogv.currentTime / OgvCtrl.ogv.duration) * 100).toFixed(2); + } + + let dm = Math.floor(OgvCtrl.ogv.duration / 60); + let ds = Math.floor(OgvCtrl.ogv.duration - dm * 60); + + let m = Math.floor(OgvCtrl.ogv.currentTime / 60); + let s = Math.floor(OgvCtrl.ogv.currentTime - m * 60); + + OgvCtrl.ctrl.ts.textContent = `${m}:${s.toString().padStart(2, '0')} / ${dm}:${ds.toString().padStart(2, '0')}`; + }, + + togglePlay: function() { + if (OgvCtrl.ogv.paused) { + OgvCtrl.ogv.play(); + } + else { + OgvCtrl.ogv.pause(); + } + OgvCtrl.ctrl.play.classList.toggle('ogv-toggled'); + }, + + onSeek: function() { + OgvCtrl.ogv.currentTime = (this.value / 100) * OgvCtrl.ogv.duration; + }, + + toggleMute: function() { + OgvCtrl.ogv.muted = !OgvCtrl.ogv.muted; + OgvCtrl.ctrl.mute.classList.toggle('ogv-toggled'); + }, + + onVolInput: function() { + OgvCtrl.ogv.volume = this.value / 100; + }, + + toggleFullscreen: function() { + if (document.fullscreenElement) { + document.exitFullscreen(); + } + else { + OgvCtrl.ogv.parentNode.requestFullscreen(); + } + + OgvCtrl.ctrl.fs.classList.toggle('ogv-toggled'); + } +}; + +/** + * Post Menu + */ +var PostMenu = { + activeBtn: null +}; + +PostMenu.open = function(btn) { + var div, html, pid, board, btnPos, el, href, left, limit, isOP, file; + + if (PostMenu.activeBtn == btn) { + PostMenu.close(); + return; + } + + PostMenu.close(); + + pid = btn.parentNode.id.replace(/^[0-9]*[^0-9]+/, ''); + + board = btn.parentNode.getAttribute('data-board'); + + isOP = !board && !!$.id('t' + pid); + + html = '
    • Report post
    • '; + + if (isOP) { + if (Config.threadHiding && !Main.tid) { + html += '
    • ' + + ($.hasClass($.id('t' + pid), 'post-hidden') ? 'Unhide' : 'Hide') + + ' thread
    • '; + } + if (Config.threadWatcher) { + html += '
    • ' + + (ThreadWatcher.watched[pid + '-' + Main.board] ? 'Remove from' : 'Add to') + + ' watch list
    • '; + } + } + else if (el = $.id('pc' + pid)) { + html += '
    • ' + + ($.hasClass(el, 'post-hidden') ? 'Unhide' : 'Hide') + + ' post
    • '; + } + + if (Main.hasMobileLayout) { + html += '
    • Delete post
    • '; + } + + if (file = $.id('fT' + pid)) { + el = $.cls('fileThumb', file.parentNode)[0]; + + if (el) { + if (/(gif|jpg|png)$/.test(el.href)) { + href = el.href; + } + else { + href = 'http://i.4cdn.org/' + Main.board + '/' + + el.href.match(/\/([0-9]+)m?\..+$/)[1] + 's.jpg'; + } + + if (Main.hasMobileLayout) { + html += '
    • Delete file
    • ' + + '
    • Open original file
    • ' + + '
    • Search image on Google
    • ' + + '
    • Search image on Yandex
    • ' + + '
    • Search image on SauceNAO
    • '; + } + else { + html += '
    • Image search »
    • '; + } + } + } + + if (Config.filter) { + html += '
    • Filter selected text
    • '; + } + + div = document.createElement('div'); + div.id = 'post-menu'; + div.className = 'dd-menu'; + div.innerHTML = html + '
    '; + + btnPos = btn.getBoundingClientRect(); + + div.style.top = btnPos.bottom + 3 + window.pageYOffset + 'px'; + + document.addEventListener('click', PostMenu.close, false); + + $.addClass(btn, 'menuOpen'); + PostMenu.activeBtn = btn; + + UA.dispatchEvent('4chanPostMenuReady', { postId: pid, isOP: isOP, node: div.firstElementChild }); + + document.body.appendChild(div); + + left = btnPos.left + window.pageXOffset; + limit = $.docEl.clientWidth - div.offsetWidth; + + if (left > (limit - 75)) { + div.className += ' dd-menu-left'; + } + + if (left > limit) { + left = limit; + } + + div.style.left = left + 'px'; +}; + +PostMenu.close = function() { + var el; + + if (el = $.id('post-menu')) { + el.parentNode.removeChild(el); + document.removeEventListener('click', PostMenu.close, false); + $.removeClass(PostMenu.activeBtn, 'menuOpen'); + PostMenu.activeBtn = null; + } +}; + +/** + * + */ +var Search = { + xhr: null, + + pageSize: 10, + + maxPages: 10, + + init: function() { + var el; + + if (el = $.id('g-search-form')) { + $.on(el, 'submit', Search.onSearch); + $.on(window, 'hashchange', Search.onHashChanged); + + if (location.host == 'boards.4channel.org') { + Search.initSelector(); + } + + Search.initFromURL(true); + } + }, + + initSelector: function() { + var i, el, nodes, sel, len; + + sel = $.id('js-sf-bf'); + nodes = sel.options; + len = nodes.length; + + for (i = len - 1; i >= 0; i--) { + el = nodes[i]; + if (el.value === '' || $L.d(el.value) === '4chan.org') { + sel.removeChild(el); + } + } + }, + + initFromURL: function(init) { + var self, frag, i, el, nodes, opt; + + self = Search; + + self.query = ''; + self.board = ''; + self.offset = 0; + + frag = window.location.hash; + + if (frag !== '' && frag.length <= 512) { + frag = frag.split('/').slice(1); + + if (frag[0]) { + self.query = decodeURIComponent(frag[0]); + } + else { + self.query = ''; + } + + self.board = frag[1] || ''; + + if (frag[1]) { + if (frag[1] === 'all') { + self.board = ''; + } + else { + self.board = frag[1]; + } + } + + self.offset = self.pageToOffset(0 | frag[2]); + } + + if (init && self.query === '') { + return; + } + + $.id('js-sf-qf').value = self.query; + + el = $.id('js-sf-bf'); + + el.selectedIndex = 0; + + for (i = 0, nodes = el.options; opt = nodes[i]; ++i) { + if (opt.value === self.board) { + el.selectedIndex = i; + break; + } + } + + if (el.selectedIndex === 0 && self.board !== '') { + self.board = ''; + } + + Search.exec(self.query, self.board, self.offset); + }, + + onHashChanged: function() { + Search.initFromURL(); + }, + + pageToOffset: function(p) { + if (p < 1 || p > Search.maxPages) { + p = 1; + } + + return (p - 1) * Search.pageSize; + }, + + offsetToPage: function(o) { + var p = o / Search.pageSize + 1; + + if (p < 1 || p > Search.maxPages) { + p = 1; + } + + return p; + }, + + updateURL: function() { + var self, frags = []; + + self = Search; + + if (self.query !== '') { + frags.push(encodeURIComponent(self.query)); + + if (self.offset > 0) { + if (!self.board || self.board === '') { + frags.push('all'); + } + else { + frags.push(self.board); + } + + frags.push(Search.offsetToPage(self.offset)); + } + else if (self.board) { + frags.push(self.board); + } + } + + if (frags.length) { + window.history.replaceState(null, '', '#/' + frags.join('/')); + } + else { + window.history.replaceState(null, '', + window.location.href.replace(/#.*$/, '') + ); + } + }, + + onSearch: function(e) { + var qf, bf; + + e && e.preventDefault(); + + Search.query = qf = $.id('js-sf-qf').value; + Search.board = bf = $.id('js-sf-bf').value; + + Search.exec(qf, bf, 0); + }, + + exec: function(query, board, offset) { + var self, qs = []; + + self = Search; + + self.toggleSpinner(false); + + self.updateCtrl(false); + + if (self.xhr) { + self.xhr.abort(); + self.xhr = null; + } + + if (query === '') { + return; + } + + qs.push('q=' + encodeURIComponent(query)); + + if (board !== '') { + qs.push('b=' + board); + } + + if (offset) { + qs.push('o=' + (0 | offset)); + } + + qs = qs.join('&'); + + self.query = query; + self.board = board; + self.offset = offset; + + self.updateURL(); + + self.toggleSpinner(true); + + self.xhr = $.get('https://find.' + location.host.replace(/^boards\./, '') + '/api?' + qs, { + onload: self.onLoad, + onerror: self.onError, + withCredentials: true + }); + }, + + onPageClick: function() { + var offset; + + offset = +this.getAttribute('data-o'); + + Search.exec(Search.query, Search.board, offset); + }, + + onLoad: function() { + var data; + + Search.toggleSpinner(false); + + try { + data = JSON.parse(this.responseText); + } + catch (err) { + Search.showError('Something went wrong.'); + console.log(err); + return; + } + + Search.buildResults(data); + }, + + onError: function() { + Search.toggleSpinner(false); + Search.showError('Connection error.'); + }, + + updateCtrl: function(offset, total) { + var cnt, el, el2, maxPage, curPage; + + if (offset === false) { + el = $.id('js-sf-pl'); + + if (el) { + el.parentNode.removeChild(el); + } + + return; + } + + cnt = $.id('js-sf-pl'); + + if (cnt) { + cnt.parentNode.removeChild(cnt); + } + + cnt = $.el('div'); + cnt.id = 'js-sf-pl'; + + if (!Main.hasMobileLayout) { + cnt.className = 'pagelist desktop'; + } + else { + cnt.className = 'mPagelist mobile'; + } + + total = +total; + offset = +offset; + + maxPage = Math.ceil(total / Search.pageSize); + + if (maxPage > Search.maxPages) { + maxPage = Search.maxPages; + } + + curPage = offset / Search.pageSize + 1; + + if (curPage > 1) { + el = $.el('div'); + el.className = 'prev'; + + if (!Main.hasMobileLayout) { + el2 = $.el('input'); + el2.type = 'button'; + el2.value = 'Previous'; + } + else { + el2 = $.el('a'); + el2.className = 'button'; + el2.textContent = 'Previous'; + } + + el2.setAttribute('data-o', offset - Search.pageSize); + + $.on(el2, 'click', Search.onPageClick); + + el.appendChild(el2); + + cnt.appendChild(el); + } + + el = $.el('div'); + el.className = 'pages'; + el.textContent = 'Page ' + curPage + ' / ' + maxPage; + + cnt.appendChild(el); + + if (curPage < maxPage) { + el = $.el('div'); + el.className = 'next'; + + if (!Main.hasMobileLayout) { + el2 = $.el('input'); + el2.type = 'button'; + el2.value = 'Next'; + } + else { + el2 = $.el('a'); + el2.className = 'button'; + el2.textContent = 'Next'; + } + + el2.setAttribute('data-o', offset + Search.pageSize); + + $.on(el2, 'click', Search.onPageClick); + + el.appendChild(el2); + + cnt.appendChild(el); + } + + el = $.id('delform'); + + el.parentNode.insertBefore(cnt, el.nextElementSibling); + }, + + showStatus: function(msg, cls) { + var cnt; + + cnt = $.cls('board')[0]; + + if (msg) { + cnt.innerHTML = '
    ' + msg + '
    '; + } + else { + cnt.innerHTML = ''; + } + }, + + showError: function(msg) { + Search.showStatus(msg, 'error'); + }, + + toggleSpinner: function(flag) { + var el = $.id('js-sf-btn'); + + if (flag) { + el.disabled = true; + Search.showStatus('Searching…', 'spnr'); + } + else { + el.disabled = false; + Search.showStatus(false); + } + }, + + buildResults: function(data) { + var j, k, op, cnt, threads, thread, boardDiv, reply; + + threads = data.threads; + + if (threads.length < 1) { + Search.showError('Nothing found.'); + Search.updateCtrl(false); + return; + } + + boardDiv = $.cls('board')[0]; + + boardDiv.textContent = ''; + + for (j = 0; thread = threads[j]; ++j) { + op = thread.posts[0]; + + cnt = $.el('div'); + cnt.id = 't' + op.no; + cnt.className = 'thread'; + + cnt.appendChild(Parser.buildHTMLFromJSON(op, thread.board, true)); + + for (k = 1; reply = thread.posts[k]; ++k) { + cnt.appendChild(Parser.buildHTMLFromJSON(reply, thread.board)); + } + + boardDiv.appendChild(cnt); + + boardDiv.appendChild($.el('hr')); + } + + Search.updateCtrl(data.offset, data.nhits); + } +}; + +/** + * Depager + */ +var Depager = {}; + +Depager.init = function() { + var el, el2, cnt; + + this.isLoading = false; + this.isEnabled = false; + this.isComplete = false; + this.threadsLoaded = false; + this.threadQueue = []; + this.debounce = 100; + this.threshold = 350; + + this.adId = 'azk53379'; + this.adZones = [ 16258, 16260 ]; + + this.boardHasAds = !!$.id(this.adId); + + if (this.boardHasAds) { + el = $.cls('ad-plea'); + this.adPlea = el[el.length - 1]; + } + + if (Main.hasMobileLayout) { + el = $.cls('next')[1]; + + if (!el) { + return; + } + + el = el.firstElementChild; + el.textContent = 'Load More'; + el.className += ' m-depagelink'; + el.setAttribute('data-cmd', 'depage'); + } + else { + el = $.cls('prev')[0]; + + if (!el) { + return; + } + + el.innerHTML = '[All]'; + el = el.firstElementChild; + } + + if (Config.alwaysDepage) { + this.isEnabled = true; + el.parentNode.parentNode.className += ' depagerEnabled'; + Depager.bindHandlers(); + + if (!Main.hasMobileLayout && (cnt = $.cls('board')[0])) { + el2 = document.createElement('span'); + el2.className = 'depageNumber'; + el2.textContent = 'Page 1'; + cnt.insertBefore(el2, cnt.firstElementChild); + } + } + else { + el.setAttribute('data-cmd', 'depage'); + } +}; + +Depager.onScroll = function() { + if (document.documentElement.scrollHeight + <= (Math.ceil(window.innerHeight + window.pageYOffset) + Depager.threshold)) { + if (Depager.threadsLoaded) { + Depager.renderNext(); + } + else { + Depager.depage(); + } + } +}; + +Depager.trackPageview = function(pageId) { + var url; + + try { + if (window._gat) { + url = '/' + Main.board + '/' + pageId; + window._gat._getTrackerByName()._trackPageview(url); + } + + if (window.__qc) { + window.__qc.qpixelsent = []; + window._qevents.push({ qacct: window.__qc.qopts.qacct }); + window.__qc.firepixels(); + } + } + catch(e) { + console.log(e); + } +}; + +Depager.insertAd = function(pageId, frag, zone, isLastPage) { + var wrap, cnt, nodes; + + if (!Depager.boardHasAds || !window.ados_add_placement) { + return; + } + + if (isLastPage) { + nodes = $.cls('bottomad'); + wrap = nodes[nodes.length - 1]; + cnt = document.createElement('div'); + cnt.id = 'azkDepage' + (pageId + 1); + wrap.appendChild(cnt); + window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone); + } + + wrap = document.createElement('div'); + wrap.className = 'bottomad center depaged-ad'; + + if (pageId == 2) { + cnt = $.id(Depager.adId); + } + else { + cnt = document.createElement('div'); + cnt.id = 'azkDepage' + pageId; + } + + wrap.appendChild(cnt); + frag.appendChild(wrap); + + if (Depager.adPlea) { + frag.appendChild(Depager.adPlea.cloneNode(true)); + } + + frag.appendChild(document.createElement('hr')); + + if (pageId != 2) { + window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone); + } +}; + +Depager.loadAds = function() { + if (!Depager.boardHasAds || !window.ados_load) { + return; + } + + window.ados_load(); +}; + +Depager.renderNext = function() { + var el, frag, i, j, k, threads, op, summary, cnt, reply, parseList, scroll, + last_replies, boardDiv, pageId, data, isLastPage; + + parseList = []; + + scroll = window.pageYOffset; + + frag = document.createDocumentFragment(); + + data = Depager.threadQueue.shift(); + + if (!data) { + return; + } + + threads = data.threads; + pageId = data.page; + + isLastPage = !Depager.threadQueue.length; + + Depager.insertAd(pageId, frag, data.adZone, isLastPage); + + el = document.createElement('span'); + el.className = 'depageNumber'; + el.textContent = 'Page ' + pageId; + frag.appendChild(el); + + for (j = 0; op = threads[j]; ++j) { + if ($.id('t' + op.no)) { + continue; + } + + cnt = document.createElement('div'); + cnt.id = 't' + op.no; + cnt.className = 'thread'; + + cnt.appendChild(Parser.buildHTMLFromJSON(op, Main.board, true)); + + if (summary = Parser.buildSummary(op.no, op.omitted_posts, op.omitted_images)) { + cnt.appendChild(summary); + } + + if (op.replies) { + last_replies = op.last_replies; + + for (k = 0; reply = last_replies[k]; ++k) { + cnt.appendChild(Parser.buildHTMLFromJSON(reply, Main.board)); + } + } + + frag.appendChild(cnt); + + frag.appendChild(document.createElement('hr')); + + parseList.push(op.no); + } + + if (isLastPage) { + Depager.unbindHandlers(); + Depager.isComplete = true; + Depager.setStatus('disabled'); + } + + boardDiv = $.cls('board')[0]; + boardDiv.insertBefore(frag, boardDiv.lastElementChild); + + Depager.trackPageview(pageId); + + Depager.loadAds(); + + for (i = 0; op = parseList[i]; ++i) { + Parser.parseThread(op); + } + + window.scrollTo(0, scroll); +}; + +Depager.bindHandlers = function() { + window.addEventListener('scroll', Depager.onScroll, false); + window.addEventListener('resize', Depager.onScroll, false); +}; + +Depager.unbindHandlers = function() { + window.removeEventListener('scroll', Depager.onScroll, false); + window.removeEventListener('resize', Depager.onScroll, false); +}; + +Depager.setStatus = function(type) { + var i, el, links, p, caption; + + if (!Main.hasMobileLayout) { + links = $.cls('depagelink'); + caption = 'All'; + } + else { + links = $.cls('m-depagelink'); + caption = 'Load More'; + } + + if (!links.length) { + return; + } + + if (type == 'enabled') { + for (i = 0; el = links[i]; ++i) { + el.textContent = caption; + p = el.parentNode.parentNode; + if (!$.hasClass(p, 'depagerEnabled')) { + $.addClass(p,'depagerEnabled'); + } + } + } + else if (type == 'loading') { + for (i = 0; el = links[i]; ++i) { + el.textContent = 'Loading…'; + } + } + else if (type == 'disabled') { + for (i = 0; el = links[i]; ++i) { + if (!Main.hasMobileLayout) { + el.textContent = caption; + $.removeClass(el.parentNode.parentNode,'depagerEnabled'); + } + else { + el.parentNode.parentNode.removeChild(el.parentNode); + } + } + } + else if (type == 'error') { + for (i = 0; el = links[i]; ++i) { + el.textContent = 'Error'; + el.removeAttribute('title'); + el.removeAttribute('data-cmd'); + $.removeClass(el.parentNode.parentNode, 'depagerEnabled'); + } + } +}; + +Depager.toggle = function() { + if (Depager.isLoading || Depager.isComplete) { + return; + } + + if (Depager.isEnabled) { + Depager.disable(); + } + else { + Depager.enable(); + } + + Depager.isEnabled = !Depager.isEnabled; +}; + +Depager.enable = function() { + Depager.bindHandlers(); + Depager.setStatus('enabled'); + Depager.onScroll(); +}; + +Depager.disable = function() { + Depager.unbindHandlers(); + Depager.setStatus('disabled'); +}; + +Depager.depage = function() { + if (Depager.isLoading) { + return; + } + + Depager.isLoading = true; + + $.get('//a.4cdn.org/' + Main.board + '/catalog.json', { + onload: Depager.onLoad, + onerror: Depager.onError + }); + + Depager.setStatus('loading'); +}; + +Depager.onLoad = function() { + var catalog, i, page, queue, adZone; + + Depager.isLoading = false; + Depager.threadsLoaded = true; + + if (this.status == 200) { + Depager.setStatus('enabled'); + + if (!Config.alwaysDepage) { + Depager.bindHandlers(); + } + + catalog = Parser.parseCatalogJSON(this.responseText); + + queue = Depager.threadQueue; + + adZone = 0; + for (i = 1; page = catalog[i]; ++i) { + page.adZone = Depager.adZones[adZone]; + queue.push(page); + adZone = adZone ? 0 : 1; + } + + Depager.renderNext(); + } + else if (this.status == 404) { + Depager.unbindHandlers(); + Depager.setStatus('error'); + } + else { + Depager.unbindHandlers(); + console.log('Error: ' + this.status); + Depager.setStatus('error'); + } +}; + +Depager.onError = function() { + Depager.isLoading = false; + Depager.unbindHandlers(); + console.log('Error: ' + this.status); + Depager.setStatus('error'); +}; + +/** + * Quote inlining + */ +var QuoteInline = {}; + +QuoteInline.isSelfQuote = function(node, pid, board) { + if (board && board != Main.board) { + return false; + } + + node = node.parentNode; + + if ((node.nodeName == 'BLOCKQUOTE' && node.id.split('m')[1] == pid) + || node.parentNode.id.split('_')[1] == pid) { + return true; + } + + return false; +}; + +QuoteInline.toggle = function(link, e) { + var i, j, t, pfx, src, el, count, media; + + t = link.getAttribute('href').match(/(?:\/([a-z0-9]+)\/thread\/)?([0-9]+)?#p([0-9]+)$/); + + if (!t || QuoteInline.isSelfQuote(link, t[3], t[1])) { + return; + } + + e && e.preventDefault(); + + if (pfx = link.getAttribute('data-pfx')) { + link.removeAttribute('data-pfx'); + $.removeClass(link, 'linkfade'); + + el = $.id(pfx + 'p' + t[3]); + + media = $.cls('expandedWebm', el); + + for (i = 0; j = media[i]; ++i) { + j.pause(); + } + + el.parentNode.removeChild(el); + + if (link.parentNode.parentNode.className == 'backlink') { + el = $.id('pc' + t[3]); + count = +el.getAttribute('data-inline-count') - 1; + if (count === 0) { + el.style.display = ''; + el.removeAttribute('data-inline-count'); + } + else { + el.setAttribute('data-inline-count', count); + } + } + + return; + } + + if (src = $.id('p' + t[3])) { + QuoteInline.inline(link, src, t[3]); + } + else { + QuoteInline.inlineRemote(link, t[1] || Main.board, t[2], t[3]); + } +}; + +QuoteInline.inlineRemote = function(link, board, tid, pid) { + var onload, onerror, cached, key, el, dummy; + + if (link.hasAttribute('data-loading')) { + return; + } + + key = board + '-' + tid; + + if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) { + Parser.parsePost(el); + QuoteInline.inline(link, el); + return; + } + + if ((dummy = link.nextElementSibling) && $.hasClass(dummy, 'spinner')) { + dummy.parentNode.removeChild(dummy); + return; + } + else { + dummy = document.createElement('div'); + } + + dummy.className = 'preview spinner inlined'; + dummy.textContent = 'Loading...'; + link.parentNode.insertBefore(dummy, link.nextSibling); + + onload = function() { + var el, thread; + + link.removeAttribute('data-loading'); + + if (this.status === 200 || this.status === 304 || this.status === 0) { + thread = Parser.parseThreadJSON(this.responseText); + + $.cache[key] = thread; + + if (el = Parser.buildPost(thread, board, pid)) { + dummy.parentNode && dummy.parentNode.removeChild(dummy); + Parser.parsePost(el); + QuoteInline.inline(link, el); + } + else { + $.addClass(link, 'deadlink'); + dummy.textContent = 'This post doesn\'t exist anymore'; + } + } + else if (this.status === 404) { + $.addClass(link, 'deadlink'); + dummy.textContent = 'This thread doesn\'t exist anymore'; + } + else { + this.onerror(); + } + }; + + onerror = function() { + dummy.textContent = 'Error: ' + this.statusText + ' (' + this.status + ')'; + link.removeAttribute('data-loading'); + }; + + link.setAttribute('data-loading', '1'); + + $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json', + { + onload: onload, + onerror: onerror + } + ); +}; + +QuoteInline.inline = function(link, src, id) { + var i, j, now, el, blcnt, isBl, inner, tblcnt, pfx, dest, count, cnt; + + now = Date.now(); + + if (id) { + if ((blcnt = link.parentNode.parentNode).className == 'backlink') { + el = blcnt.parentNode.parentNode.parentNode; + isBl = true; + } + else { + el = blcnt.parentNode; + } + + while (el.parentNode !== document) { + if (el.id.split('m')[1] == id) { + return; + } + el = el.parentNode; + } + } + + link.className += ' linkfade'; + link.setAttribute('data-pfx', now); + + QuotePreview.stopMedia(src); + + el = src.cloneNode(true); + el.id = now + el.id; + el.setAttribute('data-pfx', now); + el.className += ' preview inlined'; + $.removeClass(el, 'highlight'); + $.removeClass(el, 'highlight-anti'); + + if ((inner = $.cls('inlined', el))[0]) { + while (j = inner[0]) { + j.parentNode.removeChild(j); + } + inner = $.cls('quotelink', el); + for (i = 0; j = inner[i]; ++i) { + j.removeAttribute('data-pfx'); + $.removeClass(j, 'linkfade'); + } + } + + for (i = 0; j = el.children[i]; ++i) { + j.id = now + j.id; + } + + if (tblcnt = $.cls('backlink', el)[0]) { + tblcnt.id = now + tblcnt.id; + } + + if (isBl) { + pfx = blcnt.parentNode.parentNode.getAttribute('data-pfx') || ''; + dest = $.id(pfx + 'm' + blcnt.id.split('_')[1]); + dest.insertBefore(el, dest.firstChild); + if (count = src.parentNode.getAttribute('data-inline-count')) { + count = +count + 1; + } + else { + count = 1; + src.parentNode.style.display = 'none'; + } + src.parentNode.setAttribute('data-inline-count', count); + } + else { + if ($.hasClass(link.parentNode, 'quote')) { + link = link.parentNode; + cnt = link.parentNode; + } + else { + cnt = link.parentNode; + } + + while (cnt.nodeName === 'S') { + link = cnt; + cnt = cnt.parentNode; + } + + cnt.insertBefore(el, link.nextSibling); + } +}; + +/** + * Quote preview + */ +var QuotePreview = {}; + +QuotePreview.init = function() { + this.regex = /(?:\/([a-z0-9]+)\/thread\/)?([0-9]+)?#p([0-9]+)$/; + this.highlight = null; + this.highlightAnti = null; + this.cur = null; +}; + +QuotePreview.resolve = function(link) { + var self, t, post, offset, pfx; + + self = QuotePreview; + self.cur = null; + + t = link.getAttribute('href').match(self.regex); + + if (!t) { + return; + } + + // Quoted post in scope + pfx = link.getAttribute('data-pfx') || ''; + + if (post = document.getElementById(pfx + 'p' + t[3])) { + // Visible and not filtered out? + offset = post.getBoundingClientRect(); + if (offset.top > 0 + && offset.bottom < document.documentElement.clientHeight + && !$.hasClass(post.parentNode, 'post-hidden')) { + if (!$.hasClass(post, 'highlight') && location.hash.slice(1) != post.id) { + self.highlight = post; + $.addClass(post, 'highlight'); + } + else if (!$.hasClass(post, 'op')) { + self.highlightAnti = post; + $.addClass(post, 'highlight-anti'); + } + return; + } + // Nope + self.show(link, post); + } + // Quoted post out of scope + else { + if (!UA.hasCORS) { + return; + } + self.showRemote(link, t[1] || Main.board, t[2], t[3]); + } +}; + +QuotePreview.showRemote = function(link, board, tid, pid) { + var onload, onerror, el, cached, key; + + key = board + '-' + tid; + + QuotePreview.cur = key; + + if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) { + QuotePreview.show(link, el); + return; + } + + link.style.cursor = 'wait'; + + onload = function() { + var el, thread; + + link.style.cursor = ''; + + if (this.status === 200 || this.status === 304 || this.status === 0) { + thread = Parser.parseThreadJSON(this.responseText); + + $.cache[key] = thread; + + if ($.id('quote-preview') || QuotePreview.cur != key) { + return; + } + + if (el = Parser.buildPost(thread, board, pid)) { + el.className = 'post preview'; + el.style.display = 'none'; + el.id = 'quote-preview'; + document.body.appendChild(el); + QuotePreview.show(link, el, true); + } + else { + $.addClass(link, 'deadlink'); + } + } + else if (this.status === 404) { + $.addClass(link, 'deadlink'); + } + }; + + onerror = function() { + link.style.cursor = ''; + }; + + $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json', + { + onload: onload, + onerror: onerror + } + ); +}; + +QuotePreview.show = function(link, post, remote) { + var rect, postHeight, doc, docWidth, style, pos, quotes, i, j, qid, + top, scrollTop, img, media; + + QuotePreview.stopMedia(post); + + if (remote) { + Parser.parsePost(post); + post.style.display = ''; + } + else { + post = post.cloneNode(true); + if (location.hash && location.hash == ('#' + post.id)) { + post.className += ' highlight'; + } + post.id = 'quote-preview'; + post.className += ' preview' + + (!$.hasClass(link.parentNode.parentNode, 'backlink') ? ' reveal-spoilers' : ''); + + if (Config.imageExpansion && (img = $.cls('expanded-thumb', post)[0])) { + ImageExpansion.contract(img); + } + } + + if (media = $.cls('expandedWebm', post)[0]) { + media.controls = false; + media.autoplay = false; + } + + if (!link.parentNode.className) { + quotes = $.qsa( + '#' + $.cls('postMessage', post)[0].id + ' > .quotelink', post + ); + if (quotes[1]) { + qid = '>>' + link.parentNode.parentNode.id.split('_')[1]; + for (i = 0; j = quotes[i]; ++i) { + if (j.textContent == qid) { + $.addClass(j, 'dotted'); + break; + } + } + } + } + + rect = link.getBoundingClientRect(); + doc = document.documentElement; + docWidth = doc.offsetWidth; + style = post.style; + + document.body.appendChild(post); + + if (Main.isMobileDevice) { + style.top = rect.top + link.offsetHeight + window.pageYOffset + 'px'; + + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + style.right = docWidth - rect.right + 'px'; + } + else { + style.left = rect.left + 'px'; + } + } + else { + if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) { + pos = docWidth - rect.left; + style.right = pos + 5 + 'px'; + } + else { + pos = rect.left + rect.width; + style.left = pos + 5 + 'px'; + } + + top = rect.top + link.offsetHeight + window.pageYOffset + - post.offsetHeight / 2 - rect.height / 2; + + postHeight = post.getBoundingClientRect().height; + + if (doc.scrollTop != document.body.scrollTop) { + scrollTop = doc.scrollTop + document.body.scrollTop; + } else { + scrollTop = document.body.scrollTop; + } + + if (top < scrollTop) { + style.top = scrollTop + 'px'; + } + else if (top + postHeight > scrollTop + doc.clientHeight) { + style.top = scrollTop + doc.clientHeight - postHeight + 'px'; + } + else { + style.top = top + 'px'; + } + } +}; + +QuotePreview.remove = function(el) { + var self, cnt; + + self = QuotePreview; + self.cur = null; + + if (self.highlight) { + $.removeClass(self.highlight, 'highlight'); + self.highlight = null; + } + else if (self.highlightAnti) { + $.removeClass(self.highlightAnti, 'highlight-anti'); + self.highlightAnti = null; + } + + if (el) { + el.style.cursor = ''; + } + + if (cnt = $.id('quote-preview')) { + document.body.removeChild(cnt); + } +}; + +QuotePreview.stopMedia = function(el) { + var i, media; + + if ((media = $.tag('VIDEO', el))[0]) { + for (i = 0; el = media[i]; ++i) { + el.autoplay = false; + } + } + + if ((media = $.tag('AUDIO', el))[0]) { + for (i = 0; el = media[i]; ++i) { + el.autoplay = false; + } + } +}; + +/** + * Image expansion + */ +var ImageExpansion = { + activeVideos: [], + timeout: null, + pendingTarget: null +}; + +ImageExpansion.loadOgv = function(target) { + ImageExpansion.pendingTarget = target; + + if ($.id('js-ogv-scr')) { + return; + } + + let s = $.el('script'); + s.id = 'js-ogv-scr'; + s.onload = ImageExpansion.onOgvLoaded; + s.src = 'https://s.4cdn.org/js/ogv/ogv.js'; + document.body.appendChild(s); +}; + +ImageExpansion.onOgvLoaded = function() { + let self = ImageExpansion; + if (self.pendingTarget) { + self.expandWebm(self.pendingTarget); + } +}; + +ImageExpansion.expand = function(thumb) { + var img, href, ext, a; + + if (Config.imageHover) { + ImageHover.hide(); + } + + a = thumb.parentNode; + + href = a.getAttribute('href'); + + if (ext = href.match(/\.(?:webm|mp4|pdf)$/)) { + if (ext[0] == '.webm' || ext[0] == '.mp4') { + return ImageExpansion.expandWebm(thumb); + } + return false; + } + + if (Main.hasMobileLayout && a.hasAttribute('data-m')) { + href = ImageExpansion.setMobileSrc(a); + } + + thumb.setAttribute('data-expanding', '1'); + + img = document.createElement('img'); + img.alt = 'Image'; + img.setAttribute('src', href); + img.className = 'expanded-thumb'; + img.style.display = 'none'; + img.onerror = this.onError; + + thumb.parentNode.insertBefore(img, thumb.nextElementSibling); + + if (UA.hasCORS) { + thumb.style.opacity = '0.75'; + this.timeout = this.checkLoadStart(img, thumb); + } + else { + this.onLoadStart(img, thumb); + } + + return true; +}; + +ImageExpansion.contract = function(img) { + var cnt, p; + + clearTimeout(this.timeout); + + p = img.parentNode; + cnt = p.parentNode.parentNode; + + $.removeClass(p.parentNode, 'image-expanded'); + + if (Config.centeredThreads) { + $.removeClass(cnt.parentNode, 'centre-exp'); + cnt.parentNode.style.marginLeft = ''; + } + + if (!Main.tid && Config.threadHiding) { + $.removeClass(p, 'image-expanded-anti'); + } + + p.firstChild.style.display = ''; + + p.removeChild(img); + + if (cnt.offsetTop < window.pageYOffset) { + cnt.scrollIntoView(); + } +}; + +ImageExpansion.toggle = function(t) { + if (t.hasAttribute('data-md5')) { + if (!t.hasAttribute('data-expanding')) { + return ImageExpansion.expand(t); + } + } + else { + ImageExpansion.contract(t); + } + + return true; +}; + +ImageExpansion.setMobileSrc = function(a) { + var href; + + a.removeAttribute('data-m'); + href = a.getAttribute('href'); + a.setAttribute('data-orig', href); + href = href.replace(/\/([0-9]+).+$/, '/$1m.jpg'); + a.setAttribute('href', href); + + return href; +}; + +ImageExpansion.expandWebm = function(thumb) { + var el, link, fileText, left, href, maxWidth, self, cnt; + + self = ImageExpansion; + + if (el = document.getElementById('image-hover')) { + document.body.removeChild(el); + } + + link = thumb.parentNode; + + href = link.getAttribute('href'); + + left = link.getBoundingClientRect().left; + maxWidth = document.documentElement.clientWidth - left - 25; + + link.style.display = 'none'; + + if (href.match(/\.webm$/) && /iPhone|iPad/.test(navigator.userAgent)) { + if (!window.OGVPlayer) { + OgvCtrl.init(); + self.loadOgv(thumb); + return true; + } + + if (OgvCtrl.ogv) { + self.detachOgv(OgvCtrl.ogv); + } + + cnt = document.createElement('div'); + cnt.className = 'ogv-cnt expandedWebm'; + el = new OGVPlayer({ + wasm: true, + threading: false, + simd: false + }); + el.onloadedmetadata = self.fitWebm; + el.onvolumechange = Main.getWebmVolumeChangeCb(); + $.on(el, 'ended', OgvCtrl.onPlayEnd); + cnt.appendChild(el); + link.parentNode.appendChild(cnt); + el.src = href.replace(/\/\/.+\.4chan\.org\//, '//i.4cdn.org/'); + OgvCtrl.attach(el); + if (!Config.unmuteWebm) { + OgvCtrl.toggleMute(); + } + OgvCtrl.togglePlay(); + } + else { + el = document.createElement('video'); + el.muted = !Config.unmuteWebm; + el.controls = true; + el.loop = true; + el.autoplay = true; + el.className = 'expandedWebm'; + el.onloadedmetadata = self.fitWebm; + el.onvolumechange = Main.getWebmVolumeChangeCb(); + el.onplay = self.onWebmPlay; + link.parentNode.appendChild(el); + el.src = href; + } + + if (Config.unmuteWebm) { + el.volume = Main.getWebmVolume(); + } + + if (Main.hasMobileLayout) { + el = document.createElement('div'); + el.className = 'collapseWebm'; + el.innerHTML = 'Close'; + link.parentNode.appendChild(el); + } + else { + fileText = thumb.parentNode.previousElementSibling; + el = document.createElement('span'); + el.className = 'collapseWebm'; + el.innerHTML = '-[Close]'; + fileText.appendChild(el); + } + + $.addClass(link.parentNode, 'image-expanded'); + + el.firstElementChild.addEventListener('click', self.collapseWebm, false); + + return true; +}; + +ImageExpansion.fitWebm = function() { + var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, cntEl, + centerWidth, ofs, player, target; + + player = this; + + if (OgvCtrl.ogv) { + target = player.parentNode; + $.addClass(target, 'ogv-loaded'); + } + else { + target = player; + } + + if (Config.centeredThreads) { + centerWidth = $.cls('opContainer')[0].offsetWidth; + cntEl = target.parentNode.parentNode.parentNode; + $.addClass(cntEl, 'centre-exp'); + } + + left = player.getBoundingClientRect().left; + + maxWidth = document.documentElement.clientWidth - left; + maxHeight = document.documentElement.clientHeight; + + if (!Main.hasMobileLayout) { + maxWidth -= 25; + } + + imgWidth = player.videoWidth; + imgHeight = player.videoHeight; + + if (imgWidth > maxWidth) { + ratio = maxWidth / imgWidth; + imgWidth = maxWidth; + imgHeight = imgHeight * ratio; + } + + if (Config.fitToScreenExpansion && imgHeight > maxHeight) { + ratio = maxHeight / imgHeight; + imgHeight = maxHeight; + imgWidth = imgWidth * ratio; + } + + target.style.width = (0 | imgWidth) + 'px'; + target.style.height = (0 | imgHeight) + 'px'; + + if (player !== target) { + player.style.width = target.style.width; + player.style.height = target.style.height; + } + + if (Config.centeredThreads) { + left = target.getBoundingClientRect().left; + ofs = target.offsetWidth + left * 2; + if (ofs > centerWidth) { + left = Math.floor(($.docEl.clientWidth - ofs) / 2); + + if (left > 0) { + cntEl.style.marginLeft = left + 'px'; + } + } + else { + $.removeClass(cntEl, 'centre-exp'); + } + } +}; + +ImageExpansion.onWebmPlay = function() { + var self = ImageExpansion; + + if (!self.activeVideos.length) { + document.addEventListener('scroll', self.onScroll, false); + } + + self.activeVideos.push(this); +}; + +ImageExpansion.collapseWebm = function(e) { + var cnt, el, el2; + + e.preventDefault(); + + this.removeEventListener('click', ImageExpansion.collapseWebm, false); + + cnt = this.parentNode; + + $.removeClass(cnt.parentNode, 'image-expanded'); + + if (Main.hasMobileLayout) { + el = cnt.previousElementSibling; + } + else { + el = cnt.parentNode.parentNode.getElementsByClassName('expandedWebm')[0]; + } + + if (el.classList.contains('ogv-cnt')) { + if (!el.classList.contains('ogv-detached')) { + el.firstElementChild.stop(); + OgvCtrl.detach(); + } + } + else { + el.pause(); + } + + if (Config.centeredThreads) { + el2 = el.parentNode.parentNode.parentNode; + $.removeClass(el2, 'centre-exp'); + el2.style.marginLeft = ''; + } + + el.previousElementSibling.style.display = ''; + el.parentNode.removeChild(el); + cnt.parentNode.removeChild(cnt); +}; + +ImageExpansion.detachOgv = function(ogv) { + let cnt = ogv.parentNode; + cnt.style.width = ogv.style.width; + cnt.style.height = ogv.style.height; + cnt.classList.add('ogv-detached'); + ogv.remove(); +}; + +ImageExpansion.onScroll = function() { + clearTimeout(ImageExpansion.timeout); + ImageExpansion.timeout = setTimeout(ImageExpansion.pauseVideos, 500); +}; + +ImageExpansion.pauseVideos = function() { + var self, i, el, pos, min, max, nodes; + + self = ImageExpansion; + + nodes = []; + min = window.pageYOffset; + max = window.pageYOffset + $.docEl.clientHeight; + + for (i = 0; el = self.activeVideos[i]; ++i) { + pos = el.getBoundingClientRect(); + if (pos.top + window.pageYOffset > max || pos.bottom + window.pageYOffset < min) { + el.pause(); + } + else if (!el.paused){ + nodes.push(el); + } + } + + if (!nodes.length) { + document.removeEventListener('scroll', self.onScroll, false); + } + + self.activeVideos = nodes; +}; + +ImageExpansion.onError = function(e) { + var thumb, img; + + Feedback.error('File no longer exists (404).', 2000); + + img = e.target; + thumb = $.qs('img[data-expanding]', img.parentNode); + + img.parentNode.removeChild(img); + thumb.style.opacity = ''; + thumb.removeAttribute('data-expanding'); +}; + +ImageExpansion.onLoadStart = function(img, thumb) { + var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, fileEl, cntEl, + centerWidth, ofs, el; + + thumb.removeAttribute('data-expanding'); + + fileEl = thumb.parentNode.parentNode; + + if (Config.centeredThreads) { + cntEl = fileEl.parentNode.parentNode; + centerWidth = $.cls('opContainer')[0].offsetWidth; + $.addClass(cntEl, 'centre-exp'); + } + + left = thumb.getBoundingClientRect().left; + + maxWidth = $.docEl.clientWidth - left - 25; + maxHeight = $.docEl.clientHeight; + + imgWidth = img.naturalWidth; + imgHeight = img.naturalHeight; + + if (imgWidth > maxWidth) { + ratio = maxWidth / imgWidth; + imgWidth = maxWidth; + imgHeight = imgHeight * ratio; + } + + if (Config.fitToScreenExpansion && imgHeight > maxHeight) { + ratio = maxHeight / imgHeight; + imgHeight = maxHeight; + imgWidth = imgWidth * ratio; + } + + img.style.maxWidth = imgWidth + 'px'; + img.style.maxHeight = imgHeight + 'px'; + + $.addClass(fileEl, 'image-expanded'); + + if (!Main.tid && Config.threadHiding) { + $.addClass(thumb.parentNode, 'image-expanded-anti'); + } + + img.style.display = ''; + thumb.style.display = 'none'; + + if (Config.centeredThreads) { + left = img.getBoundingClientRect().left; + ofs = img.offsetWidth + left * 2; + if (ofs > centerWidth) { + left = Math.floor(($.docEl.clientWidth - ofs) / 2); + + if (left > 0) { + cntEl.style.marginLeft = left + 'px'; + } + } + else { + $.removeClass(cntEl, 'centre-exp'); + } + } + else if (Main.hasMobileLayout) { + cntEl = thumb.parentNode.lastElementChild; + if (!cntEl.firstElementChild) { + fileEl = document.createElement('div'); + fileEl.className = 'mFileName'; + if (el = thumb.parentNode.parentNode.getElementsByClassName('fileText')[0]) { + el = el.firstElementChild; + fileEl.innerHTML = el.getAttribute('title') || el.innerHTML; + } + cntEl.insertBefore(fileEl, cntEl.firstChild); + } + } +}; + +ImageExpansion.checkLoadStart = function(img, thumb) { + if (img.naturalWidth) { + ImageExpansion.onLoadStart(img, thumb); + thumb.style.opacity = ''; + } + else { + return setTimeout(ImageExpansion.checkLoadStart, 15, img, thumb); + } +}; + +/** + * Image hover + */ +var ImageHover = {}; + +ImageHover.show = function(thumb) { + var el, href, ext; + + if (thumb.nodeName !== 'A') { + href = thumb.parentNode.getAttribute('href'); + } + else { + href = thumb.getAttribute('href'); + } + + if (ext = href.match(/\.(?:webm|mp4|pdf)$/)) { + if (ext[0] == '.webm' || ext[0] == '.mp4') { + ImageHover.showWebm(thumb); + } + return; + } + + el = document.createElement('img'); + el.id = 'image-hover'; + el.alt = 'Image'; + el.onerror = ImageHover.onLoadError; + el.src = href; + + if (Config.imageHoverBg) { + el.style.backgroundColor = 'inherit'; + } + + document.body.appendChild(el); + + if (UA.hasCORS) { + el.style.display = 'none'; + this.timeout = ImageHover.checkLoadStart(el, thumb); + } + else { + el.style.left = thumb.getBoundingClientRect().right + 10 + 'px'; + } +}; + +ImageHover.hide = function() { + var img; + + clearTimeout(this.timeout); + + if (img = $.id('image-hover')) { + if (img.play) { + img.pause(); + Tip.hide(); + } + document.body.removeChild(img); + } +}; + +ImageHover.showWebm = function(thumb) { + var el; + + el = document.createElement('video'); + el.id = 'image-hover'; + + if (Config.imageHoverBg) { + el.style.backgroundColor = 'inherit'; + } + + if (thumb.nodeName !== 'A') { + el.src = thumb.parentNode.getAttribute('href'); + } + else { + el.src = thumb.getAttribute('href'); + } + + el.loop = true; + el.muted = !Config.unmuteWebm; + el.autoplay = true; + el.onerror = ImageHover.onLoadError; + el.onloadedmetadata = function() { ImageHover.showWebMDuration(this, thumb); }; + el.onvolumechange = Main.getWebmVolumeChangeCb(); + + document.body.appendChild(el); + + if (Config.unmuteWebm) { + el.volume = Main.getWebmVolume(); + } +}; + +ImageHover.showWebMDuration = function(el, thumb) { + var w, h, aabb; + + if (!el.parentNode) { + return; + } + + var sound, ms = $.prettySeconds(el.duration); + + if (el.mozHasAudio === true + || el.webkitAudioDecodedByteCount > 0 + || (el.audioTracks && el.audioTracks.length)) { + sound = ' (audio)'; + } + else { + sound = ''; + } + + aabb = thumb.getBoundingClientRect(); + + [w, h] = $.fit(el.videoWidth, el.videoHeight, + window.innerWidth - aabb.right - 20, window.innerHeight); + + el.style.width = w + 'px'; + el.style.height = h + 'px'; + + Tip.show(thumb, ms[0] + ':' + ('0' + ms[1]).slice(-2) + sound); +}; + +ImageHover.onLoadError = function() { + Feedback.error('File no longer exists (404).', 2000); +}; + +ImageHover.onLoadStart = function(img, thumb) { + var bounds, limit; + + bounds = thumb.getBoundingClientRect(); + limit = window.innerWidth - bounds.right - 20; + + if (img.naturalWidth > limit) { + img.style.maxWidth = limit + 'px'; + } + + img.style.display = ''; +}; + +ImageHover.checkLoadStart = function(img, thumb) { + if (img.naturalWidth) { + ImageHover.onLoadStart(img, thumb); + } + else { + return setTimeout(ImageHover.checkLoadStart, 15, img, thumb); + } +}; + +/** + * Quick reply + */ +var QR = {}; + +QR.init = function() { + var item; + + if (!UA.hasFormData) { + return; + } + + this.enabled = !!document.forms.post; + this.currentTid = null; + this.cooldown = null; + this.timestamp = null; + this.auto = false; + + this.btn = null; + this.comField = null; + this.comLength = window.comlen; + this.comCheckTimeout = null; + + this.cdElapsed = 0; + this.activeDelay = 0; + + this.ctTTL = 290; + this.ctTimeout = null; + + this.cooldowns = {}; + + for (item in window.cooldowns) { + this.cooldowns[item] = window.cooldowns[item] * 1000; + } + + if (this.noCaptcha) { + for (item in this.cooldowns) { + this.cooldowns[item] = Math.ceil(this.cooldowns[item] / 2); + } + } + + this.painterTime = 0; + this.painterData = null; + this.painterSrc = null; + this.replayBlob = null; + this.canvasLoading = false; + + this.captchaWidgetCnt = null; + this.captchaWidgetId = null; + this.pulse = null; + this.xhr = null; + + this.fileDisabled = !!window.imagelimit; + + this.tracked = {}; + + if (Main.tid && !Main.hasMobileLayout && !Main.threadClosed) { + QR.addReplyLink(); + } + + window.addEventListener('storage', this.syncStorage, false); +}; + +QR.openTeXPreview = function() { + var el; + + QR.closeTeXPreview(); + + if (!window.MathJax) { + window.loadMathJax(); + } + + el = document.createElement('div'); + el.id = 'tex-preview-cnt'; + el.className = 'UIPanel'; + el.setAttribute('data-cmd', 'close-tex-preview'); + el.innerHTML = '\ +
    Preview\ +Close
    Use [math][/math] tags for inline, and [eqn][/eqn] tags for block equations.
    \ +
    '; + + document.body.appendChild(el); + + el = $.id('input-tex-preview'); + $.on(el, 'keyup', QR.onTeXChanged); +}; + +QR.closeTeXPreview = function() { + var el; + + if (el = $.id('input-tex-preview')) { + $.off(el, 'keyup', QR.onTeXChanged); + + el = $.id('tex-preview-cnt'); + el.parentNode.removeChild(el); + } +}; + +QR.onTeXChanged = function() { + clearTimeout(QR.timeoutTeX); + QR.timeoutTeX = setTimeout(QR.processTeX, 50); +}; + +QR.processTeX = function() { + var src, dest; + + if (QR.processingTeX || !window.MathJax || !(src = $.id('input-tex-preview'))) { + return; + } + + dest = $.id('output-tex-preview'); + + dest.textContent = src.value; + + QR.processingTeX = true; + + MathJax.Hub.Queue(['Typeset', MathJax.Hub, dest], ['onTeXReady', QR]); +}; + +QR.onTeXReady = function() { + QR.processingTeX = false; +}; + +QR.validateCT = function() { + var ct, now, d; + + if (!QR.captchaWidgetCnt) { + return; + } + + ct = Main.getCookie('_ct'); + + if (!ct) { + if (QR.ctTimeout) { + QR.setCTTag(); + } + return; + } + + if (QR.ctTimeout) { + return; + } + + ct = ct.split('.')[1]; + now = 0 | (Date.now() / 1000); + + d = now - ct; + + if (d >= QR.ctTTL) { + QR.setCTTag(); + } + else { + QR.setCTTag(QR.ctTTL - d); + } +}; + +QR.setCTTag = function(sec) { + if (window.t_captcha) { + return; + } + + var el = QR.captchaWidgetCnt; + + QR.clearCTTimeout(); + + if (sec && sec > 0) { + QR.ctTimeout = setTimeout(QR.setCTTag, sec * 1000); + el.style.opacity = '0.25'; + $.on(el, 'mouseover', QR.onCTMouseOver); + $.on(el, 'mouseout', QR.onCTMouseOut); + } + else { + el.style.opacity = ''; + $.off(el, 'mouseover', QR.onCTMouseOver); + $.off(el, 'mouseout', QR.onCTMouseOut); + } +}; + +QR.onCTMouseOver = function() { + QR.onCTMouseOut(); + QR.ctTipTimeout = setTimeout(Tip.show, Tip.delay, QR.captchaWidgetCnt, + 'Verification not required for your next post.'); +}; + +QR.onCTMouseOut = function() { + clearTimeout(QR.ctTipTimeout); + Tip.hide(); +}; + +QR.clearCTTimeout = function() { + clearTimeout(QR.ctTimeout); + QR.ctTimeout = null; +}; + +QR.addReplyLink = function() { + var cnt, el; + + cnt = $.cls('navLinks')[2]; + + el = document.createElement('div'); + el.className = 'open-qr-wrap'; + el.innerHTML = '[Post a Reply]'; + + cnt.insertBefore(el, cnt.firstChild); +}; + +QR.lock = function() { + QR.showPostError('This thread is closed.', 'closed', true); +}; + +QR.unlock = function() { + QR.hidePostError('closed'); +}; + +QR.syncStorage = function(e) { + var key; + + if (!e.key) { + return; + } + + key = e.key.split('-'); + + if (key[0] != '4chan') { + return; + } + + if (key[1] == 'cd' && e.newValue && Main.board == key[2]) { + QR.startCooldown(); + } +}; + +QR.onOpenInPainterClick = function(btn) { + var img, el, tid, pid; + + if (QR.canvasLoading) { + Feedback.error('An image is already being loaded.'); + return; + } + + pid = +btn.getAttribute('data-pid'); + tid = +btn.getAttribute('data-tid'); + + if (!pid || !tid) { + return false; + } + + el = $.qs('#f' + pid + ' a[class="fileThumb"]'); + + if (!el) { + return false; + } + + if (/\.(png|jpg)$/.test(el.href) === false) { + return false; + } + + QR.canvasLoading = true; + + img = new Image(); + img.crossOrigin = 'Anonymous'; + img.onload = QR.onPainterCanvasLoaded; + img.onerror = QR.onPainterCanvasError; + img._pid = pid; + + Feedback.notify('Loading…', 0); + + img.src = el.href.replace('is2.4chan.org', 'i.4cdn.org'); + + QR.show(tid); +}; + +QR.onPainterCanvasError = function() { + QR.canvasLoading = false; + Feedback.error("Couldn't load the image.", 5000); +}; + +QR.onPainterCanvasLoaded = function() { + Feedback.hideMessage(); + + QR.canvasLoading = false; + + if (!QR.currentTid) { + return; + } + + if (this.naturalWidth < 1 || this.naturalHeight < 1) { + return; + } + + if (Tegaki.startTimeStamp) { + Tegaki.destroy(); + } + + Keybinds.enabled = false; + + QR.painterSrc = this._pid; + + Tegaki.open({ + onDone: QR.onPainterDone, + onCancel: QR.onPainterCancel, + saveReplay: false, + width: 1, + height: 1 + }); + + Tegaki.onOpenImageLoaded.call(this); +}; + +QR.openPainter = function() { + var w, h, dims, cb; + + dims = $.tag('input', $.id('qr-painter-ctrl')); + + w = +dims[0].value; + h = +dims[1].value; + + if (w < 1 || h < 1) { + return; + } + + cb = $.cls('oe-r-cb', $.id('qr-painter-ctrl'))[0]; + + Keybinds.enabled = false; + + window.Tegaki.open({ + onDone: QR.onPainterDone, + onCancel: QR.onPainterCancel, + saveReplay: cb && cb.checked, + width: w, + height: h + }); +}; + +QR.onPainterDone = function() { + var el, cnt; + + Keybinds.enabled = true; + + QR.painterData = Tegaki.flatten().toDataURL('image/png'); + + if (Tegaki.saveReplay) { + QR.replayBlob = Tegaki.replayRecorder.toBlob(); + } + + QR.painterTime = 0; + + if (Tegaki.startTimeStamp) { + if (!Tegaki.hasCustomCanvas || QR.painterSrc) { + QR.painterTime = Math.round((Date.now() - Tegaki.startTimeStamp) / 1000); + } + } + + if (el = $.id('qrFile')) { + el.style.visibility = 'hidden'; + } + + cnt = $.id('qr-painter-ctrl'); + + if (el = $.tag('button', cnt)[0]) { + el.textContent = 'Edit'; + } + + if (el = $.tag('button', cnt)[1]) { + el.disabled = false; + } + + for (el of $.tag('input', cnt)) { + el.disabled = true; + } +}; + +QR.onPainterCancel = function() { + var el, cnt; + + Keybinds.enabled = true; + + QR.painterData = null; + QR.painterSrc = null; + QR.replayBlob = null; + QR.painterTime = 0; + + if (el = $.id('qrFile')) { + el.style.visibility = ''; + } + + cnt = $.id('qr-painter-ctrl'); + + if (el = $.tag('button', cnt)[0]) { + el.textContent = 'Draw'; + } + + if (el = $.tag('button', cnt)[1]) { + el.disabled = true; + } + + for (el of $.tag('input', cnt)) { + el.disabled = false; + } +}; + +QR.quotePost = function(tid, pid) { + if (!QR.noCooldown + && (Main.threadClosed || (!Main.tid && Main.isThreadClosed(tid)))) { + alert('This thread is closed'); + return; + } + QR.show(tid); + QR.addQuote(pid); +}; + +QR.addQuote = function(pid) { + var q, pos, sel, ta; + + ta = $.tag('textarea', document.forms.qrPost)[0]; + + pos = ta.selectionStart; + + sel = UA.getSelection(); + + if (pid) { + q = '>>' + pid + '\n'; + } + else { + q = ''; + } + + if (sel) { + q += '>' + sel.trim().replace(/[\r\n]+/g, '\n>') + '\n'; + } + + if (ta.value) { + ta.value = ta.value.slice(0, pos) + + q + ta.value.slice(ta.selectionEnd); + } + else { + ta.value = q; + } + if (UA.isOpera) { + pos += q.split('\n').length; + } + + ta.selectionStart = ta.selectionEnd = pos + q.length; + + if (ta.selectionStart == ta.value.length) { + ta.scrollTop = ta.scrollHeight; + } + + ta.focus(); +}; + +QR.show = function(tid) { + var i, j, cnt, postForm, form, qrForm, fields, row, spoiler, file, + el, el2, placeholder, qrError, cookie, mfs; + + window.checkIncognito && window.checkIncognito(); + + if (QR.currentTid) { + if (!Main.tid && QR.currentTid != tid) { + $.id('qrTid').textContent = $.id('qrResto').value = QR.currentTid = tid; + $.byName('com')[1].value = ''; + + QR.startCooldown(); + } + + if (Main.hasMobileLayout) { + $.id('quickReply').style.top = window.pageYOffset + 25 + 'px'; + } + + return; + } + + QR.currentTid = tid; + + postForm = $.id('postForm'); + + cnt = document.createElement('div'); + cnt.id = 'quickReply'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-trackpos', 'QR-position'); + + if (Main.hasMobileLayout) { + cnt.style.top = window.pageYOffset + 28 + 'px'; + } + else if (Config['QR-position']) { + cnt.style.cssText = Config['QR-position']; + } + else { + cnt.style.right = '0px'; + cnt.style.top = '10%'; + } + + cnt.innerHTML = + '
    ' + + (window.math_tags ? '' : '') + + 'Reply to Thread No.' + + tid + 'X
    '; + + form = postForm.parentNode.cloneNode(false); + form.setAttribute('name', 'qrPost'); + + form.innerHTML = + ( + (mfs = $.byName('MAX_FILE_SIZE')[0]) + ? ('') + : '' + ) + + '' + + ''; + + qrForm = document.createElement('div'); + qrForm.id = 'qrForm'; + + this.btn = null; + + fields = postForm.firstElementChild.children; + for (i = 0, j = fields.length - 1; i < j; ++i) { + row = document.createElement('div'); + if (fields[i].id == 'captchaFormPart') { + if (QR.noCaptcha) { + continue; + } + row.id = 'qrCaptchaContainer'; + row.classList.add('t-qr-root'); + QR.captchaWidgetCnt = row; + } + else { + placeholder = fields[i].getAttribute('data-type'); + if (placeholder == 'Password' || placeholder == 'Spoilers') { + continue; + } + else if (placeholder == 'File') { + file = fields[i].children[1].firstChild.cloneNode(false); + file.tabIndex = ''; + file.id = 'qrFile'; + file.size = '19'; + file.addEventListener('change', QR.onFileChange, false); + row.appendChild(file); + + file.title = 'Shift + Click to remove the file'; + } + else if (placeholder == 'Painter') { + row.innerHTML = fields[i].children[1].innerHTML; + row.id = 'qr-painter-ctrl'; + row.className = 'desktop'; + el = row.getElementsByTagName('button'); + el[0].setAttribute('data-cmd', 'qr-painter-draw'); + el[1].setAttribute('data-cmd', 'qr-painter-clear'); + } + else { + row.innerHTML = fields[i].children[1].innerHTML; + if (row.firstChild.type == 'hidden') { + el = row.lastChild.previousSibling; + } + else { + el = row.firstChild; + } + el.tabIndex = ''; + if (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA') { + if (el.name == 'name') { + if (cookie = Main.getCookie('4chan_name')) { + el.value = cookie; + } + } + else if (el.name == 'email') { + el.id = 'qrEmail'; + if (cookie = Main.getCookie('options')) { + el.value = cookie; + } + if (el.nextElementSibling) { + el.parentNode.removeChild(el.nextElementSibling); + } + } + else if (el.name == 'com') { + QR.comField = el; + el.addEventListener('keydown', QR.onKeyDown, false); + el.addEventListener('paste', QR.onKeyDown, false); + el.addEventListener('cut', QR.onKeyDown, false); + if (row.children[1]) { + row.removeChild(el.nextSibling); + } + } + else if (el.name == 'sub') { + continue; + } + if (placeholder !== null) { + el.setAttribute('placeholder', placeholder); + } + } + else if ((el.name == 'flag')) { + if (el2 = $.qs('option[selected]', el)) { + el2.removeAttribute('selected'); + } + if ((cookie = localStorage.getItem('4chan_flag_' + Main.board)) && + (el2 = $.qs('option[value="' + cookie + '"]', el))) { + el2.setAttribute('selected', 'selected'); + } + $.on(el, 'change', window.onBoardFlagChanged); + } + } + } + + qrForm.appendChild(row); + } + + if (!this.btn) { + this.btn = $.qs('input[type="submit"]', postForm).cloneNode(false); + this.btn.tabIndex = ''; + + if (file) { + file.parentNode.appendChild(this.btn); + } + else { + qrForm.appendChild(document.createElement('div')); + qrForm.lastElementChild.appendChild(this.btn); + } + } + + if (el = $.qs('.desktop > label > input[name="spoiler"]', postForm)) { + spoiler = document.createElement('span'); + spoiler.id = 'qrSpoiler'; + spoiler.innerHTML = ''; + file.parentNode.insertBefore(spoiler, file.nextSibling); + } + + form.appendChild(qrForm); + cnt.appendChild(form); + + qrError = document.createElement('div'); + qrError.id = 'qrError'; + cnt.appendChild(qrError); + + cnt.addEventListener('click', QR.onClick, false); + + document.body.appendChild(cnt); + + QR.startCooldown(); + + if (Main.threadClosed) { + QR.lock(); + } + + if (!window.passEnabled) { + QR.renderCaptcha(); + } + + if (!Main.hasMobileLayout) { + Draggable.set($.id('qrHeader')); + } +}; + +QR.renderCaptcha = function() { + if (window.grecaptcha) { + QR.captchaWidgetId = grecaptcha.render(QR.captchaWidgetCnt, { + sitekey: window.recaptchaKey, + theme: (Main.stylesheet === 'tomorrow' || window.dark_captcha) ? 'dark' : 'light' + }); + QR.validateCT(); + return; + } + else if (window.t_captcha) { + TCaptcha.init(QR.captchaWidgetCnt, Main.board, QR.currentTid); + TCaptcha.setErrorCb(QR.showPostError); + } +}; + +QR.resetCaptcha = function() { + if (window.grecaptcha) { + if (QR.captchaWidgetId !== null) { + grecaptcha.reset(QR.captchaWidgetId); + QR.validateCT(); + } + } + else if (window.t_captcha) { + TCaptcha.clearChallenge(); + } +}; + +QR.onPassError = function() { + var el, cnt; + + if (QR.captchaWidgetCnt) { + return; + } + + window.passEnabled = QR.noCaptcha = false; + + el = document.createElement('div'); + el.id = 'qrCaptchaContainer'; + + cnt = $.id('qrForm'); + + cnt.insertBefore(el, cnt.lastElementChild); + + QR.captchaWidgetCnt = el; + + QR.renderCaptcha(); +}; + +QR.onFileChange = function() { + var fsize, maxFilesize; + + if (this.value) { + maxFilesize = window.maxFilesize; + + if (this.files) { + fsize = this.files[0].size; + if (this.files[0].type == 'video/webm' && window.maxWebmFilesize) { + maxFilesize = window.maxWebmFilesize; + } + } + else { + fsize = 0; + } + + if (QR.fileDisabled) { + QR.showPostError('Image limit reached.', 'imagelimit', true); + } + else if (fsize > maxFilesize) { + QR.showPostError('Error: Maximum file size allowed is ' + + Math.floor(maxFilesize / 1048576) + ' MB', 'filesize', true); + } + else { + QR.hidePostError(); + } + } + else { + QR.hidePostError(); + } + + QR.painterData = null; + QR.replayBlob = null; + + QR.startCooldown(); +}; + +QR.onKeyDown = function(e) { + if (e.ctrlKey && e.keyCode == 83) { + var ta, start, end, spoiler; + + e.stopPropagation(); + e.preventDefault(); + + ta = e.target; + start = ta.selectionStart; + end = ta.selectionEnd; + + if (ta.value) { + spoiler = '[spoiler]' + ta.value.slice(start, end) + '[/spoiler]'; + ta.value = ta.value.slice(0, start) + spoiler + ta.value.slice(end); + ta.setSelectionRange(end + 19, end + 19); + } + else { + ta.value = '[spoiler][/spoiler]'; + ta.setSelectionRange(9, 9); + } + } + else if (e.keyCode == 27 && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { + QR.close(); + return; + } + + clearTimeout(QR.comCheckTimeout); + QR.comCheckTimeout = setTimeout(QR.checkCommentField, 500); +}; + +QR.checkCommentField = function() { + var byteLength; + + if (QR.comLength) { + byteLength = encodeURIComponent(QR.comField.value).split(/%..|./).length - 1; + + if (byteLength > QR.comLength) { + QR.showPostError('Error: Comment too long (' + + byteLength + '/' + QR.comLength + ').', 'length', true); + } + else { + QR.hidePostError('length'); + } + } + + if (window.sjis_tags) { + if (/\[sjis\]/.test(QR.comField.value)) { + if (!$.hasClass(QR.comField, 'sjis')) { + $.addClass(QR.comField, 'sjis'); + } + } + else { + if ($.hasClass(QR.comField, 'sjis')) { + $.removeClass(QR.comField, 'sjis'); + } + } + } +}; + +QR.close = function() { + var el, cnt = $.id('quickReply'); + + QR.comField = null; + QR.currentTid = null; + + QR.painterTime = 0; + QR.painterData = null; + QR.painterSrc = null; + QR.replayBlob = null; + QR.canvasLoading = false; + + clearInterval(QR.pulse); + + if (QR.xhr) { + QR.xhr.abort(); + QR.xhr = null; + } + + cnt.removeEventListener('click', QR.onClick, false); + + (el = $.id('qrFile')) && el.removeEventListener('change', QR.startCooldown, false); + (el = $.id('qrEmail')) && el.removeEventListener('change', QR.startCooldown, false); + $.tag('textarea', cnt)[0].removeEventListener('keydown', QR.onKeyDown, false); + + Draggable.unset($.id('qrHeader')); + + if (!QR.noCaptcha) { + QR.setCTTag(); + } + + if (window.t_captcha && TCaptcha.node === QR.captchaWidgetCnt) { + TCaptcha.destroy(); + } + + document.body.removeChild(cnt); +}; + +QR.onClick = function(e) { + var t = e.target; + + if (t.type == 'submit') { + e.preventDefault(); + QR.submit(e.shiftKey); + } + else { + switch (t.id) { + case 'qrFile': + if (e.shiftKey) { + e.preventDefault(); + QR.resetFile(); + } + break; + case 'qrDummyFile': + case 'qrDummyFileButton': + case 'qrDummyFileLabel': + e.preventDefault(); + if (e.shiftKey) { + QR.resetFile(); + } + else { + $.id('qrFile').click(); + } + break; + case 'qrClose': + QR.close(); + break; + } + } +}; + +QR.showPostError = function(msg, type, silent) { + var qrError; + + qrError = $.id('qrError'); + + if (!qrError) { + return; + } + + if (!msg) { + QR.hidePostError(); + return; + } + + qrError.innerHTML = msg; + qrError.style.display = 'block'; + + qrError.setAttribute('data-type', type || ''); + + if (!silent && (document.hidden + || document.mozHidden + || document.webkitHidden + || document.msHidden)) { + alert('Posting Error'); + } +}; + +QR.hidePostError = function(type) { + var el = $.id('qrError'); + + if (!el.hasAttribute('style')) { + return; + } + + if (!type || el.getAttribute('data-type') == type) { + el.removeAttribute('style'); + } +}; + +QR.resetFile = function() { + var file, el; + + QR.painterData = null; + QR.replayBlob = null; + + el = document.createElement('input'); + el.id = 'qrFile'; + el.type = 'file'; + el.size = '19'; + el.name = 'upfile'; + el.addEventListener('change', QR.onFileChange, false); + + file = $.id('qrFile'); + file.removeEventListener('change', QR.onFileChange, false); + + file.parentNode.replaceChild(el, file); + + QR.hidePostError('imagelimit'); + + QR.needPreuploadCaptcha = false; + + QR.startCooldown(); +}; + +QR.submit = function(force) { + var formdata; + + QR.hidePostError(); + + if (!QR.presubmitChecks(force)) { + return; + } + + QR.auto = false; + + QR.xhr = new XMLHttpRequest(); + + QR.xhr.open('POST', document.forms.qrPost.action, true); + + QR.xhr.withCredentials = true; + + QR.xhr.setRequestHeader('Accept', 'application/json'); + + QR.xhr.upload.onprogress = function(e) { + if (e.loaded >= e.total) { + QR.btn.value = '100%'; + } + else { + QR.btn.value = (0 | (e.loaded / e.total * 100)) + '%'; + } + }; + + QR.xhr.onerror = function() { + QR.xhr = null; + QR.showPostError('Connection error.'); + }; + + QR.xhr.onload = function() { + var resp, el, hasFile, ids, tid, pid, tracked; + + QR.xhr = null; + + QR.btn.value = 'Post'; + + if (this.status == 200) { + if (this.getResponseHeader("Content-Type") == 'application/json') { + try { + resp = JSON.parse(this.responseText); + } + catch (e) { + console.log('QR resp', e); + resp = { error: 'Something went wrong.' }; + } + + if (resp.error) { + if (window.passEnabled && /4chan Pass/.test(resp.error)) { + QR.onPassError(); + } + else { + QR.resetCaptcha(); + } + QR.showPostError(resp.error); + return; + } + + if (resp.pid) { + tid = resp.tid; + pid = resp.pid; + } + } + else if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) { + if (window.passEnabled && /4chan Pass/.test(resp)) { + QR.onPassError(); + } + else { + QR.resetCaptcha(); + } + QR.showPostError(resp[1]); + return; + } + else if (ids = this.responseText.match(//)) { + tid = ids[1]; + pid = ids[2]; + } + + if (pid) { + hasFile = (el = $.id('qrFile')) && el.value; + + QR.setPostTime(); + + if (Config.persistentQR) { + $.byName('com')[1].value = ''; + + if (el = $.byName('spoiler')[2]) { + el.checked = false; + } + + QR.resetCaptcha(); + + if (hasFile || QR.painterData) { + QR.resetFile(); + } + + QR.startCooldown(); + } + else { + QR.close(); + } + + if (Main.tid) { + if (Config.threadWatcher) { + ThreadWatcher.setLastRead(pid, tid); + } + QR.lastReplyId = +pid; + Parser.trackedReplies['>>' + pid] = 1; + Parser.saveTrackedReplies(tid, Parser.trackedReplies); + } + else { + tracked = Parser.getTrackedReplies(Main.board, tid) || {}; + tracked['>>' + pid] = 1; + Parser.saveTrackedReplies(tid, tracked); + } + + Parser.touchTrackedReplies(tid); + + UA.dispatchEvent('4chanQRPostSuccess', { threadId: tid, postId: pid }); + } + + if (ThreadUpdater.enabled) { + setTimeout(ThreadUpdater.forceUpdate, 500); + } + } + else { + QR.showPostError('Error: ' + this.status + ' ' + this.statusText); + } + }; + + formdata = new FormData(document.forms.qrPost); + + if (formdata.entries && formdata.delete + && (!formdata.get('upfile') || !formdata.get('upfile').size)) { + formdata.delete('upfile'); + } + + if (QR.painterData) { + QR.appendPainter(formdata); + + if (QR.replayBlob) { + formdata.append('oe_replay', QR.replayBlob, 'tegaki.tgkr'); + } + + formdata.append('oe_time', QR.painterTime); + + if (QR.painterSrc) { + formdata.append('oe_src', QR.painterSrc); + } + } + + clearInterval(QR.pulse); + + QR.btn.value = 'Sending'; + + QR.xhr.send(formdata); +}; + +QR.appendPainter = function(formdata) { + var blob; + + blob = QR.b64toBlob(QR.painterData.slice(QR.painterData.indexOf(',') + 1)); + + if (blob) { + if (blob.size > window.maxFilesize) { + QR.showPostError('Error: Maximum file size allowed is ' + + Math.floor(window.maxFilesize / 1048576) + ' MB', 'filesize', true); + + return; + } + + formdata.append('upfile', blob, 'tegaki.png'); + } +}; + +QR.b64toBlob = function(data) { + var i, bytes, ary, bary, len; + + bytes = atob(data); + len = bytes.length; + + ary = new Array(len); + + for (i = 0; i < len; ++i) { + ary[i] = bytes.charCodeAt(i); + } + + bary = new Uint8Array(ary); + + return new Blob([bary]); +}; + +QR.presubmitChecks = function(force) { + if (QR.xhr) { + QR.xhr.abort(); + QR.xhr = null; + QR.showPostError('Aborted.'); + QR.btn.value = 'Post'; + return false; + } + + if (!force && QR.cooldown) { + if (QR.auto = !QR.auto) { + QR.btn.value = QR.cooldown + 's (auto)'; + } + else { + QR.btn.value = QR.cooldown + 's'; + } + return false; + } + + if (window.isIncognito) { + let el = $.id('qrFile'); + if (el && el.value && !QR.painterData) { + QR.showPostError('Uploading files in incognito mode is not allowed.' + + '
    The File field has been cleared.'); + QR.resetFile(); + return false; + } + } + + return true; +}; + +QR.getCooldown = function(type) { + return QR.cooldowns[type]; +}; + +QR.setPostTime = function() { + return localStorage.setItem('4chan-cd-' + Main.board, Date.now()); +}; + +QR.getPostTime = function() { + return localStorage.getItem('4chan-cd-' + Main.board); +}; + +QR.removePostTime = function() { + return localStorage.removeItem('4chan-cd-' + Main.board); +}; + +QR.startCooldown = function() { + var type, el, time; + + if (QR.noCooldown || !$.id('quickReply') || QR.xhr) { + return; + } + + clearInterval(QR.pulse); + + type = ((el = $.id('qrFile')) && el.value) ? 'image' : 'reply'; + + time = QR.getPostTime(type); + + if (!time) { + QR.btn.value = 'Post'; + return; + } + + QR.timestamp = parseInt(time, 10); + + QR.activeDelay = QR.getCooldown(type); + + QR.cdElapsed = Date.now() - QR.timestamp; + + QR.cooldown = Math.ceil((QR.activeDelay - QR.cdElapsed) / 1000); + + if (QR.cooldown <= 0 || QR.cdElapsed < 0) { + QR.cooldown = false; + QR.btn.value = 'Post'; + return; + } + + QR.btn.value = QR.cooldown + 's'; + + QR.pulse = setInterval(QR.onPulse, 1000); +}; + +QR.onPulse = function() { + QR.cdElapsed = Date.now() - QR.timestamp; + QR.cooldown = Math.ceil((QR.activeDelay - QR.cdElapsed) / 1000); + if (QR.cooldown <= 0) { + clearInterval(QR.pulse); + QR.btn.value = 'Post'; + QR.cooldown = false; + if (QR.auto) { + QR.submit(); + } + } + else { + QR.btn.value = QR.cooldown + (QR.auto ? 's (auto)' : 's'); + } +}; + +/** + * Thread hiding + */ +var ThreadHiding = {}; + +ThreadHiding.init = function() { + this.threshold = 43200000; // 12 hours + + this.hidden = {}; + + this.load(); + + this.purge(); +}; + +ThreadHiding.clear = function(silent) { + var i, id, key, msg; + + this.load(); + + i = 0; + + for (id in this.hidden) { + ++i; + } + + key = '4chan-hide-t-' + Main.board; + + if (!silent) { + if (!i) { + alert("You don't have any hidden threads on /" + Main.board + '/'); + return; + } + + msg = 'This will unhide ' + i + ' thread' + (i > 1 ? 's' : '') + ' on /' + Main.board + '/'; + + if (!confirm(msg)) { + return; + } + + localStorage.removeItem(key); + } + else { + localStorage.removeItem(key); + } +}; + +ThreadHiding.isHidden = function(tid) { + return !!ThreadHiding.hidden[tid]; +}; + +ThreadHiding.toggle = function(tid) { + if (this.isHidden(tid)) { + this.show(tid); + } + else { + this.hide(tid); + } + this.save(); +}; + +ThreadHiding.show = function(tid) { + var sa, th; + + th = $.id('t' + tid); + sa = $.id('sa' + tid); + + if (Main.hasMobileLayout) { + sa.parentNode.removeChild(sa); + th.style.display = null; + $.removeClass(th.nextElementSibling, 'mobile-hr-hidden'); + } + else { + sa.removeAttribute('data-hidden'); + sa.firstChild.src = Main.icons.minus; + $.removeClass(th, 'post-hidden'); + } + + delete this.hidden[tid]; +}; + +ThreadHiding.hide = function(tid) { + var sa, th; + + th = $.id('t' + tid); + + if (Main.hasMobileLayout) { + th.style.display = 'none'; + $.addClass(th.nextElementSibling, 'mobile-hr-hidden'); + + sa = document.createElement('span'); + sa.id = 'sa' + tid; + sa.setAttribute('data-cmd', 'hide'); + sa.setAttribute('data-id', tid); + sa.textContent = 'Show Hidden Thread'; + sa.className = 'mobileHideButton button mobile-tu-show'; + th.parentNode.insertBefore(sa, th); + } + else { + if (Config.hideStubs && !$.cls('stickyIcon', th)[0]) { + th.style.display = th.nextElementSibling.style.display = 'none'; + } + else { + sa = $.id('sa' + tid); + sa.setAttribute('data-hidden', tid); + sa.firstChild.src = Main.icons.plus; + th.className += ' post-hidden'; + } + } + + this.hidden[tid] = Date.now(); +}; + +ThreadHiding.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-hide-t-' + Main.board)) { + this.hidden = JSON.parse(storage); + } +}; + +ThreadHiding.purge = function() { + var i, hasHidden, lastPurged, key; + + key = '4chan-purge-t-' + Main.board; + + lastPurged = localStorage.getItem(key); + + for (i in this.hidden) { + hasHidden = true; + break; + } + + if (!hasHidden) { + return; + } + + if (!lastPurged || lastPurged < Date.now() - this.threshold) { + $.get('//a.4cdn.org/' + Main.board + '/threads.json', + { + onload: function() { + var i, j, t, p, pages, threads, alive; + + if (this.status == 200) { + alive = {}; + pages = JSON.parse(this.responseText); + for (i = 0; p = pages[i]; ++i) { + threads = p.threads; + for (j = 0; t = threads[j]; ++j) { + if (ThreadHiding.hidden[t.no]) { + alive[t.no] = 1; + } + } + } + ThreadHiding.hidden = alive; + ThreadHiding.save(); + localStorage.setItem(key, Date.now()); + } + else { + console.log('Bad status code while purging threads'); + } + }, + onerror: function() { + console.log('Error while purging hidden threads'); + } + }); + } +}; + +ThreadHiding.save = function() { + var i; + + for (i in this.hidden) { + localStorage.setItem('4chan-hide-t-' + Main.board, + JSON.stringify(this.hidden) + ); + return; + } + localStorage.removeItem('4chan-hide-t-' + Main.board); +}; + +/** + * Reply hiding + */ +var ReplyHiding = {}; + +ReplyHiding.init = function() { + this.threshold = 7 * 86400000; + this.hidden = {}; + this.hiddenR = {}; + this.hiddenRMap = {}; + this.hasR = false; + this.load(); +}; + +ReplyHiding.isHidden = function(pid) { + var sa = $.id('sa' + pid); + + return !sa || sa.hasAttribute('data-hidden'); +}; + +ReplyHiding.toggle = function(pid) { + this.load(); + + if (this.isHidden(pid)) { + this.show(pid); + } + else { + this.hide(pid); + } + this.save(); +}; + +ReplyHiding.toggleR = function(pid) { + var i, el, post, nodes, rid, parentPid; + + this.load(); + + if (parentPid = this.hiddenRMap['>>' + pid]) { + this.showR(parentPid, parentPid); + + for (i in this.hiddenRMap) { + if (this.hiddenRMap[i] == parentPid) { + this.showR(i.slice(2)); + } + } + } + else { + this.hideR(pid, pid); + + post = $.id('m' + pid); + nodes = $.cls('postMessage'); + + for (i = 1; nodes[i] !== post; ++i) {} + + for (; el = nodes[i]; ++i) { + if (ReplyHiding.shouldToggleR(el)) { + rid = el.id.slice(1); + this.hideR(rid, pid); + } + } + } + + this.hasR = false; + + for (i in this.hiddenRMap) { + this.hasR = true; + break; + } + + this.save(); +}; + +ReplyHiding.shouldToggleR = function(el) { + var j, ql, hit, quotes; + + if (el.parentNode.hasAttribute('data-pfx')) { + return false; + } + + quotes = $.qsa('#' + el.id + ' > .quotelink', el); + + if (!quotes[0]) { + return false; + } + + hit = this.hiddenRMap[quotes[0].textContent]; + + if (quotes.length === 1 && hit) { + return hit; + } + else { + for (j = 0; ql = quotes[j]; ++j) { + if (!this.hiddenRMap[ql.textContent]) { + return false; + } + } + } + + return hit; +}; + +ReplyHiding.show = function(pid) { + $.removeClass($.id('pc' + pid), 'post-hidden'); + $.id('sa' + pid).removeAttribute('data-hidden'); + + delete ReplyHiding.hidden[pid]; +}; + +ReplyHiding.showR = function(pid, parentPid) { + $.removeClass($.id('pc' + pid), 'post-hidden'); + $.id('sa' + pid).removeAttribute('data-hidden'); + + delete ReplyHiding.hiddenRMap['>>' + pid]; + + if (parentPid) { + delete ReplyHiding.hiddenR[parentPid]; + } +}; + +ReplyHiding.hide = function(pid) { + $.addClass($.id('pc' + pid), 'post-hidden'); + $.id('sa' + pid).setAttribute('data-hidden', pid); + + ReplyHiding.hidden[pid] = Date.now(); +}; + +ReplyHiding.hideR = function(pid, parentPid) { + $.addClass($.id('pc' + pid), 'post-hidden'); + $.id('sa' + pid).setAttribute('data-hidden', pid); + + ReplyHiding.hiddenRMap['>>' + pid] = parentPid; + + if (pid === parentPid) { + ReplyHiding.hiddenR[pid] = Date.now(); + } + + ReplyHiding.hasR = true; +}; + +ReplyHiding.load = function() { + var storage; + + this.hasHiddenR = false; + + if (storage = localStorage.getItem('4chan-hide-r-' + Main.board)) { + this.hidden = JSON.parse(storage); + } + + if (storage = localStorage.getItem('4chan-hide-rr-' + Main.board)) { + this.hiddenR = JSON.parse(storage); + } +}; + +ReplyHiding.purge = function() { + var tid, now; + + now = Date.now(); + + for (tid in this.hidden) { + if (now - this.hidden[tid] > this.threshold) { + delete this.hidden[tid]; + } + } + + for (tid in this.hiddenR) { + if (now - this.hiddenR[tid] > this.threshold) { + delete this.hiddenR[tid]; + } + } + + this.save(); +}; + +ReplyHiding.save = function() { + var i, clr; + + clr = true; + + for (i in this.hidden) { + localStorage.setItem('4chan-hide-r-' + Main.board, + JSON.stringify(this.hidden) + ); + clr = false; + break; + } + + clr && localStorage.removeItem('4chan-hide-r-' + Main.board); + + clr = true; + + for (i in this.hiddenR) { + localStorage.setItem('4chan-hide-rr-' + Main.board, + JSON.stringify(this.hiddenR) + ); + clr = false; + break; + } + + clr && localStorage.removeItem('4chan-hide-rr-' + Main.board); +}; + +/** + * Thread watcher + */ +var ThreadWatcher = {}; + +ThreadWatcher.init = function() { + var cnt, jumpTo, rect, el, awt; + + this.listNode = null; + this.charLimit = 45; + this.watched = {}; + this.blacklisted = {}; + this.isRefreshing = false; + + if (Main.hasMobileLayout) { + el = document.createElement('a'); + el.href = '#'; + el.textContent = 'TW'; + el.addEventListener('click', ThreadWatcher.toggleList, false); + cnt = $.id('settingsWindowLinkMobile'); + cnt.parentNode.insertBefore(el, cnt); + cnt.parentNode.insertBefore(document.createTextNode(' '), cnt); + } + + if (location.hash && (jumpTo = location.hash.split('lr')[1])) { + if (jumpTo = $.id('pc' + jumpTo)) { + if (jumpTo.nextElementSibling) { + jumpTo = jumpTo.nextElementSibling; + if (el = $.id('p' + jumpTo.id.slice(2))) { + $.addClass(el, 'highlight'); + } + } + + rect = jumpTo.getBoundingClientRect(); + + if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) { + window.scrollBy(0, rect.top); + } + } + + if (window.history && history.replaceState) { + history.replaceState(null, '', location.href.split('#', 1)[0]); + } + } + + cnt = document.createElement('div'); + cnt.id = 'threadWatcher'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-trackpos', 'TW-position'); + + if (Main.hasMobileLayout) { + cnt.style.display = 'none'; + } + else { + if (Config['TW-position']) { + cnt.style.cssText = Config['TW-position']; + } + else { + cnt.style.left = '10px'; + cnt.style.top = '380px'; + } + + if (Config.fixedThreadWatcher) { + cnt.style.position = 'fixed'; + } + else { + cnt.style.position = ''; + } + } + + cnt.innerHTML = '
    ' + + (Main.hasMobileLayout ? ('X') : '') + + 'Thread Watcher' + + (UA.hasCORS ? ('R
    ') : '
    '); + + this.listNode = document.createElement('ul'); + this.listNode.id = 'watchList'; + + this.load(); + + if (Config.threadAutoWatcher) { + if (awt = Main.getCookie('4chan_awt')) { + Main.removeCookie('4chan_awt', '.' + $L.d(Main.board), '/' + Main.board + '/'); + + this.add(+awt, Main.board); + this.save(); + } + + if (document.forms.post) { + el = $.el('input'); + el.type = 'hidden'; + el.name = 'awt'; + el.value = '1'; + + document.forms.post.appendChild(el); + } + } + + if (Main.tid) { + this.refreshCurrent(); + } + + this.build(); + + cnt.appendChild(this.listNode); + document.body.appendChild(cnt); + cnt.addEventListener('mouseup', this.onClick, false); + Draggable.set($.id('twHeader')); + window.addEventListener('storage', this.syncStorage, false); + + if (Main.hasMobileLayout) { + if (Main.tid) { + ThreadWatcher.initMobileButtons(); + } + } + else if (!Main.tid && this.canAutoRefresh()) { + this.refresh(); + } +}; + +ThreadWatcher.toggleList = function(e) { + var el = $.id('threadWatcher'); + + e && e.preventDefault(); + + if (!Main.tid && ThreadWatcher.canAutoRefresh()) { + ThreadWatcher.refresh(); + } + + if (el.style.display == 'none') { + el.style.top = (window.pageYOffset + 30) + 'px'; + el.style.display = ''; + } + else { + el.style.display = 'none'; + } +}; + +ThreadWatcher.syncStorage = function(e) { + var key; + + if (!e.key) { + return; + } + + key = e.key.split('-'); + + if (key[0] == '4chan' && key[1] == 'watch' && !key[2] && e.newValue != e.oldValue) { + ThreadWatcher.load(); + ThreadWatcher.build(true); + } +}; + +ThreadWatcher.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-watch')) { + this.watched = JSON.parse(storage); + } + if (storage = localStorage.getItem('4chan-watch-bl')) { + this.blacklisted = JSON.parse(storage); + } +}; + +ThreadWatcher.build = function(rebuildButtons) { + var html, tuid, key, cls; + + html = ''; + + for (key in this.watched) { + tuid = key.split('-'); + html += '
  • × (' + this.watched[key][2] + ') '; + } + else { + html += (cls[0] ? ('class="' + cls.join(' ') + '"') : '') + '>'; + } + } + + html += '/' + tuid[1] + '/ - ' + this.watched[key][0] + '
  • '; + } + + if (rebuildButtons) { + ThreadWatcher.rebuildButtons(); + } + + ThreadWatcher.listNode.innerHTML = html; +}; + +ThreadWatcher.rebuildButtons = function() { + var i, buttons, key, btn; + + buttons = $.cls('wbtn'); + + for (i = 0; btn = buttons[i]; ++i) { + key = btn.getAttribute('data-id') + '-' + Main.board; + if (ThreadWatcher.watched[key]) { + if (!btn.hasAttribute('data-active')) { + btn.src = Main.icons.watched; + btn.setAttribute('data-active', '1'); + } + } + else { + if (btn.hasAttribute('data-active')) { + btn.src = Main.icons.notwatched; + btn.removeAttribute('data-active'); + } + } + } +}; + +ThreadWatcher.initMobileButtons = function() { + var el, cnt, key, ref; + + el = document.createElement('img'); + + key = Main.tid + '-' + Main.board; + + if (ThreadWatcher.watched[key]) { + el.src = Main.icons.watched; + el.setAttribute('data-active', '1'); + } + else { + el.src = Main.icons.notwatched; + } + + el.className = 'extButton wbtn wbtn-' + key; + el.setAttribute('data-cmd', 'watch'); + el.setAttribute('data-id', Main.tid); + el.alt = 'W'; + + cnt = document.createElement('span'); + cnt.className = 'mobileib button'; + + cnt.appendChild(el); + + if (ref = $.cls('navLinks')[0]) { + ref.appendChild(document.createTextNode(' ')); + ref.appendChild(cnt); + } + + if (ref = $.cls('navLinks')[3]) { + ref.appendChild(document.createTextNode(' ')); + ref.appendChild(cnt.cloneNode(true)); + } +}; + +ThreadWatcher.onClick = function(e) { + var t = e.target; + + if (t.hasAttribute('data-id')) { + ThreadWatcher.toggle( + t.getAttribute('data-id'), + t.getAttribute('data-board') + ); + } + else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) { + ThreadWatcher.refreshWithAutoWatch(); + } + else if (t.id == 'twClose') { + ThreadWatcher.toggleList(); + } +}; + +ThreadWatcher.generateLabel = function(sub, com, tid) { + var label; + + if (label = sub) { + label = label.slice(0, this.charLimit); + } + else if (label = com) { + label = label.replace(/(?:
    )+/g, ' ') + .replace(/<[^>]*?>/g, '').slice(0, this.charLimit); + } + else { + label = 'No.' + tid; + } + + return label; +}; + +ThreadWatcher.toggle = function(tid, board) { + var key; + + key = tid + '-' + (board || Main.board); + + if (this.watched[key]) { + this.blacklisted[key] = 1; + delete this.watched[key]; + } + else { + this.add(tid, board, key); + } + + this.save(); + this.load(); + this.build(true); +}; + +ThreadWatcher.add = function(tid, board) { + var key, label, sub, com, lastReply, thread; + + key = tid + '-' + (board || Main.board); + + sub = $.cls('subject', $.id('pi' + tid))[0].textContent; + com = $.id('m' + tid).innerHTML; + + label = this.generateLabel(sub, com, tid); + + if ((thread = $.id('t' + tid)).children[1]) { + lastReply = thread.lastElementChild.id.slice(2); + } + else { + lastReply = tid; + } + + this.watched[key] = [ label, lastReply, 0 ]; +}; + +ThreadWatcher.addRaw = function(post, board) { + var key, label; + + key = post.no + '-' + (board || Main.board); + + if (this.watched[key]) { + return; + } + + label = ThreadWatcher.generateLabel(post.sub, post.com, post.no); + + this.watched[key] = [ label, 0, 0 ]; +}; + +ThreadWatcher.save = function() { + var i; + + ThreadWatcher.sortByBoard(); + + localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched)); + + //StorageSync.sync('4chan-watch'); + + for (i in ThreadWatcher.blacklisted) { + localStorage.setItem('4chan-watch-bl', JSON.stringify(ThreadWatcher.blacklisted)); + //StorageSync.sync('4chan-watch-bl'); + break; + } +}; + +ThreadWatcher.sortByBoard = function() { + var i, self, key, sorted, keys; + + self = ThreadWatcher; + + sorted = {}; + keys = []; + + for (key in self.watched) { + keys.push(key); + } + + keys.sort(function(a, b) { + a = a.split('-')[1]; + b = b.split('-')[1]; + + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }); + + for (i = 0; key = keys[i]; ++i) { + sorted[key] = self.watched[key]; + } + + self.watched = sorted; +}; + +ThreadWatcher.canAutoRefresh = function() { + var time; + + if (time = localStorage.getItem('4chan-tw-timestamp')) { + return Date.now() - (+time) >= 60000; + } + + return true; +}; + +ThreadWatcher.setRefreshTimestamp = function() { + localStorage.setItem('4chan-tw-timestamp', Date.now()); + //StorageSync.sync('4chan-tw-timestamp'); +}; + +ThreadWatcher.refreshWithAutoWatch = function() { + var i, f, count, board, boards, img; + + if (!Config.filter) { + this.refresh(); + return; + } + + Filter.load(); + + boards = {}; + count = 0; + + for (i = 0; f = Filter.activeFilters[i]; ++i) { + if (!f.auto || !f.boards) { + continue; + } + for (board in f.boards) { + if (boards[board]) { + continue; + } + boards[board] = true; + ++count; + } + } + + if (!count) { + this.refresh(); + return; + } + + img = $.id('twPrune'); + img.src = Main.icons.rotate; + this.isRefreshing = true; + + this.fetchCatalogs(boards, count); +}; + +ThreadWatcher.fetchCatalogs = function(boards, count) { + var to, board, catalogs, meta; + + catalogs = {}; + meta = { count: count }; + to = 0; + + for (board in boards) { + setTimeout(ThreadWatcher.fetchCatalog, to, board, catalogs, meta); + to += 200; + } +}; + +ThreadWatcher.fetchCatalog = function(board, catalogs, meta) { + var xhr; + + xhr = new XMLHttpRequest(); + xhr.open('GET', '//a.4cdn.org/' + board + '/catalog.json'); + xhr.onload = function() { + meta.count--; + catalogs[board] = Parser.parseCatalogJSON(this.responseText); + if (!meta.count) { + ThreadWatcher.onCatalogsLoaded(catalogs); + } + }; + xhr.onerror = function() { + meta.count--; + if (!meta.count) { + ThreadWatcher.onCatalogsLoaded(catalogs); + } + }; + xhr.send(null); +}; + +ThreadWatcher.onCatalogsLoaded = function(catalogs) { + var i, j, board, page, pages, threads, thread, key, blacklisted; + + $.id('twPrune').src = Main.icons.refresh; + this.isRefreshing = false; + + blacklisted = {}; + + for (board in catalogs) { + pages = catalogs[board]; + for (i = 0; page = pages[i]; ++i) { + threads = page.threads; + for (j = 0; thread = threads[j]; ++j) { + key = thread.no + '-' + board; + if (this.blacklisted[key]) { + blacklisted[key] = 1; + continue; + } + if (Filter.match(thread, board)) { + this.addRaw(thread, board); + } + } + } + } + + this.blacklisted = blacklisted; + this.build(true); + this.refresh(); +}; + +ThreadWatcher.refresh = function() { + var i, to, key, total, img; + + if (total = $.id('watchList').children.length) { + i = to = 0; + img = $.id('twPrune'); + img.src = Main.icons.rotate; + ThreadWatcher.isRefreshing = true; + ThreadWatcher.setRefreshTimestamp(); + for (key in ThreadWatcher.watched) { + setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null); + to += 200; + } + } +}; + +ThreadWatcher.refreshCurrent = function(rebuild) { + var key, thread, lastReply; + + key = Main.tid + '-' + Main.board; + + if (this.watched[key]) { + if ((thread = $.id('t' + Main.tid)).children[1]) { + lastReply = thread.lastElementChild.id.slice(2); + } + else { + lastReply = Main.tid; + } + if (this.watched[key][1] < lastReply) { + this.watched[key][1] = lastReply; + } + + this.watched[key][2] = 0; + this.watched[key][4] = 0; + this.save(); + + if (rebuild) { + this.build(); + } + } +}; + +ThreadWatcher.setLastRead = function(pid, tid) { + var key = tid + '-' + Main.board; + + if (this.watched[key]) { + this.watched[key][1] = pid; + this.watched[key][2] = 0; + this.watched[key][4] = 0; + this.save(); + this.build(); + } +}; + +ThreadWatcher.onRefreshEnd = function(img) { + img.src = Main.icons.refresh; + this.isRefreshing = false; + this.save(); + this.load(); + this.build(); +}; + +ThreadWatcher.fetch = function(key, img) { + var tuid, xhr, li; + + li = $.id('watch-' + key); + + if (ThreadWatcher.watched[key][1] == -1) { + delete ThreadWatcher.watched[key]; + li.parentNode.removeChild(li); + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + return; + } + + tuid = key.split('-'); // tid, board + + xhr = new XMLHttpRequest(); + xhr.onload = function() { + var i, newReplies, posts, lastReply, trackedReplies, dummy, quotelinks, q, j; + if (this.status == 200) { + posts = Parser.parseThreadJSON(this.responseText); + lastReply = ThreadWatcher.watched[key][1]; + newReplies = 0; + + if (!ThreadWatcher.watched[key][4]) { + trackedReplies = Parser.getTrackedReplies(tuid[1], tuid[0]); + + if (trackedReplies) { + dummy = document.createElement('div'); + } + } + else { + trackedReplies = null; + } + + for (i = posts.length - 1; i >= 1; i--) { + if (posts[i].no <= lastReply) { + break; + } + ++newReplies; + + if (trackedReplies) { + dummy.innerHTML = posts[i].com; + quotelinks = $.cls('quotelink', dummy); + + if (!quotelinks[0]) { + continue; + } + + for (j = 0; q = quotelinks[j]; ++j) { + if (trackedReplies[q.textContent]) { + ThreadWatcher.watched[key][4] = 1; + trackedReplies = null; + break; + } + } + } + } + if (newReplies > ThreadWatcher.watched[key][2]) { + ThreadWatcher.watched[key][2] = newReplies; + } + if (posts[0].archived) { + ThreadWatcher.watched[key][3] = 1; + } + } + else if (this.status == 404) { + ThreadWatcher.watched[key][1] = -1; + } + if (img) { + ThreadWatcher.onRefreshEnd(img); + } + }; + if (img) { + xhr.onerror = xhr.onload; + } + xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json'); + xhr.send(null); +}; + +/** + * Thread expansion + */ +var ThreadExpansion = {}; + +ThreadExpansion.init = function() { + this.enabled = UA.hasCORS; + this.fetchXhr = null; +}; + +ThreadExpansion.expandComment = function(link) { + var ids, tid, pid, abbr; + + if (!(ids = link.getAttribute('href').match(/^(?:thread\/)([0-9]+)#p([0-9]+)$/))) { + return; + } + + tid = ids[1]; + pid = ids[2]; + + abbr = link.parentNode; + abbr.textContent = 'Loading...'; + + $.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json', + { + onload: function() { + var i, msg, post, posts; + + if (this.status == 200) { + msg = $.id('m' + pid); + + posts = Parser.parseThreadJSON(this.responseText); + + if (tid == pid) { + post = posts[0]; + } + else { + for (i = posts.length - 1; i > 0; i--) { + if (posts[i].no == pid) { + post = posts[i]; + break; + } + } + } + + if (post) { + post = Parser.buildHTMLFromJSON(post, Main.board); + + msg.innerHTML = $.cls('postMessage', post)[0].innerHTML; + + if (Parser.prettify) { + Parser.parseMarkup(msg); + } + if (window.math_tags) { + Parser.parseMathOne(msg); + } + } + else { + abbr.textContent = "This post doesn't exist anymore."; + } + } + else if (this.status == 404) { + abbr.textContent = "This thread doesn't exist anymore."; + } + else { + abbr.textContent = 'Connection Error'; + console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText); + } + }, + onerror: function() { + abbr.textContent = 'Connection Error'; + console.log('ThreadExpansion: xhr failed'); + } + } + ); +}; + +ThreadExpansion.toggle = function(tid) { + var thread, msg, expmsg, summary, tmp; + + thread = $.id('t' + tid); + summary = thread.children[1]; + if (thread.hasAttribute('data-truncated')) { + msg = $.id('m' + tid); + expmsg = msg.nextSibling; + } + + if ($.hasClass(thread, 'tExpanded')) { + thread.className = thread.className.replace(' tExpanded', ' tCollapsed'); + summary.children[0].src = Main.icons.plus; + summary.children[1].style.display = 'inline'; + summary.children[2].style.display = 'none'; + if (msg) { + tmp = msg.innerHTML; + msg.innerHTML = expmsg.textContent; + expmsg.textContent = tmp; + } + } + else if ($.hasClass(thread, 'tCollapsed')) { + thread.className = thread.className.replace(' tCollapsed', ' tExpanded'); + summary.children[0].src = Main.icons.minus; + summary.children[1].style.display = 'none'; + summary.children[2].style.display = 'inline'; + if (msg) { + tmp = msg.innerHTML; + msg.innerHTML = expmsg.textContent; + expmsg.textContent = tmp; + } + } + else { + summary.children[0].src = Main.icons.rotate; + if (!ThreadExpansion.fetchXhr) { + ThreadExpansion.fetch(tid); + } + } +}; + +ThreadExpansion.fetch = function(tid) { + ThreadExpansion.fetchXhr = $.get( + '//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json', + { + onload: function() { + var i, p, n, frag, thread, tail, posts, msg, metacap, + expmsg, summary, abbr; + + ThreadExpansion.fetchXhr = null; + + thread = $.id('t' + tid); + summary = thread.children[1]; + + if (this.status == 200) { + tail = $.cls('reply', thread); + + posts = Parser.parseThreadJSON(this.responseText); + + if (!Config.revealSpoilers && posts[0].custom_spoiler) { + Parser.setCustomSpoiler(Main.board, posts[0].custom_spoiler); + } + + frag = document.createDocumentFragment(); + + if (tail[0]) { + tail = +tail[0].id.slice(1); + + for (i = 1; p = posts[i]; ++i) { + if (p.no < tail) { + n = Parser.buildHTMLFromJSON(p, Main.board); + n.className += ' rExpanded'; + frag.appendChild(n); + } + else { + break; + } + } + } + else { + for (i = 1; p = posts[i]; ++i) { + n = Parser.buildHTMLFromJSON(p, Main.board); + n.className += ' rExpanded'; + frag.appendChild(n); + } + } + + msg = $.id('m' + tid); + if ((abbr = $.cls('abbr', msg)[0]) + && /^Comment/.test(abbr.textContent)) { + thread.setAttribute('data-truncated', '1'); + expmsg = document.createElement('div'); + expmsg.style.display = 'none'; + expmsg.textContent = msg.innerHTML; + msg.parentNode.insertBefore(expmsg, msg.nextSibling); + if (metacap = $.cls('capcodeReplies', msg)[0]) { + msg.innerHTML = posts[0].com + '

    '; + msg.appendChild(metacap); + } + else { + msg.innerHTML = posts[0].com; + } + if (Parser.prettify) { + Parser.parseMarkup(msg); + } + if (window.math_tags) { + Parser.parseMathOne(msg); + } + } + + thread.insertBefore(frag, summary.nextSibling); + Parser.parseThread(tid, 1, i - 1); + + thread.className += ' tExpanded'; + summary.children[0].src = Main.icons.minus; + summary.children[1].style.display = 'none'; + summary.children[2].style.display = 'inline'; + } + else if (this.status == 404) { + summary.children[0].src = Main.icons.plus; + summary.children[0].display = 'none'; + summary.children[1].textContent = "This thread doesn't exist anymore."; + } + else { + summary.children[0].src = Main.icons.plus; + console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText); + } + }, + onerror: function() { + ThreadExpansion.fetchXhr = null; + $.id('t' + tid).children[1].children[0].src = Main.icons.plus; + console.log('ThreadExpansion: xhr failed'); + } + } + ); +}; + +/** + * Thread updater + */ +var ThreadUpdater = {}; + +ThreadUpdater.init = function() { + if (!UA.hasCORS || window.thread_archived) { + return; + } + + this.enabled = true; + + this.pageTitle = document.title; + + this.unreadCount = 0; + this.auto = this.hadAuto = false; + + this.delayId = 0; + this.delayIdHidden = 4; + this.delayRange = [ 10, 15, 20, 30, 60, 90, 120, 180, 240, 300 ]; + this.timeLeft = 0; + this.interval = null; + + this.tailSize = window.tailSize || 0; + this.lastUpdated = Date.now(); + this.lastModified = '0'; + this.lastModifiedTail = '0'; + this.lastReply = null; + + this.currentIcon = null; + this.iconPath = '//s.4cdn.org/image/'; + this.iconNode = $.qs('link[rel="shortcut icon"]', document.head); + this.iconNode.type = 'image/x-icon'; + this.defaultIcon = this.iconNode.getAttribute('href').replace(this.iconPath, ''); + + this.deletionQueue = {}; + + if (Config.updaterSound) { + this.audioEnabled = false; + this.audio = document.createElement('audio'); + this.audio.src = '//s.4cdn.org/media/beep.ogg'; + } + + this.hidden = 'hidden'; + this.visibilitychange = 'visibilitychange'; + + this.adsTTL = 900000; + this.adsReloadTs = 0; + + if (typeof document.hidden === 'undefined') { + if ('mozHidden' in document) { + this.hidden = 'mozHidden'; + this.visibilitychange = 'mozvisibilitychange'; + } + else if ('webkitHidden' in document) { + this.hidden = 'webkitHidden'; + this.visibilitychange = 'webkitvisibilitychange'; + } + else if ('msHidden' in document) { + this.hidden = 'msHidden'; + this.visibilitychange = 'msvisibilitychange'; + } + } + + this.initControls(); + + document.addEventListener('scroll', this.onScroll, false); + + if (Config.alwaysAutoUpdate || sessionStorage.getItem('4chan-auto-' + Main.tid)) { + this.start(); + } +}; + +ThreadUpdater.apiUrlFilter = null; + +ThreadUpdater.buildMobileControl = function(el, bottom) { + var wrap, cnt, ctrl, cb, label, oldBtn, btn; + + bottom = (bottom ? 'Bot' : ''); + + wrap = document.createElement('div'); + wrap.className = 'btn-row'; + + // Update button + oldBtn = el.parentNode; + + btn = oldBtn.cloneNode(true); + btn.innerHTML = ''; + + wrap.appendChild(btn); + cnt = el.parentNode.parentNode; + ctrl = document.createElement('span'); + ctrl.className = 'mobileib button'; + + // Auto checkbox + label = document.createElement('label'); + cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.setAttribute('data-cmd', 'auto'); + this['autoNode' + bottom] = cb; + label.appendChild(cb); + label.appendChild(document.createTextNode('Auto')); + ctrl.appendChild(label); + wrap.appendChild(document.createTextNode(' ')); + wrap.appendChild(ctrl); + + // Status label + label = document.createElement('div'); + label.className = 'mobile-tu-status'; + + wrap.appendChild(this['statusNode' + bottom] = label); + + cnt.appendChild(wrap); + + // Remove Update button + oldBtn.parentNode.removeChild(oldBtn); + + if (cnt = $.id('mpostform')) { + cnt.parentNode.style.marginTop = ''; + } +}; + +ThreadUpdater.buildDesktopControl = function(bottom) { + var frag, el, label, navlinks; + + bottom = (bottom ? 'Bot' : ''); + + frag = document.createDocumentFragment(); + + // Update button + frag.appendChild(document.createTextNode(' [')); + el = document.createElement('a'); + el.href = ''; + el.textContent = 'Update'; + el.setAttribute('data-cmd', 'update'); + frag.appendChild(el); + frag.appendChild(document.createTextNode(']')); + + // Auto checkbox + frag.appendChild(document.createTextNode(' [')); + label = document.createElement('label'); + el = document.createElement('input'); + el.type = 'checkbox'; + el.title = 'Fetch new replies automatically'; + el.setAttribute('data-cmd', 'auto'); + this['autoNode' + bottom] = el; + label.appendChild(el); + label.appendChild(document.createTextNode('Auto')); + frag.appendChild(label); + frag.appendChild(document.createTextNode('] ')); + + if (Config.updaterSound) { + // Sound checkbox + frag.appendChild(document.createTextNode(' [')); + label = document.createElement('label'); + el = document.createElement('input'); + el.type = 'checkbox'; + el.title = 'Play a sound on new replies to your posts'; + el.setAttribute('data-cmd', 'sound'); + this['soundNode' + bottom] = el; + label.appendChild(el); + label.appendChild(document.createTextNode('Sound')); + frag.appendChild(label); + frag.appendChild(document.createTextNode('] ')); + } + + // Status label + frag.appendChild( + this['statusNode' + bottom] = document.createElement('span') + ); + + if (bottom) { + navlinks = $.cls('navLinks' + bottom)[0]; + } + else { + navlinks = $.cls('navLinks')[1]; + } + + if (navlinks) { + navlinks.appendChild(frag); + } +}; + +ThreadUpdater.initControls = function() { + // Mobile + if (Main.hasMobileLayout) { + this.buildMobileControl($.id('refresh_top')); + this.buildMobileControl($.id('refresh_bottom'), true); + } + // Desktop + else { + this.buildDesktopControl(); + this.buildDesktopControl(true); + } +}; + +ThreadUpdater.start = function() { + if (this.dead) { + return; + } + this.auto = this.hadAuto = true; + this.autoNode.checked = this.autoNodeBot.checked = true; + this.force = this.updating = false; + if (this.hidden) { + document.addEventListener(this.visibilitychange, + this.onVisibilityChange, false); + } + this.delayId = 0; + this.timeLeft = this.delayRange[0]; + this.pulse(); + sessionStorage.setItem('4chan-auto-' + Main.tid, 1); +}; + +ThreadUpdater.stop = function(manual) { + clearTimeout(this.interval); + this.auto = this.updating = this.force = false; + this.autoNode.checked = this.autoNodeBot.checked = false; + if (this.hidden) { + document.removeEventListener(this.visibilitychange, + this.onVisibilityChange, false); + } + if (manual) { + this.setStatus(''); + this.setIcon(null); + } + sessionStorage.removeItem('4chan-auto-' + Main.tid); +}; + +ThreadUpdater.pulse = function() { + var self = ThreadUpdater; + + if (self.timeLeft === 0) { + self.update(); + } + else { + self.setStatus(self.timeLeft--); + self.interval = setTimeout(self.pulse, 1000); + } +}; + +ThreadUpdater.adjustDelay = function(postCount) +{ + if (postCount === 0) { + if (!this.force) { + if (this.delayId < this.delayRange.length - 1) { + ++this.delayId; + } + } + } + else { + this.delayId = document[this.hidden] ? this.delayIdHidden : 0; + } + this.timeLeft = this.delayRange[this.delayId]; + if (this.auto) { + this.pulse(); + } +}; + +ThreadUpdater.onVisibilityChange = function() { + var self = ThreadUpdater; + + if (document[self.hidden] && self.delayId < self.delayIdHidden) { + self.delayId = self.delayIdHidden; + } + else { + self.delayId = 0; + } + + self.timeLeft = self.delayRange[0]; + clearTimeout(self.interval); + self.pulse(); +}; + +ThreadUpdater.onScroll = function() { + if (ThreadUpdater.hadAuto && + (document.documentElement.scrollHeight + <= (Math.ceil(window.innerHeight + window.pageYOffset)) + && !document[ThreadUpdater.hidden])) { + ThreadUpdater.clearUnread(); + } +}; + +ThreadUpdater.clearUnread = function() { + if (!this.dead) { + this.setIcon(null); + } + if (this.lastReply) { + this.unreadCount = 0; + document.title = this.pageTitle; + $.removeClass(this.lastReply, 'newPostsMarker'); + this.lastReply = null; + } +}; + +ThreadUpdater.forceUpdate = function() { + ThreadUpdater.force = true; + ThreadUpdater.update(); +}; + +ThreadUpdater.toggleAuto = function() { + if (this.updating) { + return; + } + this.auto ? this.stop(true) : this.start(); +}; + +ThreadUpdater.toggleSound = function() { + this.soundNode.checked = this.soundNodeBot.checked = + this.audioEnabled = !this.audioEnabled; +}; + +ThreadUpdater.update = function(full) { + var self, isTail, url; + + self = ThreadUpdater; + + if (self.updating || self.dead) { + return; + } + + clearTimeout(self.interval); + + self.updating = true; + + self.setStatus('Updating...'); + + isTail = !full && self.checkTailUpdate(); + + url = '//a.4cdn.org/' + Main.board + '/thread/' + Main.tid + + (isTail ? '-tail' : '') + '.json'; + + if (self.apiUrlFilter) { + url = self.apiUrlFilter(url); + } + + $.get(url, + { + onload: self.onload, + onerror: self.onerror, + istail: isTail + }, + { + 'If-Modified-Since': isTail ? self.lastModifiedTail : self.lastModified + } + ); +}; + +ThreadUpdater.checkTailUpdate = function() { + var self, nodes, el, dtr, dtp; + + self = ThreadUpdater; + + if (!self.tailSize) { + return false; + } + + nodes = $.cls('replyContainer'); + + el = nodes[nodes.length - self.tailSize]; + + if (!el) { + return true; + } + + el = $.cls('dateTime', el)[0]; + + dtp = (0 | (self.lastUpdated / 1000)) - (+el.getAttribute('data-utc')); + dtr = 0 | ((Date.now() - self.lastUpdated) / 1000); + + return dtp > dtr; +}; + +ThreadUpdater.checkTailValid = function(posts) { + var op = posts[0]; + + if (!op || !op.tail_id) { + ThreadUpdater.tailSize = 0; + return false; + } + + return !!$.id('p' + op.tail_id); +}; + +ThreadUpdater.reloadAds = function() { + if (!window.Danbo || !window.reloadAdsDanbo) { + return; + } + + let self = ThreadUpdater; + + let now = Date.now(); + + if (!self.adsReloadTs) { + self.adsReloadTs = now; + return; + } + + if (now - self.adsReloadTs < self.adsTTL) { + return; + } + + self.adsReloadTs = now; + + window.reloadAdsDanbo(); +}; + +ThreadUpdater.onload = function() { + var i, state, self, nodes, thread, newposts, frag, lastrep, lastid, + op, doc, autoscroll, count, fromQR, lastRepPos; + + self = ThreadUpdater; + nodes = []; + + self.setStatus(''); + + if (this.status == 200) { + newposts = Parser.parseThreadJSON(this.responseText); + + if (this.istail) { + if (!self.checkTailValid(newposts)) { + self.updating = false; + self.update(true); + return; + } + self.lastModifiedTail = this.getResponseHeader('Last-Modified'); + } + else { + if (newposts[0].tail_size !== self.tailSize) { + self.tailSize = newposts[0].tail_size || 0; + } + self.lastModified = this.getResponseHeader('Last-Modified'); + } + + thread = $.id('t' + Main.tid); + + lastrep = thread.children[thread.childElementCount - 1]; + lastid = +lastrep.id.slice(2); + + state = !!newposts[0].archived; + if (window.thread_archived !== undefined && state != window.thread_archived) { + QR.enabled && $.id('quickReply') && QR.lock(); + Main.setThreadState('archived', state); + } + + state = !!newposts[0].closed; + if (state != Main.threadClosed) { + if (newposts[0].archived) { + state = false; + } + else if (QR.enabled && $.id('quickReply')) { + if (state) { + QR.lock(); + } + else { + QR.unlock(); + } + } + Main.setThreadState('closed', state); + } + + state = !!newposts[0].sticky; + if (state != Main.threadSticky) { + Main.setThreadState('sticky', state); + } + + state = !!newposts[0].imagelimit; + if (QR.enabled && state != QR.fileDisabled) { + QR.fileDisabled = state; + } + + if (!Config.revealSpoilers && newposts[0].custom_spoiler) { + Parser.setCustomSpoiler(Main.board, newposts[0].custom_spoiler); + } + + for (i = newposts.length - 1; i >= 0; i--) { + if (newposts[i].no <= lastid) { + break; + } + nodes.push(newposts[i]); + } + + count = nodes.length; + + if (count == 1 && QR.lastReplyId == nodes[0].no) { + fromQR = true; + QR.lastReplyId = null; + } + + if (count) { + doc = document.documentElement; + + autoscroll = ( + Config.autoScroll + && document[self.hidden] + && doc.scrollHeight == Math.ceil(window.innerHeight + window.pageYOffset) + ); + + if (window.chrome && document.activeElement) { + if (document.activeElement.href || document.activeElement.type === 'checkbox') { + document.activeElement.blur(); + } + } + + frag = document.createDocumentFragment(); + for (i = nodes.length - 1; i >= 0; i--) { + frag.appendChild(Parser.buildHTMLFromJSON(nodes[i], Main.board)); + } + thread.appendChild(frag); + + lastRepPos = lastrep.offsetTop; + + Parser.hasYouMarkers = false; + Parser.hasHighlightedPosts = false; + Parser.parseThread(thread.id.slice(1), -nodes.length); + + if (lastRepPos != lastrep.offsetTop) { + window.scrollBy(0, lastrep.offsetTop - lastRepPos); + } + + if (!fromQR) { + if (!self.force && doc.scrollHeight > window.innerHeight) { + if (!self.lastReply && lastid != Main.tid) { + (self.lastReply = lastrep.lastChild).className += ' newPostsMarker'; + } + if (Parser.hasYouMarkers) { + self.setIcon('rep'); + if (self.audioEnabled && document[self.hidden]) { + self.audio.play(); + } + } + else if (Parser.hasHighlightedPosts && self.currentIcon !== 'rep') { + self.setIcon('hl'); + } + else if (self.unreadCount === 0) { + self.setIcon('new'); + } + self.unreadCount += count; + document.title = '(' + self.unreadCount + ') ' + self.pageTitle; + } + else { + self.setStatus(count + ' new post' + (count > 1 ? 's' : '')); + } + } + + if (autoscroll) { + window.scrollTo(0, document.documentElement.scrollHeight); + } + + if (Config.threadWatcher) { + ThreadWatcher.refreshCurrent(true); + } + + if (Config.threadStats) { + op = newposts[0]; + ThreadStats.update(op.replies, op.images, op.unique_ips, op.bumplimit, op.imagelimit); + } + + if (self.force) { + self.reloadAds(); + } + + UA.dispatchEvent('4chanThreadUpdated', { count: count }); + } + else { + self.setStatus('No new posts'); + } + + if (newposts[0].archived) { + self.setError('This thread is archived'); + if (!self.dead) { + self.setIcon('dead'); + window.thread_archived = true; + self.dead = true; + self.stop(); + } + } + } + else if (this.status === 304 || this.status === 0) { + self.setStatus('No new posts'); + } + else if (this.status === 404) { + if (this.istail) { + self.updating = false; + self.update(true); + return; + } + self.setIcon('dead'); + self.setError('This thread has been pruned or deleted'); + self.dead = true; + self.stop(); + return; + } + + self.lastUpdated = Date.now(); + self.adjustDelay(nodes.length); + self.updating = self.force = false; +}; + +ThreadUpdater.onerror = function() { + var self = ThreadUpdater; + + if (UA.isOpera && !this.statusText && this.status === 0) { + self.setStatus('No new posts'); + } + else { + self.setError('Connection Error'); + } + + self.lastUpdated = Date.now(); + self.adjustDelay(0); + self.updating = self.force = false; +}; + +ThreadUpdater.setStatus = function(msg) { + this.statusNode.textContent = this.statusNodeBot.textContent = msg; +}; + +ThreadUpdater.setError = function(msg) { + this.statusNode.innerHTML + = this.statusNodeBot.innerHTML + = '' + msg + ''; +}; + +ThreadUpdater.setIcon = function(type) { + var icon; + + if (type === null) { + icon = this.defaultIcon; + } + else { + icon = this.icons[Main.type + type]; + } + + this.currentIcon = type; + this.iconNode.href = this.iconPath + icon; + document.head.appendChild(this.iconNode); +}; + +ThreadUpdater.icons = { + wsnew: 'favicon-ws-newposts.ico', + nwsnew: 'favicon-nws-newposts.ico', + wsrep: 'favicon-ws-newreplies.ico', + nwsrep: 'favicon-nws-newreplies.ico', + wsdead: 'favicon-ws-deadthread.ico', + nwsdead: 'favicon-nws-deadthread.ico', + wshl: 'favicon-ws-newfilters.ico', + nwshl: 'favicon-nws-newfilters.ico' +}; + +/** + * Thread stats + */ +var ThreadStats = {}; + +ThreadStats.init = function() { + var cnt; + + this.nodeTop = document.createElement('div'); + this.nodeTop.className = 'thread-stats'; + + if (!Main.hasMobileLayout) { + this.nodeBot = this.nodeTop.cloneNode(false); + + cnt = $.cls('navLinks'); + cnt[1] && cnt[1].appendChild(this.nodeTop); + cnt[2] && cnt[2].appendChild(this.nodeBot); + } + else { + this.nodeBot = {}; + + cnt = $.cls('navLinks'); + if (cnt[0]) { + cnt = cnt[cnt.length - 1].nextElementSibling; + cnt.parentNode.insertBefore(this.nodeTop, cnt); + } + } + + this.pageNumber = null; + this.update(null, null, null, window.bumplimit, window.imagelimit); + + if (!window.thread_archived) { + this.updatePageNumber(); + this.pageInterval = setInterval(this.updatePageNumber, 3 * 60000); + } +}; + +ThreadStats.update = function(replies, images, ips, isBumpFull, isImageFull) { + var stats; + + if (replies === null) { + replies = $.cls('replyContainer').length; + images = $.cls('fileText').length - ($.id('fT' + Main.tid) ? 1 : 0); + } + + stats = []; + + if (Main.threadSticky) { + stats.push('Sticky'); + } + + if (window.thread_archived) { + stats.push('Archived'); + } + else if (Main.threadClosed) { + stats.push('Closed'); + } + + if (isBumpFull) { + stats.push('' + replies + ''); + } + else { + stats.push('' + replies + ''); + } + + if (isImageFull) { + stats.push('' + images + ''); + } + else { + stats.push('' + images + ''); + } + + if (!window.thread_archived) { + if (window.unique_ips) { + stats.push('' + (ips || window.unique_ips) + ''); + } + stats.push('' + (this.pageNumber || '?') + ''); + } + + this.nodeTop.innerHTML = this.nodeBot.innerHTML + = stats.join(' / '); +}; + +ThreadStats.updatePageNumber = function() { + $.get('//a.4cdn.org/' + Main.board + '/threads.json', + { + onload: ThreadStats.onCatalogLoad, + onerror: ThreadStats.onCatalogError + } + ); +}; + +ThreadStats.onCatalogLoad = function() { + var self, i, j, k, n, page, post, threads, catalog, tid, nodes; + + self = ThreadStats; + + if (this.status == 200) { + tid = +Main.tid; + catalog = JSON.parse(this.responseText); + for (i = 0; page = catalog[i]; ++i) { + threads = page.threads; + for (j = 0; post = threads[j]; ++j) { + if (post.no == tid) { + nodes = $.cls('ts-page'); + for (k = 0; n = nodes[k]; ++k) { + n.textContent = page.page; + } + self.pageNumber = page.page; + return; + } + } + } + clearInterval(self.pageInterval); + } + else { + ThreadStats.onCatalogError(); + } +}; + +ThreadStats.onCatalogError = function() { + console.log('ThreadStats: couldn\'t get the catalog (' + this.status + ')'); +}; + +/** + * Filter + */ +var Filter = {}; + +Filter.init = function() { + this.entities = document.createElement('div'); + Filter.load(); +}; + +Filter.onClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'filters-add': + Filter.add(); + break; + case 'filters-save': + Filter.save(); + Filter.close(); + break; + case 'filters-close': + Filter.close(); + break; + case 'filters-palette': + Filter.openPalette(e.target); + break; + case 'filters-palette-close': + Filter.closePalette(); + break; + case 'filters-palette-clear': + Filter.clearPalette(); + break; + case 'filters-up': + Filter.moveUp(e.target.parentNode.parentNode); + break; + case 'filters-del': + Filter.remove(e.target.parentNode.parentNode); + break; + case 'filters-help-open': + Filter.openHelp(); + break; + case 'filters-help-close': + Filter.closeHelp(); + break; + } + } +}; + +Filter.onPaletteClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'palette-pick': + Filter.pickColor(e.target); + break; + case 'palette-clear': + Filter.pickColor(e.target, true); + break; + case 'palette-close': + Filter.closePalette(); + break; + } + } +}; + +Filter.match = function(post, board) { + var i, com, f, filters, hit; + + hit = false; + filters = Filter.activeFilters; + + for (i = 0; f = filters[i]; ++i) { + // boards + if (!f.boards[board]) { + continue; + } + // tripcode + if (f.type === 0) { + if (f.pattern === post.trip) { + hit = true; + break; + } + } + // name + else if (f.type === 1) { + if (f.pattern === post.name) { + hit = true; + break; + } + } + // comment + else if (f.type === 2 && post.com) { + if (com === undefined) { + this.entities.innerHTML + = post.com.replace(/
    /g, '\n').replace(/[<[^>]+>/g, ''); + com = this.entities.textContent; + } + if (f.pattern.test(com)) { + hit = true; + break; + } + } + // user id + else if (f.type === 4) { + if (f.pattern === post.id) { + hit = true; + break; + } + } + // subject + else if (f.type === 5) { + if (f.pattern.test(post.sub)) { + hit = true; + break; + } + } + // filename + else if (f.type === 6) { + if (f.pattern.test(post.filename)) { + hit = true; + break; + } + } + } + + return hit; +}; + +Filter.exec = function(cnt, pi, msg, tid) { + var i, el, trip, name, com, uid, sub, fname, f, filters, hit, currentBoard; + + if (Parser.trackedReplies && Parser.trackedReplies['>>' + pi.id.slice(2)]) { + return false; + } + + currentBoard = Main.board; + filters = Filter.activeFilters; + hit = false; + + for (i = 0; f = filters[i]; ++i) { + // boards + if (f.boards && !f.boards[currentBoard]) { + continue; + } + // tripcode + if (f.type === 0) { + if ((trip !== undefined || (trip = pi.getElementsByClassName('postertrip')[0]) + ) && f.pattern == trip.textContent) { + hit = true; + break; + } + } + // name + else if (f.type === 1) { + if ((name || (name = pi.getElementsByClassName('name')[0])) + && f.pattern == name.textContent) { + hit = true; + break; + } + } + // comment + else if (f.type === 2) { + if (com === undefined) { + this.entities.innerHTML + = msg.innerHTML.replace(/
    /g, '\n').replace(/[<[^>]+>/g, ''); + com = this.entities.textContent; + } + if (f.pattern.test(com)) { + hit = true; + break; + } + } + // user id + else if (f.type === 4) { + if ((uid || + ((uid = pi.getElementsByClassName('posteruid')[0]) + && (uid = uid.firstElementChild.textContent) + ) + ) && f.pattern == uid) { + hit = true; + break; + } + } + // subject + else if (!Main.tid && f.type === 5) { + if ((sub || + ((sub = pi.getElementsByClassName('subject')[0]) + && (sub = sub.textContent) + ) + ) && f.pattern.test(sub)) { + hit = true; + break; + } + } + // filename + else if (f.type === 6) { + if (fname === undefined) { + if ((fname = pi.parentNode.getElementsByClassName('fileText')[0])) { + fname = fname.firstElementChild.textContent; + } + else { + fname = ''; + } + } + if (f.pattern.test(fname)) { + hit = true; + break; + } + } + } + + if (hit) { + if (f.hide) { + if (tid && Config.hideStubs && !$.cls('stickyIcon', cnt)[0]) { + cnt.style.display = cnt.nextElementSibling.style.display = 'none'; + } + else { + cnt.className += ' post-hidden'; + el = document.createElement('span'); + if (!tid) { + el.textContent = '[View]'; + el.setAttribute('data-filtered', '1'); + el.setAttribute('data-cmd', 'unfilter'); + } + else { + el.innerHTML = '[View]'; + } + el.className = 'filter-preview'; + pi.appendChild(el); + } + return true; + } + else { + cnt.className += ' filter-hl'; + cnt.style.boxShadow = '-3px 0 ' + f.color; + Parser.hasHighlightedPosts = true; + } + } + return false; +}; + +Filter.unfilter = function(t) { + var cnt = t.parentNode.parentNode; + + QuotePreview.remove(); + $.removeClass(cnt, 'post-hidden'); + t.parentNode.removeChild(t); +}; + +Filter.load = function() { + var i, j, f, rawFilters, rawPattern, fid, regexEscape, regexType, + wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard, + tmp, boards, pattern, match; + + this.activeFilters = []; + + if (!(rawFilters = localStorage.getItem('4chan-filters'))) { + return; + } + + rawFilters = JSON.parse(rawFilters); + + regexEscape = new RegExp('(\\' + + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\') + + ')', 'g'); + regexType = /^\/(.*)\/(i?)$/; + wordSepS = '(?=.*\\b'; + wordSepE = '\\b)'; + regexWildcard = /\\\*/g; + replaceWildcard = '[^\\s]*'; + + try { + for (fid = 0; f = rawFilters[fid]; ++fid) { + if (f.active && f.pattern !== '') { + // Boards + if (f.boards) { + tmp = f.boards.split(/[^a-z0-9]+/i); + boards = {}; + for (i = 0; j = tmp[i]; ++i) { + boards[j] = true; + } + } + else { + boards = false; + } + + rawPattern = f.pattern; + // Name, Tripcode or ID, string comparison + if (!f.type || f.type == 1 || f.type == 4) { + pattern = rawPattern; + } + // /RegExp/ + else if (match = rawPattern.match(regexType)) { + pattern = new RegExp(match[1], match[2]); + } + // "Exact match" + else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') { + pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1')); + } + // Full words, AND operator + else { + words = rawPattern.split(' '); + pattern = ''; + for (i = 0, j = words.length; i < j; ++i) { + inner = words[i] + .replace(regexEscape, '\\$1') + .replace(regexWildcard, replaceWildcard); + pattern += wordSepS + inner + wordSepE; + } + pattern = new RegExp('^' + pattern, 'im'); + } + //console.log('Resulting pattern: ' + pattern); + this.activeFilters.push({ + type: f.type, + pattern: pattern, + boards: boards, + color: f.color, + hide: f.hide, + auto: f.auto + }); + } + } + } + catch (e) { + alert('There was an error processing one of the filters: ' + + e + ' in: ' + rawPattern); + } +}; + +Filter.addSelection = function() { + var text, type, node, sel = UA.getSelection(true); + + if (Filter.open() === false) { + return; + } + + if (typeof sel == 'string') { + text = sel.trim(); + } + else { + node = sel.anchorNode.parentNode; + text = sel.toString().trim(); + + if ($.hasClass(node, 'name')) { + type = 1; + } + else if ($.hasClass(node, 'postertrip')) { + type = 0; + } + else if ($.hasClass(node, 'subject')) { + type = 5; + } + else if ($.hasClass(node, 'posteruid') || $.hasClass(node, 'hand')) { + type = 4; + } + else if ($.hasClass(node, 'fileText')) { + type = 6; + } + else { + type = 2; + } + } + + Filter.add(text, type); +}; + +Filter.openHelp = function() { + var cnt; + + if ($.id('filtersHelp')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'filtersHelp'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'filters-help-close'); + cnt.innerHTML = '\ +
    Filters & Highlights Help\ +Close
    \ +

    Tripcode, Name and ID filters:

    \ +
    • Those use simple string comparison.
    • \ +
    • Type them exactly as they appear on 4chan, including the exclamation mark for tripcode filters.
    • \ +
    • Example: !Ep8pui8Vw2
    \ +

    Comment, Subject and E-mail filters:

    \ +
    • Matching whole words:
    • \ +
    • feel — will match "feel" but not "feeling". This search is case-insensitive.
    \ +
    • AND operator:
    • \ +
    • feel girlfriend — will match "feel" AND "girlfriend" in any order.
    \ +
    • Exact match:
    • \ +
    • "that feel when" — place double quotes around the pattern to search for an exact string
    \ +
    • Wildcards:
    • \ +
    • feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc…
    • \ +
    • idolm*ster — this can match "idolmaster" or "idolm@ster", etc…
    \ +
    • Regular expressions:
    • \ +
    • /feel when no (girl|boy)friend/i
    • \ +
    • /^(?!.*touhou).*$/i — NOT operator.
    • \ +
    • /^>/ — comments starting with a quote.
    • \ +
    • /^$/ — comments with no text.
    \ +

    Colors:

    \ +
    • The color field can accept any valid CSS color:
    • \ +
    • red, #0f0, #00ff00, rgba( 34, 12, 64, 0.3), etc…
    \ +

    Boards:

    \ +
    • A space separated list of boards on which the filter will be active. Leave blank to apply to all boards.
    \ +

    Auto-watching:

    \ +
    • Enabling the "Auto" option will automatically add matched threads to the Thread Watcher when it is manually refreshed. This only works when the "Boards" field is not empty, and searches catalog JSON for the selected boards(s).
    \ +

    Shortcut:

    \ +
    • If you have Keyboard shortcuts enabled, pressing F will add the selected text to your filters.
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); +}; + +Filter.closeHelp = function() { + var cnt; + + if (cnt = $.id('filtersHelp')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Filter.open = function() { + var i, f, cnt, rawFilters, filterList; + + if ($.id('filtersMenu')) { + return false; + } + + cnt = document.createElement('div'); + cnt.id = 'filtersMenu'; + cnt.className = 'UIPanel'; + cnt.style.display = 'none'; + cnt.setAttribute('data-cmd', 'filters-close'); + cnt.innerHTML = '\ +
    Filters & Highlights\ +HelpClose
    \ +\ +\ +\ +\ +\ +\ +\ +\ +\ +\ +
    OnPatternBoardsTypeColorAutoHideDel
    \ +\ +\ +
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); + + filterList = $.id('filter-list'); + + if (rawFilters = localStorage.getItem('4chan-filters')) { + rawFilters = JSON.parse(rawFilters); + for (i = 0; f = rawFilters[i]; ++i) { + filterList.appendChild(this.buildEntry(f, i)); + } + } + + cnt.style.display = ''; +}; + +Filter.close = function() { + var cnt; + + if (cnt = $.id('filtersMenu')) { + this.closePalette(); + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Filter.moveUp = function(el) { + var prev; + + if (prev = el.previousElementSibling) { + el.parentNode.insertBefore(el, prev); + } +}; + +Filter.add = function(pattern, type, boards) { + var filter, id, el; + + filter = { + active: true, + type: type || 0, + pattern: pattern || '', + boards: boards || '', + color: '', + auto: false, + hide: false + }; + + id = this.getNextFilterId(); + el = this.buildEntry(filter, id); + + $.id('filter-list').appendChild(el); + $.cls('fPattern', el)[0].focus(); +}; + +Filter.remove = function(tr) { + $.id('filter-list').removeChild(tr); +}; + +Filter.save = function() { + var i, rawFilters, entries, tr, f, color, type; + + rawFilters = []; + entries = $.id('filter-list').children; + + for (i = 0; tr = entries[i]; ++i) { + type = tr.children[4].firstChild; + + f = { + active: tr.children[1].firstChild.checked, + pattern: tr.children[2].firstChild.value, + boards: tr.children[3].firstChild.value, + type: +type.options[type.selectedIndex].value, + auto: tr.children[6].firstChild.checked, + hide: tr.children[7].firstChild.checked + }; + + color = tr.children[5].firstChild; + + if (!color.hasAttribute('data-nocolor')) { + f.color = color.style.backgroundColor; + } + + rawFilters.push(f); + } + + if (rawFilters[0]) { + localStorage.setItem('4chan-filters', JSON.stringify(rawFilters)); + } + else { + localStorage.removeItem('4chan-filters'); + } +}; + +Filter.getNextFilterId = function() { + var i, j, max, entries = $.id('filter-list').children; + + if (!entries.length) { + return 0; + } + else { + max = 0; + for (i = 0; j = entries[i]; ++i) { + j = +j.id.slice(7); + if (j > max) { + max = j; + } + } + return max + 1; + } +}; + +Filter.buildEntry = function(filter, id) { + var tr, html, sel; + + tr = document.createElement('tr'); + tr.id = 'filter-' + id; + + html = ''; + + // Move up + html += ''; + + // On + html += '' : '>'); + + // Pattern + html += ''; + + // Boards + html += ''; + + // FIXME + if (filter.type === 3) { + filter.type = 4; + } + + // Type + sel = [ '', '', '', '', '', '', '' ]; + sel[filter.type] = ' selected="selected"'; + + html += ''; + + // Color + html += ''; + } + html += ''; + + // Auto + html += '' : '>'); + + // Hide + html += '' : '>'); + + // Del + html += '×'; + + tr.innerHTML = html; + + return tr; +}; + +Filter.buildPalette = function(id) { + var i, j, cnt, html, colors, rowCount, colCount; + + colors = [ + ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00'], + ['#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB'], + ['#00A550', '#007FFF', '#AF0A0F', '#B5BD68'] + ]; + + rowCount = colors.length; + colCount = colors[0].length; + + html = '
    '; + + for (i = 0; i < rowCount; ++i) { + html += ''; + for (j = 0; j < colCount; ++j) { + html += ''; + } + html += ''; + } + + html += '
    Custom\ +
    \ +
    \ +[Close]\ +[Clear]
    '; + + cnt = document.createElement('div'); + cnt.id = 'filter-palette'; + cnt.setAttribute('data-target', id); + cnt.setAttribute('data-cmd', 'palette-close'); + cnt.className = 'UIMenu'; + cnt.innerHTML = html; + + return cnt; +}; + +Filter.openPalette = function(target) { + var el, pos, id, picker; + + Filter.closePalette(); + + pos = target.getBoundingClientRect(); + id = target.parentNode.parentNode.id.slice(7); + + el = Filter.buildPalette(id); + document.body.appendChild(el); + + $.id('filter-palette').addEventListener('click', Filter.onPaletteClick, false); + $.id('palette-custom-input').addEventListener('keyup', Filter.setCustomColor, false); + + picker = el.firstElementChild; + picker.style.cssText = 'top:' + pos.top + 'px;left:' + + (pos.left - picker.clientWidth - 10) + 'px;'; +}; + +Filter.closePalette = function() { + var el; + + if (el = $.id('filter-palette')) { + $.id('filter-palette').removeEventListener('click', Filter.onPaletteClick, false); + $.id('palette-custom-input').removeEventListener('keyup', Filter.setCustomColor, false); + el.parentNode.removeChild(el); + } +}; + +Filter.pickColor = function(el, clear) { + var id, target; + + id = $.id('filter-palette').getAttribute('data-target'); + target = $.id('filter-' + id); + + if (!target) { + return; + } + + target = $.cls('colorbox', target)[0]; + + if (clear === true) { + target.setAttribute('data-nocolor', '1'); + target.innerHTML = '∕'; + target.style.background = ''; + } + else { + target.removeAttribute('data-nocolor'); + target.innerHTML = ''; + target.style.background = el.style.backgroundColor; + } + + Filter.closePalette(); +}; + +Filter.setCustomColor = function() { + var input, box; + + input = $.id('palette-custom-input'); + box = $.id('palette-custom-ok'); + + box.style.backgroundColor = input.value; +}; + +/** + * ID colors + */ +var IDColor = { + css: 'padding: 0 5px; border-radius: 6px; font-size: 0.8em;', + ids: {} +}; + +IDColor.init = function() { + var style; + + if (window.user_ids) { + this.enabled = true; + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = '.posteruid .hand {' + this.css + '}'; + document.head.appendChild(style); + } +}; + +IDColor.compute = function(str) { + var rgb, hash; + + rgb = []; + hash = $.hash(str); + + rgb[0] = (hash >> 24) & 0xFF; + rgb[1] = (hash >> 16) & 0xFF; + rgb[2] = (hash >> 8) & 0xFF; + rgb[3] = ((rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114)) > 125; + + this.ids[str] = rgb; + + return rgb; +}; + +IDColor.apply = function(uid) { + var rgb; + + rgb = IDColor.ids[uid.textContent] || IDColor.compute(uid.textContent); + uid.style.cssText = '\ + background-color: rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ');\ + color: ' + (rgb[3] ? 'black;' : 'white;'); +}; + +IDColor.applyRemote = function(uid) { + this.apply(uid); + uid.style.cssText += this.css; +}; + +/** + * SWF embed + */ +var SWFEmbed = {}; + +SWFEmbed.init = function() { + if (Main.tid) { + this.processThread(); + } + else if (!Main.hasMobileLayout) { + this.processIndex(); + } +}; + +SWFEmbed.processThread = function() { + var fileText, el; + + fileText = $.id('fT' + Main.tid); + + if (!fileText) { + return; + } + + el = document.createElement('a'); + el.href = 'javascript:;'; + el.textContent = 'Embed'; + el.addEventListener('click', SWFEmbed.toggleThread, false); + + fileText.appendChild(document.createTextNode('-[')); + fileText.appendChild(el); + fileText.appendChild(document.createTextNode(']')); +}; + +SWFEmbed.processIndex = function() { + var i, tr, el, cnt, nodes, srcIndex, src; + + srcIndex = 2; + + cnt = $.cls('postblock')[0]; + + if (!cnt) { + return; + } + + tr = cnt.parentNode; + + el = document.createElement('td'); + el.className = 'postblock'; + tr.insertBefore(el, tr.children[srcIndex].nextElementSibling); + + cnt = $.cls('flashListing')[0]; + + if (!cnt) { + return; + } + + nodes = $.tag('tr', cnt); + + for (i = 1; tr = nodes[i]; ++i) { + src = tr.children[srcIndex].firstElementChild; + el = document.createElement('td'); + el.innerHTML = '[Embed]'; + el.firstElementChild.addEventListener('click', SWFEmbed.embedIndex, false); + tr.insertBefore(el, tr.children[srcIndex].nextElementSibling); + } +}; + +SWFEmbed.toggleThread = function(e) { + var cnt, link, el, post, maxWidth, ratio, width, height; + + if (cnt = $.id('swf-embed')) { + cnt.parentNode.removeChild(cnt); + e.target.textContent = 'Embed'; + return; + } + + link = $.tag('a', e.target.parentNode)[0]; + + maxWidth = document.documentElement.clientWidth - (!Main.hasMobileLayout ? 100 : 30); + + width = +link.getAttribute('data-width'); + height = +link.getAttribute('data-height'); + + if (width > maxWidth) { + ratio = width / height; + width = maxWidth; + height = Math.round(maxWidth / ratio); + } + + cnt = document.createElement('div'); + cnt.id = 'swf-embed'; + + el = SWFEmbed.getFrameNode(link.href, width, height); + + cnt.appendChild(el); + + post = $.id('m' + Main.tid); + post.insertBefore(cnt, post.firstChild); + + $.cls('thread')[0].scrollIntoView(true); + + e.target.textContent = 'Remove'; +}; + +SWFEmbed.embedIndex = function(e) { + var el, cnt, header, icon, backdrop, width, height, cntWidth, cntHeight, + maxWidth, maxHeight, docWidth, docHeight, margins, headerHeight, fileName, + ratio; + + e.preventDefault(); + + margins = 10; + headerHeight = 20; + + el = e.target.parentNode.parentNode.children[2].firstElementChild; + + fileName = el.getAttribute('title') || el.textContent; + + cntWidth = width = +el.getAttribute('data-width'); + cntHeight = height = +el.getAttribute('data-height'); + + docWidth = document.documentElement.clientWidth; + docHeight = document.documentElement.clientHeight; + + maxWidth = docWidth - margins; + maxHeight = docHeight - margins - headerHeight; + + ratio = width / height; + + if (cntWidth > maxWidth) { + cntWidth = maxWidth; + cntHeight = Math.round(maxWidth / ratio); + } + + if (cntHeight > maxHeight) { + cntHeight = maxHeight; + cntWidth = Math.round(maxHeight * ratio); + } + + el = SWFEmbed.getFrameNode(e.target.href, cntWidth, cntHeight); + + cnt = document.createElement('div'); + cnt.style.position = 'fixed'; + cnt.style.width = cntWidth + 'px'; + cnt.style.height = cntHeight + 'px'; + cnt.style.top = '50%'; + cnt.style.left = '50%'; + cnt.style.marginTop = (-cntHeight / 2 - headerHeight / 2) + 'px'; + cnt.style.marginLeft = (-cntWidth / 2) + 'px'; + cnt.style.background = 'white'; + + header = document.createElement('div'); + header.id = 'swf-embed-header'; + header.className = 'postblock'; + header.textContent = fileName + ', ' + width + 'x' + height; + + icon = document.createElement('img'); + icon.id = 'swf-embed-close'; + icon.className = 'pointer'; + icon.src = Main.icons.cross; + + header.appendChild(icon); + + cnt.appendChild(header); + cnt.appendChild(el); + + backdrop = document.createElement('div'); + backdrop.id = 'swf-embed'; + backdrop.style.cssText = 'width: 100%; height: 100%; position: fixed;\ + top: 0; left: 0; background: rgba(128, 128, 128, 0.5)'; + + backdrop.appendChild(cnt); + backdrop.addEventListener('click', SWFEmbed.onBackdropClick, false); + + document.body.appendChild(backdrop); +}; + +SWFEmbed.getFrameNode = function(file_url, width, height) { + var el, filename; + + filename = file_url.replace(/^https:\/\/i\.4cdn\.org\/f\//, ''); + + el = document.createElement('iframe'); + + el.setAttribute('allow', 'autoplay; fullscreen'); + el.setAttribute('sandbox', 'allow-scripts allow-same-origin'); + el.setAttribute('scrolling', 'no'); + el.setAttribute('frameborder', '0'); + el.setAttribute('width', +width); + el.setAttribute('height', +height); + + el.src = `//s.4cdn.org/media/flash/embed.html?4#${+width},${+height},${filename},1`; + + return el; +}; + +SWFEmbed.onBackdropClick = function(e) { + var backdrop = $.id('swf-embed'); + + if (e.target === backdrop || e.target.id == 'swf-embed-close') { + backdrop.removeEventListener('click', SWFEmbed.onBackdropClick, false); + backdrop.parentNode.removeChild(backdrop); + } +}; + +/** + * Linkify + */ +var Linkify = { + init: function() { + this.probeRe = /(?:^|[^\B"])https?:\/\/[-.a-z0-9]+\.[a-z]{2,4}/; + this.linkRe = /(^|[^\B"])(https?:\/\/[-.a-z0-9\u200b]+\.[a-z\u200b]{2,15}(?:\/[^\s<>]*)?)/ig; + this.punct = /[:!?,.'"]+$/g; + this.derefer = '//sys.' + $L.d(Main.board) + '/derefer?url='; + }, + + exec: function(el) { + if (!this.probeRe.test(el.innerHTML)) { + return; + } + + el.innerHTML = el.innerHTML + .replace(//g, '\u200b') + .replace(this.linkRe, this.funk) + .replace(/\u200b/g, ''); + }, + + funk: function(match, pfx, url, o, str) { + var m, mm, end, len, sfx; + + m = o + match.length; + + if (str.slice(m, m + 4) === '') { + return match; + } + + end = len = url.length; + + if (m = url.match(Linkify.punct)) { + end -= m[0].length; + } + + if (m = url.match(/\)+$/g)) { + mm = m[0].length; + if (m = url.match(/\(/g)) { + mm = mm - m.length; + if (mm > 0) { + end -= mm; + } + } + else { + end -= mm; + } + } + + if (end < len) { + sfx = url.slice(end); + url = url.slice(0, end); + } + else { + sfx = ''; + } + + return pfx + '' + + url + '' + sfx; + } +}; + +/** + * Media + */ +var Media = {}; + +Media.init = function() { + this.matchSC = /(?:soundcloud\.com|snd\.sc)\/[^\s<]+(?:)?[^\s<]*/g; + this.matchYT = /(?:youtube\.com\/watch\?[^\s]*?v=|youtu\.be\/)[^\s<]+(?:)?[^\s<]*(?:)?[^\s<]*(?:)?[^\s<]*/g; + this.toggleYT = /(?:v=|\.be\/)([a-zA-Z0-9_-]{11})/; + this.timeYT = /[\?#&]t=([ms0-9]+)/; + + this.map = { + yt: this.toggleYouTube, + sc: this.toggleSoundCloud, + }; +}; + +Media.parseSoundCloud = function(msg) { + msg.innerHTML = msg.innerHTML.replace(this.matchSC, this.replaceSoundCloud); +}; + +Media.replaceSoundCloud = function(link, o, str) { + var pfx; + + if (Config.linkify) { + if (str[o + link.length - 1] === '"') { + return link; + } + else { + pfx = link + ''; + } + } + else { + pfx = '' + link + ''; + } + + return pfx + ' [Embed]'; +}; + +Media.toggleSoundCloud = function(node) { + var xhr, url; + + if (node.textContent == 'Remove') { + node.parentNode.removeChild(node.nextElementSibling); + node.textContent = 'Embed'; + } + else if (node.textContent == 'Embed') { + url = node.previousElementSibling.textContent; + + xhr = new XMLHttpRequest(); + xhr.open('GET', '//soundcloud.com/oembed?show_artwork=false&' + + 'maxwidth=500px&show_comments=false&format=json&url=' + + 'http://' + url.replace(/^https?:\/\//i, '')); + xhr.onload = function() { + var el; + + if (this.status == 200 || this.status == 304) { + el = document.createElement('div'); + el.className = 'media-embed'; + el.innerHTML = JSON.parse(this.responseText).html; + node.parentNode.insertBefore(el, node.nextElementSibling); + node.textContent = 'Remove'; + } + else { + node.textContent = 'Error'; + console.log('SoundCloud Error (HTTP ' + this.status + ')'); + } + }; + node.textContent = 'Loading...'; + xhr.send(null); + } +}; + +Media.parseYouTube = function(msg) { + msg.innerHTML = msg.innerHTML.replace(this.matchYT, this.replaceYouTube); +}; + +Media.replaceYouTube = function(link, o, str) { + var pfx; + + if (Config.linkify) { + if (str[o + link.length - 1] === '"') { + return link; + } + else { + pfx = link + ''; + } + } + else { + pfx = '' + link + ''; + } + + return pfx + ' [' + + (!Main.hasMobileLayout ? 'Embed' : 'Open') + ']'; +}; + +Media.showYTPreview = function(link) { + var cnt, img, vid, aabb, x, y, tw, th, pad; + + tw = 320; th = 180; pad = 5; + + aabb = link.getBoundingClientRect(); + + vid = link.previousElementSibling.textContent.match(this.toggleYT)[1]; + + if (aabb.right + tw + pad > $.docEl.clientWidth) { + x = aabb.left - tw - pad; + } + else { + x = aabb.right + pad; + } + + y = aabb.top - th / 2 + aabb.height / 2; + + img = document.createElement('img'); + img.width = tw; + img.height = th; + img.alt = ''; + img.src = '//i1.ytimg.com/vi/' + encodeURIComponent(vid) + '/mqdefault.jpg'; + + cnt = document.createElement('div'); + cnt.id = 'yt-preview'; + cnt.className = 'reply'; + cnt.style.left = (x + window.pageXOffset) + 'px'; + cnt.style.top = (y + window.pageYOffset) + 'px'; + + cnt.appendChild(img); + + document.body.appendChild(cnt); +}; + +Media.removeYTPreview = function() { + var el; + + if (el = $.id('yt-preview')) { + document.body.removeChild(el); + } +}; + +Media.toggleYouTube = function(node) { + var vid, time, el, url; + + if (node.textContent == 'Remove') { + node.parentNode.removeChild(node.nextElementSibling); + node.textContent = 'Embed'; + } + else { + url = node.previousElementSibling.textContent; + vid = url.match(this.toggleYT); + time = url.match(this.timeYT); + + if (vid && (vid = vid[1])) { + vid = encodeURIComponent(vid); + + if (time && (time = time[1])) { + vid += '?start=' + encodeURIComponent(time); + } + + if (Main.hasMobileLayout) { + window.open('//www.youtube.com/watch?v=' + vid); + return; + } + + el = document.createElement('div'); + el.className = 'media-embed'; + el.innerHTML = ''; + + node.parentNode.insertBefore(el, node.nextElementSibling); + + node.textContent = 'Remove'; + } + else { + node.textContent = 'Error'; + } + } +}; + +Media.toggleEmbed = function(node) { + var fn, type = node.getAttribute('data-type'); + + if (type && (fn = Media.map[type])) { + fn.call(this, node); + } +}; + +/** + * Sticky nav auto-hide + */ +var StickyNav = { + thres: 5, + pos: 0, + timeout: null, + el: null, + + init: function() { + this.el = Config.classicNav ? $.id('boardNavDesktop') : $.id('boardNavMobile'); + $.addClass(this.el, 'autohide-nav'); + window.addEventListener('scroll', this.onScroll, false); + }, + + onScroll: function() { + clearTimeout(StickyNav.timeout); + StickyNav.timeout = setTimeout(StickyNav.checkScroll, 50); + }, + + checkScroll: function() { + var thisPos; + + thisPos = window.pageYOffset; + + if (Math.abs(StickyNav.pos - thisPos) <= StickyNav.thres) { + return; + } + + if (thisPos < StickyNav.pos) { + StickyNav.el.style.top = ''; + } + else { + StickyNav.el.style.top = '-' + StickyNav.el.offsetHeight + 'px'; + } + + StickyNav.pos = thisPos; + } +}; + +/** + * Custom CSS + */ +var CustomCSS = {}; + +CustomCSS.init = function() { + var style, css; + if (css = localStorage.getItem('4chan-css')) { + style = document.createElement('style'); + style.id = 'customCSS'; + style.setAttribute('type', 'text/css'); + style.textContent = css; + document.head.appendChild(style); + } +}; + +CustomCSS.open = function() { + var cnt, ta, data; + + if ($.id('customCSSMenu')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'customCSSMenu'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'css-close'); + cnt.innerHTML = '\ +
    Custom CSS\ +Close
    \ +\ +
    \ +
    '; + + document.body.appendChild(cnt); + + cnt.addEventListener('click', this.onClick, false); + + ta = $.id('customCSSBox'); + + if (data = localStorage.getItem('4chan-css')) { + ta.textContent = data; + } + + ta.focus(); +}; + +CustomCSS.save = function() { + var ta, style; + + if (ta = $.id('customCSSBox')) { + localStorage.setItem('4chan-css', ta.value); + if (Config.customCSS && (style = $.id('customCSS'))) { + document.head.removeChild(style); + CustomCSS.init(); + } + } +}; + +CustomCSS.close = function() { + var cnt; + + if (cnt = $.id('customCSSMenu')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +CustomCSS.onClick = function(e) { + var cmd; + + if (cmd = e.target.getAttribute('data-cmd')) { + switch (cmd) { + case 'css-close': + CustomCSS.close(); + break; + case 'css-save': + CustomCSS.save(); + CustomCSS.close(); + break; + } + } +}; + +/** + * Keyboard shortcuts + */ +var Keybinds = { + enabled: false +}; + +Keybinds.init = function() { + this.enabled = true; + + this.map = { + // A + 65: function() { + if (ThreadUpdater.enabled) ThreadUpdater.toggleAuto(); + }, + // F + 70: function() { + if (Config.filter) { + Filter.addSelection(); + } + }, + // Q + 81: function() { + if (QR.enabled && Main.tid) { + QR.quotePost(Main.tid); + } + }, + // R + 82: function() { + if (ThreadUpdater.enabled) ThreadUpdater.forceUpdate(); + }, + // W + 87: function() { + if (Config.threadWatcher && Main.tid) ThreadWatcher.toggle(Main.tid); + }, + // B + 66: function() { + var el; + (el = $.cls('prev')[0]) && (el = $.tag('form', el)[0]) && el.submit(); + }, + // C + 67: function() { + location.href = '/' + Main.board + '/catalog'; + }, + // N + 78: function() { + var el; + (el = $.cls('next')[0]) && (el = $.tag('form', el)[0]) && el.submit(); + }, + // I + 73: function() { + location.href = '/' + Main.board + '/'; + } + }; + + document.addEventListener('keydown', this.resolve, false); +}; + +Keybinds.resolve = function(e) { + var bind, el = e.target; + + if (!Keybinds.enabled || el.nodeName == 'TEXTAREA' || el.nodeName == 'INPUT') { + return; + } + + bind = Keybinds.map[e.keyCode]; + + if (bind && !e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + bind(); + } +}; + +Keybinds.open = function() { + var cnt; + + if ($.id('keybindsHelp')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'keybindsHelp'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'keybinds-close'); + cnt.innerHTML = '\ +
    Keyboard Shortcuts\ +Close
    \ +
      \ +
    • Global
    • \ +
    • A — Toggle auto-updater
    • \ +
    • Q — Open Quick Reply
    • \ +
    • R — Update thread
    • \ +
    • W — Watch/Unwatch thread
    • \ +
    • B — Previous page
    • \ +
    • N — Next page
    • \ +
    • I — Return to index
    • \ +
    • C — Open catalog
    • \ +
    • F — Filter selected text
    • \ +
      \ +
    • Quick Reply (always enabled)
    • \ +
    • Ctrl + Click the post number — Quote without linking
    • \ +
    • Ctrl + S — Spoiler tags
    • \ +
    • Esc — Close the Quick Reply
    • \ +
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); +}; + +Keybinds.close = function() { + var cnt; + + if (cnt = $.id('keybindsHelp')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +Keybinds.onClick = function(e) { + var cmd; + + if ((cmd = e.target.getAttribute('data-cmd')) && cmd == 'keybinds-close') { + Keybinds.close(); + } +}; + +var Del = { + deletePost: function(pid, file_only) { + var params; + + if (!confirm('Delete ' + (file_only ? 'file' : 'post') + '?')) { + return; + } + + params = { + mode: window.thread_archived ? 'arcdel' : 'usrdel' + }; + + params[pid] = 'delete'; + + if (file_only) { + params['onlyimgdel'] = 'on'; + } + + $.xhr('POST', 'https://sys.' + $L.d(Main.board) + '/' + Main.board + '/imgboard.php', + { + onload: Del.onPostDeleted, + onerror: Del.onError, + withCredentials: true, + pid: pid, + file_only: file_only + }, + params + ); + }, + + onPostDeleted: function() { + var el; + + if (!this.file_only) { + el = $.id('pc' + this.pid); + } + else if (el = $.id('f' + this.pid)) { + el = $.tag('img', el)[0]; + if (!el.hasAttribute('data-md5')) { + return; + } + } + + if (!el) { + return; + } + + if (/Updating index|Can't find the post/.test(this.responseText)) { + if (!$.hasClass(el, 'deleted')) { + $.addClass(el, 'deleted'); + } + } + else { + Del.onError(); + } + }, + + onError: function() { + Feedback.error('Something went wrong.'); + } +}; + +/** + * Reporting + */ +var Report = { + init: function() { + window.addEventListener('message', Report.onMessage, false); + } +}; + +Report.onMessage = function(e) { + var id; + + if (e.origin === ('https://sys.' + $L.d(Main.board)) && /^done-report/.test(e.data)) { + id = e.data.split('-')[2]; + + if (Config.threadHiding && $.id('t' + id)) { + if (!ThreadHiding.isHidden(id)) { + ThreadHiding.hide(id); + ThreadHiding.save(); + } + + return; + } + + if ($.id('p' + id)) { + if (!ReplyHiding.isHidden(id)) { + ReplyHiding.hide(id); + ReplyHiding.save(); + } + + return; + } + } +}; + +Report.open = function(pid, board) { + var height, altc; + + if (QR.noCaptcha) { + height = 205; + altc = ''; + } + else { + height = 510; + altc = ''; + } + + window.open('https://sys.' + $L.d(Main.board) + '/' + + (board || Main.board) + '/imgboard.php?mode=report&no=' + pid + altc, Date.now(), + "toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height=" + height); +}; + +/** + * Custom Menu + */ +var CustomMenu = {}; + +CustomMenu.initCtrl = function() { + var el, cnt; + + el = document.createElement('span'); + el.className = 'custom-menu-ctrl'; + el.innerHTML = '[Edit]'; + + if (Config.dropDownNav && !Config.classicNav && !Main.hasMobileLayout) { + cnt = $.id('boardSelectMobile').parentNode; + cnt.insertBefore(el, cnt.lastChild); + } + else { + cnt = $.cls('boardList'); + cnt[0] && cnt[0].appendChild(el); + cnt[1] && cnt[1].appendChild(el.cloneNode(true)); + } +}; +/* +CustomMenu.showNWSBoards = function() { + var i, el, nodes, len; + + nodes = $.cls('nwsb'); + len = nodes.length; + + for (i = len - 1; el = nodes[i]; i--) { + $.removeClass(el, 'nwsb'); + } +}; +*/ +CustomMenu.reset = function() { + var i, el, full, custom, navs; + + full = $.cls('boardList'); + custom = $.cls('customBoardList'); + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.removeEventListener('click', CustomMenu.reset, false); + } + + for (i = custom.length - 1; el = custom[i]; i--) { + full[i].style.display = null; + el.parentNode.removeChild(el); + } +}; + +CustomMenu.apply = function(str) { + var i, el, cntBottom, board, navs, boardList, cnt; + + if (!str) { + if (Config.dropDownNav && !Config.classicNav && !Main.hasMobileLayout) { + if (el = $.cls('customBoardList')[0]) { + el.parentNode.removeChild(el); + } + } + return; + } + + boardList = str.split(/[^0-9a-z]/i); + + cnt = document.createElement('span'); + cnt.className = 'customBoardList'; + + for (i = 0; board = boardList[i]; ++i) { + if (i) { + cnt.appendChild(document.createTextNode(' / ')); + } + else { + cnt.appendChild(document.createTextNode('[')); + } + el = document.createElement('a'); + el.textContent = board; + el.href = '//boards.' + $L.d(board) + '/' + board + '/'; + cnt.appendChild(el); + } + + cnt.appendChild(document.createTextNode(']')); + + if (Config.dropDownNav && !Config.classicNav && !Main.hasMobileLayout) { + if (el = $.cls('customBoardList')[0]) { + el.parentNode.removeChild(el); + } + navs = $.id('boardSelectMobile'); + navs && navs.parentNode.insertBefore(cnt, navs.nextSibling); + } + else { + cnt.appendChild(document.createTextNode(' [')); + el = document.createElement('a'); + el.textContent = '…'; + el.title = 'Show all'; + el.className = 'show-all-boards pointer'; + cnt.appendChild(el); + cnt.appendChild(document.createTextNode('] ')); + + cntBottom = cnt.cloneNode(true); + + navs = $.cls('boardList'); + + for (i = 0; el = navs[i]; ++i) { + el.style.display = 'none'; + el.parentNode.insertBefore(i ? cntBottom : cnt, el); + } + + navs = $.cls('show-all-boards'); + + for (i = 0; el = navs[i]; ++i) { + el.addEventListener('click', CustomMenu.reset, false); + } + } +}; + +CustomMenu.onClick = function(e) { + var t; + + if ((t = e.target) == document) { + return; + } + + if (t.hasAttribute('data-close')) { + CustomMenu.closeEditor(); + } + else if (t.hasAttribute('data-save')) { + CustomMenu.save($.id('customMenu').hasAttribute('data-standalone')); + } +}; + +CustomMenu.showEditor = function(standalone) { + var cnt; + + cnt = document.createElement('div'); + cnt.id = 'customMenu'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-close', '1'); + + if (standalone === true) { + cnt.setAttribute('data-standalone', '1'); + } + + cnt.innerHTML = '\ +
    Custom Board List\ +Close
    \ +\ +
    '; + + document.body.appendChild(cnt); + + if (Config.customMenuList) { + $.id('customMenuBox').value = Config.customMenuList; + } + + cnt.addEventListener('click', CustomMenu.onClick, false); +}; + +CustomMenu.closeEditor = function() { + var el; + + if (el = $.id('customMenu')) { + el.removeEventListener('click', CustomMenu.onClick, false); + document.body.removeChild(el); + } +}; + +CustomMenu.save = function(standalone) { + var input; + + if (input = $.id('customMenuBox')) { + Config.customMenuList = input.value; + + if (standalone === true) { + CustomMenu.apply(Config.customMenuList); + Config.customMenu = true; + Config.save(); + } + } + + CustomMenu.closeEditor(); +}; + +/** + * Draggable helper + */ +var Draggable = { + el: null, + key: null, + scrollX: null, + scrollY: null, + dx: null, dy: null, right: null, bottom: null, offsetTop: null, + + set: function(handle) { + handle.addEventListener('mousedown', Draggable.startDrag, false); + }, + + unset: function(handle) { + handle.removeEventListener('mousedown', Draggable.startDrag, false); + }, + + startDrag: function(e) { + var self, doc, offs; + + if (this.parentNode.hasAttribute('data-shiftkey') && !e.shiftKey) { + return; + } + + e.preventDefault(); + + self = Draggable; + doc = document.documentElement; + + self.el = this.parentNode; + + self.key = self.el.getAttribute('data-trackpos'); + offs = self.el.getBoundingClientRect(); + self.dx = e.clientX - offs.left; + self.dy = e.clientY - offs.top; + self.right = doc.clientWidth - offs.width; + self.bottom = doc.clientHeight - offs.height; + + if (getComputedStyle(self.el, null).position != 'fixed') { + self.scrollX = window.pageXOffset; + self.scrollY = window.pageYOffset; + } + else { + self.scrollX = self.scrollY = 0; + } + + self.offsetTop = Main.getDocTopOffset(); + + document.addEventListener('mouseup', self.endDrag, false); + document.addEventListener('mousemove', self.onDrag, false); + }, + + endDrag: function() { + document.removeEventListener('mouseup', Draggable.endDrag, false); + document.removeEventListener('mousemove', Draggable.onDrag, false); + if (Draggable.key) { + Config[Draggable.key] = Draggable.el.style.cssText; + Config.save(); + } + delete Draggable.el; + }, + + onDrag: function(e) { + var left, top, style; + + left = e.clientX - Draggable.dx + Draggable.scrollX; + top = e.clientY - Draggable.dy + Draggable.scrollY; + style = Draggable.el.style; + if (left < 1) { + style.left = '0'; + style.right = ''; + } + else if (Draggable.right < left) { + style.left = ''; + style.right = '0'; + } + else { + style.left = (left / document.documentElement.clientWidth * 100) + '%'; + style.right = ''; + } + if (top <= Draggable.offsetTop) { + style.top = Draggable.offsetTop + 'px'; + style.bottom = ''; + } + else if (Draggable.bottom < top && + Draggable.el.clientHeight < document.documentElement.clientHeight) { + style.bottom = '0'; + style.top = ''; + } + else { + style.top = (top / document.documentElement.clientHeight * 100) + '%'; + style.bottom = ''; + } + } +}; + +/** + * User Agent + */ +var UA = {}; + +UA.init = function() { + document.head = document.head || $.tag('head')[0]; + + this.isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; + + this.hasCORS = 'withCredentials' in new XMLHttpRequest(); + + this.hasFormData = 'FormData' in window; +}; + +UA.dispatchEvent = function(name, detail) { + var e = document.createEvent('Event'); + e.initEvent(name, false, false); + if (detail) { + e.detail = detail; + } + document.dispatchEvent(e); +}; + +UA.getSelection = function(raw) { + var sel; + + if (UA.isOpera && typeof (sel = document.getSelection()) == 'string') {} + else { + sel = window.getSelection(); + + if (!raw) { + sel = sel.toString(); + } + } + + return sel; +}; + +/** + * Config + */ +var Config = { + quotePreview: true, + backlinks: true, + quickReply: true, + threadUpdater: true, + threadHiding: true, + + alwaysAutoUpdate: false, + topPageNav: false, + threadWatcher: false, + threadAutoWatcher: false, + imageExpansion: true, + fitToScreenExpansion: false, + threadExpansion: true, + alwaysDepage: false, + localTime: true, + stickyNav: false, + keyBinds: false, + inlineQuotes: false, + //showNWSBoards: false, + + filter: false, + revealSpoilers: false, + imageHover: false, + threadStats: true, + IDColor: true, + noPictures: false, + embedYouTube: true, + embedSoundCloud: false, + updaterSound: false, + + customCSS: false, + autoScroll: false, + hideStubs: false, + compactThreads: false, + centeredThreads: false, + dropDownNav: false, + autoHideNav: false, + classicNav: false, + fixedThreadWatcher: false, + persistentQR: false, + forceHTTPS: false, + darkTheme: false, + linkify: false, + unmuteWebm: false, + + disableAll: false +}; + +var ConfigMobile = { + embedYouTube: false, + compactThreads: false, + linkify: true, +}; + +Config.load = function() { + var storage; + + if (storage = localStorage.getItem('4chan-settings')) { + storage = JSON.parse(storage); + $.extend(Config, storage); + + if (Main.getCookie('https') === '1') { + Config.forceHTTPS = true; + } + else { + Config.forceHTTPS = false; + } + } + else { + Main.firstRun = true; + } +}; + +Config.loadFromURL = function() { + var cmd, data; + + cmd = location.href.split('=', 2); + + if (/#cfg$/.test(cmd[0])) { + try { + data = JSON.parse(decodeURIComponent(cmd[1])); + + history.replaceState(null, '', location.href.split('#', 1)[0]); + + $.extend(Config, JSON.parse(data.settings)); + + Config.save(); + + if (data.filters) { + localStorage.setItem('4chan-filters', data.filters); + } + + if (data.css) { + localStorage.setItem('4chan-css', data.css); + } + + if (data.catalogFilters) { + localStorage.setItem('catalog-filters', data.catalogFilters); + } + + if (data.catalogSettings) { + localStorage.setItem('catalog-settings', data.catalogSettings); + } + + return true; + } + catch (e) { + console.log(e); + } + } + + return false; +}; + +Config.toURL = function() { + var data, cfg = {}; + + cfg.settings = localStorage.getItem('4chan-settings'); + + if (data = localStorage.getItem('4chan-filters')) { + cfg.filters = data; + } + + if (data = localStorage.getItem('4chan-css')) { + cfg.css = data; + } + + if (data = localStorage.getItem('catalog-filters')) { + cfg.catalogFilters = data; + } + + if (data = localStorage.getItem('catalog-settings')) { + cfg.catalogSettings = data; + } + + return encodeURIComponent(JSON.stringify(cfg)); +}; + +Config.save = function(old) { + localStorage.setItem('4chan-settings', JSON.stringify(Config)); + + //StorageSync.sync('4chan-settings'); + + if (!old) { + return; + } + + if (Config.forceHTTPS) { + Main.setCookie('https', 1); + } + else { + Main.removeCookie('https'); + } + + if (old.darkTheme != Config.darkTheme) { + if (Config.darkTheme) { + Main.setCookie('nws_style', 'Tomorrow', '.' + $L.d(Main.board)); + Main.setCookie('ws_style', 'Tomorrow', '.' + $L.d(Main.board)); + } + else { + Main.removeCookie('nws_style', '.' + $L.d(Main.board)); + Main.removeCookie('ws_style', '.' + $L.d(Main.board)); + } + } +}; + +/** + * Settings menu + */ +var SettingsMenu = {}; + +// [ Name, Subtitle, available on mobile?, is sub-option?, is mobile only? ] +SettingsMenu.options = { + 'Quotes & Replying': { + quotePreview: [ 'Quote preview', 'Show post when mousing over post links', true ], + backlinks: [ 'Backlinks', 'Show who has replied to a post', true ], + inlineQuotes: [ 'Inline quote links', 'Clicking quote links will inline expand the quoted post, Shift-click to bypass inlining' ], + quickReply: [ 'Quick Reply', 'Quickly respond to a post by clicking its post number', true ], + persistentQR: [ 'Persistent Quick Reply', 'Keep Quick Reply window open after posting' ], + }, + 'Monitoring': { + threadUpdater: [ 'Thread updater', 'Append new posts to bottom of thread without refreshing the page', true ], + alwaysAutoUpdate:[ 'Auto-update by default', 'Always auto-update threads', true ], + threadWatcher: [ 'Thread Watcher', 'Keep track of threads you\'re watching and see when they receive new posts', true ], + threadAutoWatcher: [ 'Automatically watch threads you create', '', true, true ], + autoScroll: [ 'Auto-scroll with auto-updated posts', 'Automatically scroll the page as new posts are added' ], + updaterSound: [ 'Sound notification', 'Play a sound when somebody replies to your post(s)' ], + fixedThreadWatcher: [ 'Pin Thread Watcher to the page', 'Thread Watcher will scroll with you' ], + threadStats: [ 'Thread statistics', 'Display post and image counts on the right of the page, italics signify bump/image limit has been met', true ] + }, + 'Filters & Post Hiding': { + filter: [ 'Filter and highlight specific threads/posts [Edit]', 'Enable pattern-based filters' ], + threadHiding: [ 'Thread hiding [Clear History]', 'Hide entire threads by clicking the minus button', true ], + hideStubs: [ 'Hide thread stubs', "Don't display stubs of hidden threads" ] + }, + 'Navigation': { + threadExpansion: [ 'Thread expansion', 'Expand threads inline on board indexes', true ], + dropDownNav: [ 'Use persistent drop-down navigation bar', '' ], + classicNav: [ 'Use traditional board list', '', false, true ], + autoHideNav: [ 'Auto-hide on scroll', '', false, true ], + customMenu: [ 'Custom board list [Edit]', 'Only show selected boards in top and bottom board lists' ], + //showNWSBoards: [ 'Show all boards', 'Show all boards in top and bottom board lists on 4channel.org', true], + alwaysDepage: [ 'Always use infinite scroll', 'Enable infinite scroll by default, so reaching the bottom of the board index will load subsequent pages', true ], + topPageNav: [ 'Page navigation at top of page', 'Show the page switcher at the top of the page, hold Shift and drag to move' ], + stickyNav: [ 'Navigation arrows', 'Show top and bottom navigation arrows, hold Shift and drag to move' ], + keyBinds: [ 'Use keyboard shortcuts [Show]', 'Enable handy keyboard shortcuts for common actions' ] + }, + 'Images & Media': { + imageExpansion: [ 'Image expansion', 'Enable inline image expansion, limited to browser width', true ], + fitToScreenExpansion: [ 'Fit expanded images to screen', 'Limit expanded images to both browser width and height' ], + imageHover: [ 'Image hover', 'Mouse over images to view full size, limited to browser size' ], + imageHoverBg: [ 'Set a background color for transparent images', '', false, true ], + revealSpoilers: [ "Don't spoiler images", 'Show image thumbnail and original filename instead of spoiler placeholders', true ], + unmuteWebm: [ 'Un-mute WebM audio', 'Un-mute sound automatically for WebM playback', true ], + noPictures: [ 'Hide thumbnails', 'Don\'t display thumbnails while browsing', true ], + embedYouTube: [ 'Embed YouTube links', 'Embed YouTube player into replies' ], + embedSoundCloud: [ 'Embed SoundCloud links', 'Embed SoundCloud player into replies' ], + }, + 'Miscellaneous': { + linkify: [ 'Linkify URLs', 'Make user-posted links clickable', true ], + darkTheme: [ 'Use a dark theme', 'Use the Tomorrow theme for nighttime browsing', true, false, true ], + customCSS: [ 'Custom CSS [Edit]', 'Include your own CSS rules', true ], + IDColor: [ 'Color user IDs', 'Assign unique colors to user IDs on boards that use them', true ], + compactThreads: [ 'Force long posts to wrap', 'Long posts will wrap at 75% browser width' ], + centeredThreads: [ 'Center threads', 'Align threads to the center of page', false ], + localTime: [ 'Convert dates to local time', 'Convert 4chan server time (US Eastern Time) to your local time', true ], + forceHTTPS: [ 'Always use HTTPS', 'Rewrite 4chan URLs to always use HTTPS', true ] + } +}; + +SettingsMenu.save = function() { + var i, options, el, key, old; + + old = {}; + $.extend(old, Config); + + options = $.id('settingsMenu').getElementsByClassName('menuOption'); + + for (i = 0; el = options[i]; ++i) { + key = el.getAttribute('data-option'); + Config[key] = el.type == 'checkbox' ? el.checked : el.value; + } + + Config.save(old); + + UA.dispatchEvent('4chanSettingsSaved'); + + SettingsMenu.close(); + location.href = location.href.replace(/#.+$/, ''); +}; + +SettingsMenu.toggle = function() { + if ($.id('settingsMenu')) { + SettingsMenu.close(); + } + else { + SettingsMenu.open(); + } +}; + +SettingsMenu.open = function() { + var i, cat, categories, key, html, cnt, opts, mobileOpts, el; + + if (Main.firstRun) { + if (el = $.id('settingsTip')) { + el.parentNode.removeChild(el); + } + if (el = $.id('settingsTipBottom')) { + el.parentNode.removeChild(el); + } + Config.save(); + } + + cnt = document.createElement('div'); + cnt.id = 'settingsMenu'; + cnt.className = 'UIPanel'; + + html = '
    Settings' + + 'Close' + + '
      '; + + html += ''; + + if (Main.hasMobileLayout) { + categories = {}; + for (cat in SettingsMenu.options) { + mobileOpts = {}; + opts = SettingsMenu.options[cat]; + for (key in opts) { + if (opts[key][2]) { + mobileOpts[key] = opts[key]; + } + } + for (i in mobileOpts) { + categories[cat] = mobileOpts; + break; + } + } + } + else { + categories = SettingsMenu.options; + } + + for (cat in categories) { + opts = categories[cat]; + html += '
      • ' + + '' + + '' + + cat + '
        • '; + for (key in opts) { + // Mobile layout only? + if (opts[key][4] && !Main.hasMobileLayout) { + continue; + } + html += '' : '>') + + '' + + (opts[key][1] !== false ? '
        • ' : '">') + opts[key][1] : '') + + '
        • '; + } + html += '
      '; + } + + html += '
    • ' + + '
    ' + + '
    ' + + '
    '; + + cnt.innerHTML = html; + cnt.addEventListener('click', SettingsMenu.onClick, false); + document.body.appendChild(cnt); + + if (Main.firstRun) { + SettingsMenu.expandAll(); + } + + (el = $.cls('menuOption', cnt)[0]) && el.focus(); +}; + +SettingsMenu.showExport = function() { + var cnt, str, el; + + if ($.id('exportSettings')) { + return; + } + + str = location.href.replace(location.hash, '').replace(/^http:/, 'https:') + '#cfg=' + Config.toURL(); + + cnt = document.createElement('div'); + cnt.id = 'exportSettings'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'export-close'); + cnt.innerHTML = '\ +
    Export Settings\ +Close
    \ +

    Copy and save the URL below, and visit it from another \ +browser or computer to restore your extension and catalog settings.

    \ +

    \ +

    \ +

    Alternatively, you can drag the link below into your \ +bookmarks bar and click it to restore.

    \ +

    [Restore 4chan Settings]

    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onExportClick, false); + el = $.cls('export-field', cnt)[0]; + el.focus(); + el.select(); +}; + +SettingsMenu.closeExport = function() { + var cnt; + + if (cnt = $.id('exportSettings')) { + cnt.removeEventListener('click', this.onExportClick, false); + document.body.removeChild(cnt); + } +}; + +SettingsMenu.onExportClick = function(e) { + if (e.target.id == 'exportSettings') { + e.preventDefault(); + e.stopPropagation(); + SettingsMenu.closeExport(); + } +}; + +SettingsMenu.expandAll = function() { + var i, el, nodes = $.cls('settings-expand'); + + for (i = 0; el = nodes[i]; ++i) { + el.src = Main.icons.minus; + el.parentNode.nextElementSibling.style.display = 'block'; + } +}; + +SettingsMenu.toggleCat = function(t) { + var icon, disp, el = t.parentNode.nextElementSibling; + + if (!el.style.display) { + disp = 'block'; + icon = 'minus'; + } + else { + disp = ''; + icon = 'plus'; + } + + el.style.display = disp; + t.parentNode.firstElementChild.src = Main.icons[icon]; +}; + +SettingsMenu.onClick = function(e) { + var el, t; + + t = e.target; + + if ($.hasClass(t, 'settings-expand')) { + SettingsMenu.toggleCat(t); + } + else if (t.getAttribute('data-cmd') == 'settings-exp-all') { + e.preventDefault(); + SettingsMenu.expandAll(); + } + else if (t.id == 'settingsMenu' && (el = $.id('settingsMenu'))) { + e.preventDefault(); + SettingsMenu.close(el); + } +}; + +SettingsMenu.close = function(el) { + if (el = (el || $.id('settingsMenu'))) { + el.removeEventListener('click', SettingsMenu.onClick, false); + document.body.removeChild(el); + } +}; + +/** + * Feedback + */ +var Feedback = { + messageTimeout: null, + + showMessage: function(msg, type, timeout, onClick) { + var el; + + Feedback.hideMessage(); + + el = document.createElement('div'); + el.id = 'feedback'; + el.title = 'Dismiss'; + el.innerHTML = ''; + + $.on(el, 'click', onClick || Feedback.hideMessage); + + document.body.appendChild(el); + + if (timeout) { + Feedback.messageTimeout = setTimeout(Feedback.hideMessage, timeout); + } + }, + + hideMessage: function() { + var el = $.id('feedback'); + + if (el) { + if (Feedback.messageTimeout) { + clearTimeout(Feedback.messageTimeout); + Feedback.messageTimeout = null; + } + + $.off(el, 'click', Feedback.hideMessage); + + document.body.removeChild(el); + } + }, + + error: function(msg, timeout) { + if (timeout === undefined) { + timeout = 5000; + } + + Feedback.showMessage(msg || 'Something went wrong', 'error', timeout); + }, + + notify: function(msg, timeout) { + if (timeout === undefined) { + timeout = 3000; + } + + Feedback.showMessage(msg, 'notify', timeout); + } +}; + +/** + * Main + */ +var Main = {}; + +Main.addTooltip = function(link, message, id) { + var el, pos; + + el = document.createElement('div'); + el.className = 'click-me'; + if (id) { + el.id = id; + } + el.innerHTML = message || 'Change your settings'; + link.parentNode.appendChild(el); + + pos = (link.offsetWidth - el.offsetWidth + link.offsetLeft - el.offsetLeft) / 2; + el.style.marginLeft = pos + 'px'; + + return el; +}; + +Main.getDocTopOffset = function() { + if (Config.dropDownNav && !Config.autoHideNav) { + return $.id( + Config.classicNav ? 'boardNavDesktop' : 'boardNavMobile' + ).offsetHeight; + } + else { + return 0; + } +}; + +Main.init = function() { + var params; + + document.addEventListener('DOMContentLoaded', Main.run, false); + + Main.now = Date.now(); + + Main.is_4channel = location.host === 'boards.4channel.org'; + + UA.init(); + + Config.load(); + + if (Config.forceHTTPS && location.protocol != 'https:') { + location.href = location.href.replace(/^http:/, 'https:'); + return; + } + + if (Main.firstRun && Config.loadFromURL()) { + Main.firstRun = false; + } + + if (Main.stylesheet = Main.getCookie(style_group)) { + Main.stylesheet = Main.stylesheet.toLowerCase().replace(/ /g, '_'); + } + else { + Main.stylesheet = + style_group == 'nws_style' ? 'yotsuba_new' : 'yotsuba_b_new'; + } + + QR.noCaptcha = QR.noCaptcha || window.passEnabled; + + Main.initIcons(); + + Main.addCSS(); + + Main.type = style_group.split('_')[0]; + + params = location.pathname.split(/\//); + Main.board = params[1]; + Main.page = params[2]; + Main.tid = params[3]; + + UA.dispatchEvent('4chanMainInit'); +}; + +Main.initPersistentNav = function() { + var el, top, bottom; + + top = $.id('boardNavDesktop'); + bottom = $.id('boardNavDesktopFoot'); + + if (Config.classicNav) { + el = document.createElement('div'); + el.className = 'pageJump'; + el.innerHTML = '' + + 'Settings' + + 'Home
    '; + + top.appendChild(el); + + $.id('settingsWindowLinkClassic') + .addEventListener('click', SettingsMenu.toggle, false); + + $.addClass(top, 'persistentNav'); + } + else { + top.style.display = 'none'; + $.removeClass($.id('boardNavMobile'), 'mobile'); + } + + bottom.style.display = 'none'; + + $.addClass(document.body, 'hasDropDownNav'); +}; + +Main.checkMobileLayout = function() { + var mobile, desktop; + + if (window.matchMedia) { + return window.matchMedia('(max-width: 480px)').matches + && localStorage.getItem('4chan_never_show_mobile') != 'true'; + } + + mobile = $.id('boardNavMobile'); + desktop = $.id('boardNavDesktop'); + + return mobile && desktop && mobile.offsetWidth > 0 && desktop.offsetWidth === 0; +}; + +Main.disableDarkTheme = function() { + Config.darkTheme = false; + localStorage.setItem('4chan-settings', JSON.stringify(Config)); +}; + +Main.run = function() { + var el, thread; + + document.removeEventListener('DOMContentLoaded', Main.run, false); + + document.addEventListener('click', Main.onclick, false); + + $.id('settingsWindowLink').addEventListener('click', SettingsMenu.toggle, false); + $.id('settingsWindowLinkBot').addEventListener('click', SettingsMenu.toggle, false); + $.id('settingsWindowLinkMobile').addEventListener('click', SettingsMenu.toggle, false); + + Main.isOekakiBoard = Main.board === 'i'; + + Main.hasMobileLayout = Main.checkMobileLayout(); + Main.isMobileDevice = /Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent); + + Search.init(); + + if (Config.disableAll) { + return; + } + + Report.init(); + + if (Main.hasMobileLayout) { + $.extend(Config, ConfigMobile); + } + else { + if (el = $.id('bottomReportBtn')) { + el.style.display = 'none'; + } + + if (Main.isMobileDevice) { + $.addClass(document.body, 'isMobileDevice'); + } + } + /* + if (Main.is_4channel && Config.showNWSBoards) { + CustomMenu.showNWSBoards(); + } + */ + if (Config.linkify) { + Linkify.init(); + } + + if (Config.IDColor) { + IDColor.init(); + } + + if (Config.customCSS) { + CustomCSS.init(); + } + + if (Config.keyBinds) { + Keybinds.init(); + } + + if (Main.firstRun && Main.isMobileDevice) { + Config.topPageNav = false; + Config.dropDownNav = true; + } + + if (Config.dropDownNav && !Main.hasMobileLayout) { + Main.initPersistentNav(); + } + + $.addClass(document.body, Main.stylesheet); + $.addClass(document.body, Main.type); + + if (Config.darkTheme) { + $.addClass(document.body, 'm-dark'); + if (!Main.hasMobileLayout) { + $.cls('stylechanger')[0].addEventListener('change', Main.disableDarkTheme, false); + } + } + + if (Config.compactThreads) { + $.addClass(document.body, 'compact'); + } + else if (Config.centeredThreads) { + $.addClass(document.body, 'centeredThreads'); + } + + if (Config.noPictures) { + $.addClass(document.body, 'noPictures'); + } + + if (Config.customMenu) { + CustomMenu.apply(Config.customMenuList); + } + + CustomMenu.initCtrl(); + + if (Config.quotePreview || Config.imageHover|| Config.filter) { + thread = $.id('delform') || $.id('arc-list'); + thread.addEventListener('mouseover', Main.onThreadMouseOver, false); + thread.addEventListener('mouseout', Main.onThreadMouseOut, false); + } + + if (Config.stickyNav) { + Main.setStickyNav(); + } + + if (!Main.hasMobileLayout) { + Main.initGlobalMessage(); + if (Config.autoHideNav) { + StickyNav.init(); + } + } + else { + StickyNav.init(); + } + + if (Config.threadExpansion) { + ThreadExpansion.init(); + } + + if (Config.filter) { + Filter.init(); + } + + if (Config.threadWatcher) { + ThreadWatcher.init(); + } + + if (Main.hasMobileLayout || Config.embedSoundCloud || Config.embedYouTube) { + Media.init(); + } + + ReplyHiding.init(); + + if (Config.quotePreview) { + QuotePreview.init(); + } + + Parser.init(); + + if (Main.tid) { + Main.threadClosed = !document.forms.post || !!$.cls('closedIcon')[0]; + Main.threadSticky = !!$.cls('stickyIcon', $.id('pi' + Main.tid))[0]; + + if (Config.threadStats) { + ThreadStats.init(); + } + + Parser.parseThread(Main.tid); + + if (Config.threadUpdater) { + ThreadUpdater.init(); + } + } + else { + if (!Main.page) { + Depager.init(); + } + + if (Config.topPageNav) { + Main.setPageNav(); + } + if (Config.threadHiding) { + ThreadHiding.init(); + Parser.parseBoard(); + } + else { + Parser.parseBoard(); + } + } + + if (Main.board === 'f') { + SWFEmbed.init(); + } + + if (Config.quickReply) { + QR.init(); + } + + ReplyHiding.purge(); + + if (Config.alwaysDepage && !Main.hasMobileLayout) { + if ($.docEl.scrollHeight <= $.docEl.clientHeight) { + Depager.depage(); + } + } +}; + +Main.isThreadClosed = function(tid) { + var el; + return window.thread_archived || ((el = $.id('pi' + tid)) && $.cls('closedIcon', el)[0]); +}; + +Main.setThreadState = function(state, mode) { + var cnt, el, ref, cap; + + cap = state.charAt(0).toUpperCase() + state.slice(1); + + if (mode) { + cnt = $.cls('postNum', $.id('pi' + Main.tid))[0]; + el = document.createElement('img'); + el.className = state + 'Icon retina'; + el.title = cap; + el.src = Main.icons2[state]; + if (state == 'sticky' && (ref = $.cls('closedIcon', cnt)[0])) { + cnt.insertBefore(el, ref); + cnt.insertBefore(document.createTextNode(' '), ref); + } + else { + cnt.appendChild(document.createTextNode(' ')); + cnt.appendChild(el); + } + } + else { + if (el = $.cls(state + 'Icon', $.id('pi' + Main.tid))[0]) { + el.parentNode.removeChild(el.previousSibling); + el.parentNode.removeChild(el); + } + } + + Main['thread' + cap] = mode; +}; + +Main.icons = { + up: 'arrow_up.png', + down: 'arrow_down.png', + right: 'arrow_right.png', + download: 'arrow_down2.png', + refresh: 'refresh.png', + cross: 'cross.png', + gis: 'gis.png', + iqdb: 'iqdb.png', + minus: 'post_expand_minus.png', + plus: 'post_expand_plus.png', + rotate: 'post_expand_rotate.gif', + quote: 'quote.png', + report: 'report.png', + notwatched: 'watch_thread_off.png', + watched: 'watch_thread_on.png', + help: 'question.png' +}; + +Main.icons2 = { + archived: 'archived.gif', + closed: 'closed.gif', + sticky: 'sticky.gif', + trash: 'trash.gif' +}, + +Main.initIcons = function() { + var key, paths, url; + + paths = { + yotsuba_new: 'futaba/', + futaba_new: 'futaba/', + yotsuba_b_new: 'burichan/', + burichan_new: 'burichan/', + tomorrow: 'tomorrow/', + photon: 'photon/' + }; + + url = '//s.4cdn.org/image/'; + + if (window.devicePixelRatio >= 2) { + for (key in Main.icons) { + Main.icons[key] = Main.icons[key].replace('.', '@2x.'); + } + for (key in Main.icons2) { + Main.icons2[key] = Main.icons2[key].replace('.', '@2x.'); + } + } + + for (key in Main.icons2) { + Main.icons2[key] = url + Main.icons2[key]; + } + + url += 'buttons/' + paths[Main.stylesheet]; + for (key in Main.icons) { + Main.icons[key] = url + Main.icons[key]; + } +}; + +Main.setPageNav = function() { + var el, cnt; + + cnt = document.createElement('div'); + cnt.setAttribute('data-shiftkey', '1'); + cnt.setAttribute('data-trackpos', 'TN-position'); + cnt.className = 'topPageNav'; + + if (Config['TN-position']) { + cnt.style.cssText = Config['TN-position']; + } + else { + cnt.style.left = '10px'; + cnt.style.top = '50px'; + } + + el = $.cls('pagelist')[0]; + + if (!el) { + return; + } + + el = el.cloneNode(true); + cnt.appendChild(el); + Draggable.set(el); + document.body.appendChild(cnt); +}; + +Main.getWebmVolume = function() { + let vol = parseFloat(localStorage.getItem('4chan-volume')); + + if (!isNaN(vol)) { + return vol; + } + else { + return 0.5; + } +}; + +Main.getWebmVolumeChangeCb = function() { + let t; + + return (e) => { + clearTimeout(t); + t = setTimeout(() => { localStorage.setItem('4chan-volume', e.target.volume); }, 200); + }; +}; + +Main.initGlobalMessage = function() { + var msg, btn, thisTs, oldTs; + + if ((msg = $.id('globalMessage')) && msg.textContent) { + msg.nextElementSibling.style.clear = 'both'; + + btn = document.createElement('img'); + btn.id = 'toggleMsgBtn'; + btn.className = 'extButton'; + btn.setAttribute('data-cmd', 'toggleMsg'); + btn.alt = 'Toggle'; + btn.title = 'Toggle announcement'; + + oldTs = localStorage.getItem('4chan-global-msg'); + thisTs = msg.getAttribute('data-utc'); + + if (oldTs && thisTs <= oldTs) { + msg.style.display = 'none'; + btn.style.opacity = '0.5'; + btn.src = Main.icons.plus; + } + else { + btn.src = Main.icons.minus; + } + + msg.parentNode.insertBefore(btn, msg); + } +}; + +Main.toggleGlobalMessage = function() { + var msg, btn; + + msg = $.id('globalMessage'); + btn = $.id('toggleMsgBtn'); + if (msg.style.display == 'none') { + msg.style.display = ''; + btn.src = Main.icons.minus; + btn.style.opacity = '1'; + localStorage.removeItem('4chan-global-msg'); + } + else { + msg.style.display = 'none'; + btn.src = Main.icons.plus; + btn.style.opacity = '0.5'; + localStorage.setItem('4chan-global-msg', msg.getAttribute('data-utc')); + } + + //StorageSync.sync('4chan-global-msg'); +}; + +Main.setStickyNav = function() { + var cnt, hdr; + + cnt = document.createElement('div'); + cnt.id = 'stickyNav'; + cnt.className = 'extPanel reply'; + cnt.setAttribute('data-shiftkey', '1'); + cnt.setAttribute('data-trackpos', 'SN-position'); + + if (Config['SN-position']) { + cnt.style.cssText = Config['SN-position']; + } + else { + cnt.style.right = '10px'; + cnt.style.top = '50px'; + } + + hdr = document.createElement('div'); + hdr.innerHTML = '▲' + + '▼'; + Draggable.set(hdr); + + cnt.appendChild(hdr); + document.body.appendChild(cnt); +}; + +Main.getCookie = function(name) { + var i, c, ca, key; + + key = name + "="; + ca = document.cookie.split(';'); + + for (i = 0; c = ca[i]; ++i) { + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(key) === 0) { + return decodeURIComponent(c.substring(key.length, c.length)); + } + } + return null; +}; + +Main.setCookie = function(name, value, domain) { + var date = new Date(); + + date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000)); + + if (!domain) { + domain = location.host; + } + + document.cookie = name + '=' + value + + '; expires=' + date.toGMTString() + + '; path=/; domain=' + domain; +}; + +Main.removeCookie = function(name, domain, path) { + if (!domain) { + domain = location.host; + } + + if (!path) { + path = '/'; + } + + document.cookie = name + '=' + + '; expires=Thu, 01 Jan 1970 00:00:01 GMT;' + + '; path=' + path + '; domain=' + domain; +}; + +Main.onclick = function(e) { + var t, cmd, tid, id; + + if ((t = e.target) == document) { + return; + } + + if (cmd = t.getAttribute('data-cmd')) { + id = t.getAttribute('data-id'); + switch (cmd) { + case 'update': + e.preventDefault(); + ThreadUpdater.forceUpdate(); + break; + case 'post-menu': + e.preventDefault(); + PostMenu.open(t); + break; + case 'auto': + ThreadUpdater.toggleAuto(); + break; + case 'totop': + case 'tobottom': + if (!e.shiftKey) { + location.href = '#' + cmd.slice(2); + } + break; + case 'hide': + ThreadHiding.toggle(id); + break; + case 'watch': + ThreadWatcher.toggle(id); + break; + case 'hide-r': + if (t.hasAttribute('data-recurse')) { + ReplyHiding.toggleR(id); + } + else { + ReplyHiding.toggle(id); + } + break; + case 'expand': + ThreadExpansion.toggle(id); + break; + case 'open-qr': + e.preventDefault(); + QR.show(Main.tid); + $.tag('textarea', document.forms.qrPost)[0].focus(); + break; + case 'qr-painter-draw': + QR.openPainter(); + break; + case 'qr-painter-clear': + QR.onPainterCancel(); + break; + case 'qr-painter-edit': + e.preventDefault(); + QR.onOpenInPainterClick(t); + break; + case 'unfilter': + Filter.unfilter(t); + break; + case 'depage': + e.preventDefault(); + Depager.toggle(); + break; + case 'report': + Report.open(id, t.getAttribute('data-board')); + break; + case 'filter-sel': + e.preventDefault(); + Filter.addSelection(); + break; + case 'embed': + Media.toggleEmbed(t); + break; + case 'sound': + ThreadUpdater.toggleSound(); + break; + case 'toggleMsg': + Main.toggleGlobalMessage(); + break; + case 'settings-toggle': + SettingsMenu.toggle(); + break; + case 'settings-save': + SettingsMenu.save(); + break; + case 'keybinds-open': + Keybinds.open(); + break; + case 'filters-open': + Filter.open(); + break; + case 'thread-hiding-clear': + ThreadHiding.clear(); + break; + case 'css-open': + CustomCSS.open(); + break; + case 'settings-export': + SettingsMenu.showExport(); + break; + case 'export-close': + SettingsMenu.closeExport(); + break; + case 'custom-menu-edit': + e.preventDefault(); + CustomMenu.showEditor($.hasClass(t.parentNode, 'custom-menu-ctrl')); + break; + case 'del-post': + case 'del-file': + e.preventDefault(); + Del.deletePost(id, cmd === 'del-file'); + break; + case 'open-tex-preview': + QR.openTeXPreview(); + break; + case 'close-tex-preview': + QR.closeTeXPreview(); + break; + } + } + else if (!Config.disableAll) { + if (QR.enabled && t.title == 'Reply to this post') { + e.preventDefault(); + tid = Main.tid || t.previousElementSibling.getAttribute('href').split('#')[0].split('/').slice(-1)[0]; + QR.quotePost(tid, !e.ctrlKey && t.textContent); + } + else if (Config.imageExpansion && e.which == 1 && t.parentNode + && $.hasClass(t.parentNode, 'fileThumb') + && t.parentNode.nodeName == 'A' + && !$.hasClass(t.parentNode, 'deleted') + && !$.hasClass(t, 'mFileInfo')) { + + if (ImageExpansion.toggle(t)) { + e.preventDefault(); + } + + return; + } + else if (Config.inlineQuotes && e.which == 1 && $.hasClass(t, 'quotelink') && Main.page !== 'archive') { + if (!e.shiftKey) { + QuoteInline.toggle(t, e); + } + else { + e.preventDefault(); + window.location = t.href; + } + } + else if (Config.threadExpansion && t.parentNode && $.hasClass(t.parentNode, 'abbr')) { + e.preventDefault(); + ThreadExpansion.expandComment(t); + } + else if (Main.isMobileDevice && Config.quotePreview + && $.hasClass(t, 'quotelink') + && t.getAttribute('href').match(QuotePreview.regex)) { + e.preventDefault(); + } + else if ($.hasClass(t, 'mFileInfo')) { + e.preventDefault(); + e.stopPropagation(); + } + } + + if (Main.hasMobileLayout && (Config.disableAll || !Config.imageExpansion)) { + if (t.parentNode && t.parentNode.hasAttribute('data-m')) { + ImageExpansion.setMobileSrc(t.parentNode); + } + } +}; + +Main.onThreadMouseOver = function(e) { + var t = e.target; + + if (Config.quotePreview + && $.hasClass(t, 'quotelink') + && !$.hasClass(t, 'deadlink') + && !$.hasClass(t, 'linkfade')) { + QuotePreview.resolve(e.target); + } + else if (Config.imageHover && ( + (t.hasAttribute('data-md5') && !$.hasClass(t.parentNode, 'deleted')) + || + (t.href && !$.hasClass(t.parentNode, 'fileText') && /(i\.4cdn|is\.4chan)\.org\/[a-z0-9]+\/[0-9]+\.(gif|jpg|png|webm)$/.test(t.href)) + ) + ) { + ImageHover.show(t); + } + else if ($.hasClass(t, 'dateTime')) { + Parser.onDateMouseOver(t); + } + else if ($.hasClass(t, 'hand')) { + Parser.onUIDMouseOver(t); + } + else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) { + Media.showYTPreview(t); + } + else if (Config.filter && t.hasAttribute('data-filtered')) { + QuotePreview.show(t, + t.href ? t.parentNode.parentNode.parentNode : t.parentNode.parentNode); + } +}; + +Main.onThreadMouseOut = function(e) { + var t = e.target; + + if (Config.quotePreview && $.hasClass(t, 'quotelink')) { + QuotePreview.remove(t); + } + else if (Config.imageHover && + (t.hasAttribute('data-md5') + || (t.href && !$.hasClass(t.parentNode, 'fileText') && /(i\.4cdn|is\.4chan)\.org\/[a-z0-9]+\/[0-9]+\.(gif|jpg|png|webm)$/.test(t.href)) + ) + ) { + ImageHover.hide(); + } + else if ($.hasClass(t, 'dateTime') || $.hasClass(t, 'hand')) { + Parser.onTipMouseOut(t); + } + else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) { + Media.removeYTPreview(); + } + else if (Config.filter && t.hasAttribute('data-filtered')) { + QuotePreview.remove(t); + } +}; + +Main.linkToThread = function(tid, board, post) { + return '//' + location.host + '/' + + (board || Main.board) + '/thread/' + + tid + (post > 0 ? ('#p' + post) : ''); +}; + +Main.addCSS = function() { + var style, css = '\ +body.hasDropDownNav {\ + margin-top: 45px;\ +}\ +.extButton.threadHideButton {\ + float: left;\ + margin-right: 5px;\ + margin-top: -1px;\ +}\ +.extButton.replyHideButton {\ + margin-top: 1px;\ +}\ +div.op > span .postHideButtonCollapsed {\ + margin-right: 1px;\ +}\ +.dropDownNav #boardNavMobile, {\ + display: block !important;\ +}\ +.extPanel {\ + border: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.tomorrow .extPanel {\ + border: 1px solid #111;\ +}\ +.extButton,\ +img.pointer {\ + width: 18px;\ + height: 18px;\ +}\ +.extControls {\ + display: inline;\ + margin-left: 5px;\ +}\ +.extButton {\ + cursor: pointer;\ + margin-bottom: -4px;\ +}\ +.trashIcon {\ + width: 16px;\ + height: 16px;\ + margin-bottom: -2px;\ + margin-left: 5px;\ +}\ +.threadUpdateStatus {\ + margin-left: 0.5ex;\ +}\ +.futaba_new .stub,\ +.burichan_new .stub {\ + line-height: 1;\ + padding-bottom: 1px;\ +}\ +.stub .extControls,\ +.stub .wbtn,\ +.stub input {\ + display: none;\ +}\ +.stub .threadHideButton {\ + float: none;\ + margin-right: 2px;\ +}\ +.right {\ + float: right;\ +}\ +.center {\ + display: block;\ + margin: auto;\ +}\ +.pointer {\ + cursor: pointer;\ +}\ +.drag {\ + cursor: move !important;\ + user-select: none !important;\ + -moz-user-select: none !important;\ + -webkit-user-select: none !important;\ +}\ +#quickReply {\ + display: block;\ + position: fixed;\ + padding: 2px;\ + font-size: 10pt;\ +}\ +#qrepHeader,\ +#qrHeader {\ + font-size: 10pt;\ + text-align: center;\ + margin-bottom: 1px;\ + padding: 0;\ + height: 18px;\ + line-height: 18px;\ +}\ +#qrHeader .left { float: left; margin-left: 3px; }\ +#qrepClose,\ +#qrClose {\ + float: right;\ +}\ +#qrCaptchaContainer { width: 300px; background-color: #eee; overflow: hidden; margin-bottom: 3px }\ +.tomorrow #qrCaptchaContainer.t-qr-root { background-color: #323232; }\ +.tomorrow #qrCaptchaContainer #t-cnt { filter: invert(85%); }\ +#qrForm > div {\ + clear: both;\ +}\ +#quickReply input[type="text"],\ +#quickReply textarea,\ +#quickReply #recaptcha_response_field {\ + border: 1px solid #aaa;\ + font-family: arial,helvetica,sans-serif;\ + font-size: 10pt;\ + outline: medium none;\ + width: 296px;\ + padding: 2px;\ + margin: 0 0 1px 0;\ +}\ +.tomorrow #quickReply input[type="text"],\ +.tomorrow #quickReply textarea,\ +.tomorrow #quickReply #recaptcha_response_field {\ + border: 1px solid #515151;\ + background-color: #282a2e;\ + color: #c5c8c6;\ +}\ +.tomorrow #quickReply input[type="text"]:focus,\ +.tomorrow #quickReply textarea:focus {\ + border: 1px solid #757575;\ +}\ +#quickReply textarea {\ + min-width: 296px;\ + float: left;\ +}\ +.tomorrow #quickReply input::placeholder {\ + color: 919191 !important;\ +}\ +#quickReply input[type="submit"] {\ + width: 75px;\ + margin: 0;\ + float: right;\ +}\ +#quickReply #qrCapField {\ + display: block;\ + margin-top: 1px;\ +}\ +#quickReply input.presubmit {\ + margin-right: 1px;\ + width: 212px;\ + float: left;\ +}\ +#qrFile {\ + width: 130px;\ + margin-right: 5px;\ +}\ +.yotsuba_new #qrFile {\ + color:black;\ +}\ +#qrSpoiler {\ + display: inline;\ +}\ +#qrError {\ + width: 292px;\ + display: none;\ + font-family: monospace;\ + background-color: #E62020;\ + font-size: 12px;\ + color: white;\ + padding: 3px 5px;\ + text-shadow: 0 1px rgba(0, 0, 0, 0.20);\ + clear: both;\ +}\ +#qrError a:hover,\ +#qrError a {\ + color: white !important;\ + text-decoration: underline;\ +}\ +#twHeader {\ + font-weight: bold;\ + text-align: center;\ + height: 17px;\ +}\ +.futaba_new #twHeader,\ +.burichan_new #twHeader {\ + line-height: 1;\ +}\ +#twPrune {\ + margin-left: 3px;\ + margin-top: -1px;\ +}\ +#twClose {\ + float: left;\ + margin-top: -1px;\ +}\ +#threadWatcher {\ + max-width: 265px;\ + display: block;\ + position: absolute;\ + padding: 3px;\ +}\ +#watchList {\ + margin: 0;\ + padding: 0;\ + user-select: none;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ +}\ +#watchList li:first-child {\ + margin-top: 3px;\ + padding-top: 2px;\ + border-top: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.photon #watchList li:first-child {\ + border-top: 1px solid #ccc;\ +}\ +.yotsuba_new #watchList li:first-child {\ + border-top: 1px solid #d9bfb7;\ +}\ +.yotsuba_b_new #watchList li:first-child {\ + border-top: 1px solid #b7c5d9;\ +}\ +.tomorrow #watchList li:first-child {\ + border-top: 1px solid #111;\ +}\ +#watchList a {\ + text-decoration: none;\ +}\ +#watchList li {\ + overflow: hidden;\ + white-space: nowrap;\ + text-overflow: ellipsis;\ +}\ +div.post div.image-expanded {\ + display: table;\ +}\ +div.op div.file .image-expanded-anti {\ + margin-left: -3px;\ +}\ +#quote-preview {\ + display: block;\ + position: absolute;\ + top: 0;\ + padding: 3px 6px 6px 3px;\ + margin: 0;\ +}\ +#quote-preview .dateTime {\ + white-space: nowrap;\ +}\ +#quote-preview.reveal-spoilers s {\ + background-color: #aaa !important;\ + color: inherit !important;\ + text-decoration: none !important;\ +}\ +#quote-preview.reveal-spoilers s a {\ + background: transparent !important;\ + text-decoration: underline;\ +}\ +.yotsuba_b_new #quote-preview.reveal-spoilers s a,\ +.burichan_new #quote-preview.reveal-spoilers s a {\ + color: #D00 !important;\ +}\ +.yotsuba_new #quote-preview.reveal-spoilers s a,\ +.futaba_new #quote-preview.reveal-spoilers s a {\ + color: #000080 !important;\ +}\ +.tomorrow #quote-preview.reveal-spoilers s { color: #000 !important; }\ +.tomorrow #quote-preview.reveal-spoilers s a { color: #5F89AC !important; }\ +.photon #quote-preview.reveal-spoilers s a {\ + color: #FF6600 !important;\ +}\ +.yotsuba_new #quote-preview.highlight,\ +.yotsuba_b_new #quote-preview.highlight {\ + border-width: 1px 2px 2px 1px !important;\ + border-style: solid !important;\ +}\ +.yotsuba_new #quote-preview.highlight {\ + border-color: #D99F91 !important;\ +}\ +.yotsuba_b_new #quote-preview.highlight {\ + border-color: #BA9DBF !important;\ +}\ +.yotsuba_b_new .highlight-anti,\ +.burichan_new .highlight-anti {\ + border-width: 1px !important;\ + background-color: #bfa6ba !important;\ +}\ +.yotsuba_new .highlight-anti,\ +.futaba_new .highlight-anti {\ + background-color: #e8a690 !important;\ +}\ +.tomorrow .highlight-anti {\ + background-color: #111 !important;\ + border-color: #111;\ +}\ +.photon .highlight-anti {\ + background-color: #bbb !important;\ +}\ +.op.inlined {\ + display: block;\ +}\ +#quote-preview .inlined,\ +#quote-preview .postMenuBtn,\ +#quote-preview .extButton,\ +#quote-preview .extControls {\ + display: none;\ +}\ +.hasNewReplies { font-weight: bold; }\ +.hasYouReplies { font-style: italic; }\ +.archivelink {\ + opacity: 0.5;\ +}\ +.deadlink {\ + text-decoration: line-through !important;\ +}\ +div.backlink {\ + font-size: 0.8em !important;\ + display: inline;\ + padding: 0;\ + padding-left: 5px;\ +}\ +.backlink.mobile {\ + padding: 3px 5px;\ + display: block;\ + clear: both !important;\ + line-height: 2;\ +}\ +.op .backlink.mobile,\ +#quote-preview .backlink.mobile {\ + display: none !important;\ +}\ +.backlink.mobile .quoteLink {\ + padding-right: 2px;\ +}\ +.backlink span {\ + padding: 0;\ +}\ +.burichan_new .backlink a,\ +.yotsuba_b_new .backlink a {\ + color: #34345C !important;\ +}\ +.burichan_new .backlink a:hover,\ +.yotsuba_b_new .backlink a:hover {\ + color: #dd0000 !important;\ +}\ +.expbtn {\ + margin-right: 3px;\ + margin-left: 2px;\ +}\ +.tCollapsed .rExpanded {\ + display: none;\ +}\ +#stickyNav {\ + position: fixed;\ + font-size: 0;\ +}\ +#stickyNav img {\ + vertical-align: middle;\ +}\ +.tu-error {\ + color: red;\ +}\ +.topPageNav {\ + position: absolute;\ +}\ +.yotsuba_b_new .topPageNav {\ + border-top: 1px solid rgba(255, 255, 255, 0.25);\ + border-left: 1px solid rgba(255, 255, 255, 0.25);\ +}\ +.newPostsMarker:not(#quote-preview) {\ + box-shadow: 0 3px red;\ +}\ +#toggleMsgBtn {\ + float: left;\ + margin-bottom: 6px;\ +}\ +.panelHeader {\ + font-weight: bold;\ + font-size: 16px;\ + text-align: center;\ + margin-bottom: 5px;\ + margin-top: 5px;\ + padding-bottom: 5px;\ + border-bottom: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.yotsuba_new .panelHeader {\ + border-bottom: 1px solid #d9bfb7;\ +}\ +.yotsuba_b_new .panelHeader {\ + border-bottom: 1px solid #b7c5d9;\ +}\ +.tomorrow .panelHeader {\ + border-bottom: 1px solid #111;\ +}\ +.panelHeader .panelCtrl {\ + position: absolute;\ + right: 5px;\ + top: 5px;\ +}\ +.UIMenu,\ +.UIPanel {\ + position: fixed;\ + width: 100%;\ + height: 100%;\ + top: 0;\ + left: 0;\ + z-index: 100002;\ +}\ +.UIPanel {\ + line-height: 14px;\ + font-size: 14px;\ + background-color: rgba(0, 0, 0, 0.25);\ +}\ +.UIPanel:after {\ + display: inline-block;\ + height: 100%;\ + vertical-align: middle;\ + content: "";\ +}\ +.UIPanel > div {\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + display: inline-block;\ + height: auto;\ + max-height: 100%;\ + position: relative;\ + width: 400px;\ + left: 50%;\ + margin-left: -200px;\ + overflow: auto;\ + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\ + vertical-align: middle;\ +}\ +#settingsMenu > div {\ + top: 25px;;\ + vertical-align: top;\ + max-height: 85%;\ +}\ +.extPanel input[type="text"],\ +.extPanel textarea {\ + border: 1px solid #AAA;\ + outline: none;\ +}\ +.UIPanel .center {\ + margin-bottom: 5px;\ +}\ +.UIPanel button {\ + display: inline-block;\ + margin-right: 5px;\ +}\ +.UIPanel code {\ + background-color: #eee;\ + color: #000000;\ + padding: 1px 4px;\ + font-size: 12px;\ +}\ +.UIPanel ul {\ + list-style: none;\ + padding: 0;\ + margin: 0 0 10px;\ +}\ +.UIPanel .export-field {\ + width: 385px;\ +}\ +#settingsMenu label input {\ + margin-right: 5px;\ +}\ +.tomorrow #settingsMenu ul {\ + border-bottom: 1px solid #282a2e;\ +}\ +.settings-off {\ + padding-left: 3px;\ +}\ +.settings-cat-lbl {\ + font-weight: bold;\ + margin: 10px 0 5px;\ + padding-left: 5px;\ +}\ +.settings-cat-lbl img {\ + vertical-align: text-bottom;\ + margin-right: 5px;\ + cursor: pointer;\ + width: 18px;\ + height: 18px;\ +}\ +.settings-tip {\ + font-size: 0.85em;\ + margin: 2px 0 5px 0;\ + padding-left: 23px;\ +}\ +#settings-exp-all {\ + padding-left: 7px;\ + text-align: center;\ +}\ +#settingsMenu .settings-cat {\ + display: none;\ + margin-left: 3px;\ +}\ +#tex-preview-cnt .extPanel { width: 600px; margin-left: -300px; }\ +#tex-preview-cnt textarea,\ +#customCSSMenu textarea {\ + display: block;\ + max-width: 100%;\ + min-width: 100%;\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + height: 200px;\ + margin: 0 0 5px;\ + font-family: monospace;\ +}\ +#tex-preview-cnt textarea { height: 75px; }\ +#output-tex-preview {\ + min-height: 75px;\ + white-space: pre;\ + padding: 0 3px;\ + -moz-box-sizing: border-box; box-sizing: border-box;\ +}\ +#tex-protip { font-size: 11px; margin: 5px 0; text-align: center; }\ +a.tex-logo sub { pointer-events: none; }\ +#customCSSMenu .right,\ +#settingsMenu .right {\ + margin-top: 2px;\ +}\ +#settingsMenu label {\ + display: inline-block;\ + user-select: none;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ +}\ +#filtersHelp > div {\ + width: 600px;\ + left: 50%;\ + margin-left: -300px;\ +}\ +#filtersHelp h4 {\ + font-size: 15px;\ + margin: 20px 0 0 10px;\ +}\ +#filtersHelp h4:before {\ + content: "»";\ + margin-right: 3px;\ +}\ +#filtersHelp ul {\ + padding: 0;\ + margin: 10px;\ +}\ +#filtersHelp li {\ + padding: 3px 0;\ + list-style: none;\ +}\ +#filtersMenu table {\ + width: 100%;\ +}\ +#filtersMenu th {\ + font-size: 12px;\ +}\ +#filtersMenu tbody {\ + text-align: center;\ +}\ +#filtersMenu select,\ +#filtersMenu .fPattern,\ +#filtersMenu .fBoards,\ +#palette-custom-input {\ + padding: 1px;\ + font-size: 11px;\ +}\ +#filtersMenu select {\ + width: 75px;\ +}\ +#filtersMenu tfoot td {\ + padding-top: 10px;\ +}\ +#keybindsHelp li {\ + padding: 3px 5px;\ +}\ +.fPattern {\ + width: 110px;\ +}\ +.fBoards {\ + width: 25px;\ +}\ +.fColor {\ + width: 60px;\ +}\ +.fDel {\ + font-size: 16px;\ +}\ +.filter-preview {\ + cursor: pointer;\ + margin-left: 3px;\ +}\ +#quote-preview iframe,\ +#quote-preview .filter-preview {\ + display: none;\ +}\ +.post-hidden .extButton,\ +.post-hidden:not(#quote-preview) .postInfo {\ + opacity: 0.5;\ +}\ +.post-hidden:not(.thread) .postInfo {\ + padding-left: 5px;\ +}\ +.post-hidden:not(#quote-preview) input,\ +.post-hidden:not(#quote-preview) .replyContainer,\ +.post-hidden:not(#quote-preview) .summary,\ +.post-hidden:not(#quote-preview) .op .file,\ +.post-hidden:not(#quote-preview) .file,\ +.post-hidden .wbtn,\ +.post-hidden .postNum span,\ +.post-hidden:not(#quote-preview) .backlink,\ +div.post-hidden:not(#quote-preview) div.file,\ +div.post-hidden:not(#quote-preview) blockquote.postMessage {\ + display: none;\ +}\ +.click-me {\ + border-radius: 5px;\ + margin-top: 5px;\ + padding: 2px 5px;\ + position: absolute;\ + font-weight: bold;\ + z-index: 2;\ + white-space: nowrap;\ +}\ +.yotsuba_new .click-me,\ +.futaba_new .click-me {\ + color: #800000;\ + background-color: #F0E0D6;\ + border: 2px solid #D9BFB7;\ +}\ +.yotsuba_b_new .click-me,\ +.burichan_new .click-me {\ + color: #000;\ + background-color: #D6DAF0;\ + border: 2px solid #B7C5D9;\ +}\ +.tomorrow .click-me {\ + color: #C5C8C6;\ + background-color: #282A2E;\ + border: 2px solid #111;\ +}\ +.photon .click-me {\ + color: #333;\ + background-color: #ddd;\ + border: 2px solid #ccc;\ +}\ +.click-me:before {\ + content: "";\ + border-width: 0 6px 6px;\ + border-style: solid;\ + left: 50%;\ + margin-left: -6px;\ + position: absolute;\ + width: 0;\ + height: 0;\ + top: -6px;\ +}\ +.yotsuba_new .click-me:before,\ +.futaba_new .click-me:before {\ + border-color: #D9BFB7 transparent;\ +}\ +.yotsuba_b_new .click-me:before,\ +.burichan_new .click-me:before {\ + border-color: #B7C5D9 transparent;\ +}\ +.tomorrow .click-me:before {\ + border-color: #111 transparent;\ +}\ +.photon .click-me:before {\ + border-color: #ccc transparent;\ +}\ +.click-me:after {\ + content: "";\ + border-width: 0 4px 4px;\ + top: -4px;\ + display: block;\ + left: 50%;\ + margin-left: -4px;\ + position: absolute;\ + width: 0;\ + height: 0;\ +}\ +.yotsuba_new .click-me:after,\ +.futaba_new .click-me:after {\ + border-color: #F0E0D6 transparent;\ + border-style: solid;\ +}\ +.yotsuba_b_new .click-me:after,\ +.burichan_new .click-me:after {\ + border-color: #D6DAF0 transparent;\ + border-style: solid;\ +}\ +.tomorrow .click-me:after {\ + border-color: #282A2E transparent;\ + border-style: solid;\ +}\ +.photon .click-me:after {\ + border-color: #DDD transparent;\ + border-style: solid;\ +}\ +#image-hover {\ + position: fixed;\ + max-width: 100%;\ + max-height: 100%;\ + top: 0px;\ + right: 0px;\ + z-index: 9002;\ +}\ +.thread-stats {\ + float: right;\ + margin-right: 5px;\ + cursor: default;\ +}\ +.compact .thread {\ + max-width: 75%;\ +}\ +.dotted {\ + text-decoration: none;\ + border-bottom: 1px dashed;\ +}\ +.linkfade {\ + opacity: 0.5;\ +}\ +#quote-preview .linkfade {\ + opacity: 1.0;\ +}\ +kbd {\ + background-color: #f7f7f7;\ + color: black;\ + border: 1px solid #ccc;\ + border-radius: 3px 3px 3px 3px;\ + box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset;\ + font-family: monospace;\ + font-size: 11px;\ + line-height: 1.4;\ + padding: 0 5px;\ +}\ +.deleted {\ + opacity: 0.66;\ +}\ +div.collapseWebm { text-align: center; margin-top: 10px; }\ +.noPictures a.fileThumb img:not(.expanded-thumb) {\ + opacity: 0;\ +}\ +.noPictures.futaba_new a.fileThumb,\ +.noPictures.yotsuba_new a.fileThumb {\ + border: 1px solid #800;\ +}\ +.noPictures.burichan_new a.fileThumb,\ +.noPictures.yotsuba_b_new a.fileThumb {\ + border: 1px solid #34345C;\ +}\ +.noPictures.tomorrow a.fileThumb:not(.expanded-thumb) {\ + border: 1px solid #C5C8C6;\ +}\ +.noPictures.photon a.fileThumb:not(.expanded-thumb) {\ + border: 1px solid #004A99;\ +}\ +.spinner {\ + margin-top: 2px;\ + padding: 3px;\ + display: table;\ +}\ +#settings-presets {\ + position: relative;\ + top: -1px;\ +}\ +#colorpicker { \ + position: fixed;\ + text-align: center;\ +}\ +.colorbox {\ + font-size: 10px;\ + width: 16px;\ + height: 16px;\ + line-height: 17px;\ + display: inline-block;\ + text-align: center;\ + background-color: #fff;\ + border: 1px solid #aaa;\ + text-decoration: none;\ + color: #000;\ + cursor: pointer;\ + vertical-align: top;\ +}\ +#palette-custom-input {\ + vertical-align: top;\ + width: 45px;\ + margin-right: 2px;\ +}\ +#qrDummyFile {\ + float: left;\ + margin-right: 5px;\ + width: 220px;\ + cursor: default;\ + -moz-user-select: none;\ + -webkit-user-select: none;\ + -ms-user-select: none;\ + user-select: none;\ + white-space: nowrap;\ + text-overflow: ellipsis;\ + overflow: hidden;\ +}\ +#qrDummyFileLabel {\ + margin-left: 3px;\ +}\ +.depageNumber {\ + position: absolute;\ + right: 5px;\ +}\ +.depagerEnabled .depagelink {\ + font-weight: bold;\ +}\ +.depagerEnabled strong {\ + font-weight: normal;\ +}\ +.depagelink {\ + display: inline-block;\ + padding: 4px 0;\ + cursor: pointer;\ + text-decoration: none;\ +}\ +.burichan_new .depagelink,\ +.futaba_new .depagelink {\ + text-decoration: underline;\ +}\ +#customMenuBox {\ + margin: 0 auto 5px auto;\ + width: 385px;\ + display: block;\ +}\ +.preview-summary {\ + display: block;\ +}\ +#swf-embed-header {\ + padding: 0 0 0 3px;\ + font-weight: normal;\ + height: 20px;\ + line-height: 20px;\ +}\ +.yotsuba_new #swf-embed-header,\ +.yotsuba_b_new #swf-embed-header {\ + height: 18px;\ + line-height: 18px;\ +}\ +#swf-embed-close {\ + position: absolute;\ + right: 0;\ + top: 1px;\ +}\ +#qr-painter-ctrl { text-align: center; }\ +#qr-painter-ctrl label { margin-right: 4px; }\ +.open-qr-wrap {\ + text-align: center;\ + width: 200px;\ + position: absolute;\ + margin-left: 50%;\ + left: -100px;\ +}\ +.postMenuBtn {\ + margin-left: 5px;\ + text-decoration: none;\ + line-height: 1em;\ + display: inline-block;\ + -webkit-transition: -webkit-transform 0.1s;\ + -moz-transition: -moz-transform 0.1s;\ + transition: transform 0.1s;\ + width: 1em;\ + height: 1em;\ + text-align: center;\ + outline: none;\ + opacity: 0.8;\ +}\ +.postMenuBtn:hover{\ + opacity: 1;\ +}\ +.yotsuba_new .postMenuBtn,\ +.futaba_new .postMenuBtn {\ + color: #000080;\ +}\ +.tomorrow .postMenuBtn {\ + color: #5F89AC;\ +}\ +.tomorrow .postMenuBtn:hover {\ + color: #81a2be;\ +}\ +.photon .postMenuBtn {\ + color: #FF6600;\ +}\ +.photon .postMenuBtn:hover {\ + color: #FF3300;\ +}\ +.menuOpen {\ + -webkit-transform: rotate(90deg);\ + -moz-transform: rotate(90deg);\ + -ms-transform: rotate(90deg);\ + transform: rotate(90deg);\ +}\ +.settings-sub label:before {\ + border-bottom: 1px solid;\ + border-left: 1px solid;\ + content: " ";\ + display: inline-block;\ + height: 8px;\ + margin-bottom: 5px;\ + width: 8px;\ +}\ +.settings-sub {\ + margin-left: 25px;\ +}\ +.settings-tip.settings-sub {\ + padding-left: 32px;\ +}\ +.centeredThreads .opContainer {\ + display: block;\ +}\ +.centeredThreads .postContainer {\ + margin: auto;\ + width: 75%;\ +}\ +.centeredThreads .sideArrows {\ + display: none;\ +}\ +.centre-exp {\ + width: auto !important;\ + clear: both;\ +}\ +.centeredThreads .summary {\ + margin-left: 12.5%;\ + display: block;\ +}\ +.centre-exp div.op{\ + display: table;\ +}\ +#yt-preview { position: absolute; }\ +#yt-preview img { display: block; }\ +.autohide-nav { transition: top 0.2s ease-in-out }\ +#feedback {\ + position: fixed;\ + top: 10px;\ + text-align: center;\ + width: 100%;\ + z-index: 9999;\ +}\ +.feedback-notify,\ +.feedback-error {\ + border-radius: 5px;\ + cursor: pointer;\ + color: #fff;\ + padding: 3px 6px;\ + font-size: 16px;\ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);\ + text-shadow: 0 1px rgba(0, 0, 0, 0.2);\ +}\ +.feedback-error { background-color: #C41E3A; }\ +.feedback-notify { background-color: #00A550; }\ +.boardSelect .custom-menu-ctrl, .boardSelect .customBoardList { margin-left: 10px; }\ +.boardSelect .custom-menu-ctrl { display: none; }\ +.boardSelect:hover .custom-menu-ctrl { display: inline; }\ +.persistentNav .boardList a, .persistentNav .customBoardList a, #boardNavMobile .boardSelect a { text-decoration: none; }\ +@media only screen and (max-width: 480px) {\ +.thread-stats { float: none; text-align: center; }\ +.ts-replies:before { content: "Replies: "; }\ +.ts-images:before { content: "Images: "; }\ +.ts-ips:before { content: "Posters: "; }\ +.ts-page:before { content: "Page: "; }\ +#threadWatcher {\ + max-width: none;\ + padding: 3px 0;\ + left: 0;\ + width: 100%;\ + border-left: none;\ + border-right: none;\ +}\ +#watchList {\ + padding: 0 10px;\ +}\ +div.post div.postInfoM span.nameBlock { clear: none }\ +.btn-row {\ + margin-top: 5px;\ +}\ +.image-expanded .mFileName {\ + display: block;\ + margin-bottom: 2px;\ +}\ +.mFileName { display: none }\ +.mobile-report {\ + float: right;\ + font-size: 11px;\ + margin-bottom: 3px;\ + margin-left: 10px;\ +}\ +.mobile-report:after {\ + content: "]";\ +}\ +.mobile-report:before {\ + content: "[";\ +}\ +.nws .mobile-report:after {\ + color: #800000;\ +}\ +.nws .mobile-report:before {\ + color: #800000;\ +}\ +.ws .mobile-report {\ + color: #34345C;\ +}\ +.nws .mobile-report {\ + color:#0000EE;\ +}\ +.reply .mobile-report {\ + margin-right:5px;\ +}\ +.postLink .mobileHideButton {\ + margin-right: 3px;\ +}\ +.board .mobile-hr-hidden {\ + margin-top: 10px !important;\ +}\ +.board > .mobileHideButton {\ + margin-top: -20px !important;\ +}\ +.board > .mobileHideButton:first-child {\ + margin-top: 10px !important;\ +}\ +.extButton.threadHideButton {\ + float: none;\ + margin: 0;\ + margin-bottom: 5px;\ +}\ +.mobile-post-hidden {\ + display: none;\ +}\ +#toggleMsgBtn {\ + display: none;\ +}\ +.mobile-tu-status {\ + height: 20px;\ + line-height: 20px;\ +}\ +.mobile-tu-show {\ + width: 150px;\ + margin: auto;\ + display: block;\ + text-align: center;\ +}\ +.button input {\ + margin: 0 3px 0 0;\ + position: relative;\ + top: -2px;\ + border-radius: 0;\ + height: 10px;\ + width: 10px;\ +}\ +.UIPanel > div {\ + width: 320px;\ + margin-left: -160px;\ +}\ +.UIPanel .export-field {\ + width: 300px;\ +}\ +.yotsuba_new #quote-preview.highlight,\ +#quote-preview {\ + border-width: 1px !important;\ +}\ +.yotsuba_new #quote-preview.highlight {\ + border-color: #D9BFB7 !important;\ +}\ +#quickReply input[type="text"],\ +#quickReply textarea,\ +.extPanel input[type="text"],\ +.extPanel textarea {\ + font-size: 16px;\ +}\ +#quickReply {\ + position: absolute;\ + left: 50%;\ + margin-left: -154px;\ +}\ +.m-dark .button {\ + background-color: rgb(27,28,30);\ + background-image: url("//s.4cdn.org/image/buttonfade-dark.png");\ + background-repeat: repeat-x;\ + border: 1px solid #282A2E;\ +}\ +.depaged-ad { margin-top: -25px; margin-bottom: -25px; }\ +.depageNumber { font-size: 10px; margin-top: -21px; }\ +.m-dark a, .m-dark div#absbot a { color: #81A2BE !important; }\ +.m-dark a:hover { color: #5F89AC !important; }\ +.m-dark .button a, .m-dark .button:hover, .m-dark .button { color: #707070 !important; }\ +.m-dark #boardNavMobile { background-color: #1D1F21; border-bottom: 2px solid #282A2E; }\ +body.m-dark { background: #1D1F21 none; color: #C5C8C6; }\ +.m-dark #globalToggle {\ + background-color: #FFADAD;\ + background-image: url("//s.4cdn.org/image/buttonfade-red.png");\ + border: 1px solid #C45858;\ + color: #880000 !important;\ +}\ +.m-dark .boardTitle { color: #C5C8C6; }\ +.m-dark .mobile-report { color: #81a2be !important; }\ +.m-dark .mobile-report:after,\ +.m-dark .mobile-report:before { color: #1d1f21 !important; }\ +.m-dark hr, .m-dark div.board > hr { border-top: 1px solid #282A2E; }\ +.m-dark div.opContainer,\ +.m-dark div.reply { background-color: #282A2E; }\ +.m-dark .preview { background-color: #282A2E; border: 1px solid #333 !important; }\ +.m-dark div.post div.postInfoM { background-color: #212326; border-bottom: 1px solid #2D2F33; }\ +.m-dark div.postLink,\ +.m-dark .backlink.mobile { background-color: #212326; border-top: 1px solid #2D2F33; }\ +.m-dark div.post div.postInfoM span.dateTime,\ +.m-dark div.postLink span.info,\ +.m-dark div.post div.postInfoM span.dateTime a { color: #707070 !important; }\ +.m-dark span.subject { color: #B294BB !important; }\ +.m-dark .highlightPost:not(.op) { background: #3A171C !important; }\ +.m-dark .reply:target, .m-dark .reply.highlight { background: #1D1D21 !important; padding: 2px; }\ +.m-dark .reply:target, .m-dark .reply.highlight { background: #1D1D21 !important; padding: 2px; }\ +}'; + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = css; + document.head.appendChild(style); +}; + +Main.init(); diff --git a/js/extension.min.js b/js/extension.min.js new file mode 100644 index 0000000..2e8e5d3 --- /dev/null +++ b/js/extension.min.js @@ -0,0 +1,6 @@ +var $={};$.id=function(e){return document.getElementById(e)},$.cls=function(e,t){return(t||document).getElementsByClassName(e)},$.byName=function(e){return document.getElementsByName(e)},$.tag=function(e,t){return(t||document).getElementsByTagName(e)},$.el=function(e){return document.createElement(e)},$.qs=function(e,t){return(t||document).querySelector(e)},$.qsa=function(e,t){return(t||document).querySelectorAll(e)},$.extend=function(e,t){for(var a in t)e[a]=t[a]},$.on=function(e,t,a){e.addEventListener(t,a,!1)},$.off=function(e,t,a){e.removeEventListener(t,a,!1)},document.documentElement.classList?($.hasClass=function(e,t){return e.classList.contains(t)},$.addClass=function(e,t){e.classList.add(t)},$.removeClass=function(e,t){e.classList.remove(t)}):($.hasClass=function(e,t){return-1!=(" "+e.className+" ").indexOf(" "+t+" ")},$.addClass=function(e,t){e.className=""===e.className?t:e.className+" "+t},$.removeClass=function(e,t){e.className=(" "+e.className+" ").replace(" "+t+" ","")}),$.get=function(e,t,a){var i,n;if((n=new XMLHttpRequest).open("GET",e,!0),t)for(i in t)n[i]=t[i];if(a)for(i in a)n.setRequestHeader(i,a[i]);return n.send(null),n},$.xhr=function(e,t,a,i){var n,o,r;if((o=new XMLHttpRequest).open(e,t,!0),a)for(n in a)o[n]=a[n];if(i){r=new FormData;for(n in i)r.append(n,i[n]);i=r}else i=null;return o.send(i),o},$.fit=function(e,t,a,i){var n,o,r;return n=e/t,e>a?(o=a,(r=Math.round(o/n))>i&&(r=i,o=Math.round(r*n))):t>i?(r=i,(o=Math.round(r*n))>a&&(o=a,r=Math.round(o/n))):(o=e,r=t),[o,r]},$.ago=function(e){var t,a,i,n;return(t=Date.now()/1e3-e)<1?"moments ago":t<60?(0|t)+" seconds ago":t<3600?(a=0|t/60)>1?a+" minutes ago":"one minute ago":t<86400?(i=(a=0|t/3600)>1?a+" hours":"one hour",(n=0|t/60-60*a)>1&&(i+=" and "+n+" minutes"),i+" ago"):(i=(a=0|t/86400)>1?a+" days":"one day",(n=0|t/3600-24*a)>1&&(i+=" and "+n+" hours"),i+" ago")},$.hash=function(e){var t,a,i=0;for(t=0,a=e.length;t=2?"@2x.gif":".gif",this.icons={admin:o+"adminicon"+n,founder:o+"foundericon"+n,mod:o+"modicon"+n,dev:o+"developericon"+n,manager:o+"managericon"+n,del:o+"filedeleted-res"+n},this.prettify="function"==typeof prettyPrint,this.customSpoiler={},Config.localTime&&((e=(new Date).getTimezoneOffset())?(a=0|(t=Math.abs(e))/60,this.utcOffset="Timezone: UTC"+(e<0?"+":"-")+a+((i=t-60*a)?":"+i:"")):this.utcOffset="Timezone: UTC",this.weekdays=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]),Main.tid&&(this.trackedReplies=this.getTrackedReplies(Main.board,Main.tid),this.trackedReplies?this.touchTrackedReplies(Main.tid):this.trackedReplies={},this.pruneTrackedReplies()),this.postMenuIcon=Main.hasMobileLayout?"...":"\u25b6"},Parser.getTrackedReplies=function(e,t){var a=null;return(a=localStorage.getItem("4chan-track-"+e+"-"+t))&&(a=JSON.parse(a)),a},Parser.saveTrackedReplies=function(e,t){var a="4chan-track-"+Main.board+"-"+e;localStorage.setItem(a,JSON.stringify(t))},Parser.touchTrackedReplies=function(e){var t,a;a="4chan-track-"+Main.board+"-ts",(t=(t=localStorage.getItem(a))?JSON.parse(t):{})[e]=0|Date.now()/1e3,localStorage.setItem(a,JSON.stringify(t))},Parser.pruneTrackedReplies=function(){var e,t,a,i,n,o,r;if(o="4chan-track-"+Main.board+"-",t=localStorage.getItem(o+"ts")){n=259200,i=(a=0|Date.now()/1e3)-n,r=!1,t=JSON.parse(t),Main.tid&&t[Main.tid]&&(t[Main.tid]=a,r=!0);for(e in t)t[e]<=i&&(r=!0,delete t[e],localStorage.removeItem(o+e));if(r){localStorage.removeItem(o+"ts");for(e in t){localStorage.setItem(o+"ts",JSON.stringify(t));break}}}},Parser.parseThreadJSON=function(e){var t;try{t=JSON.parse(e).posts}catch(a){console.log(a),t=[]}return t},Parser.parseCatalogJSON=function(e){var t;try{t=JSON.parse(e)}catch(a){console.log(a),t=[]}return t},Parser.setCustomSpoiler=function(e,t){var a;!this.customSpoiler[e]&&(t=parseInt(t))&&(e==Main.board&&(a=$.cls("imgspoiler")[0])?this.customSpoiler[e]=a.firstChild.src.match(/spoiler(-[a-z0-9]+)\.png$/)[1]:this.customSpoiler[e]="-"+e+(Math.floor(Math.random()*t)+1))},Parser.buildPost=function(e,t,a){var i,n,o,r=null;for(i=0;n=e[i];++i)n.no==a&&(!Config.revealSpoilers&&e[0].custom_spoiler&&Parser.setCustomSpoiler(t,e[0].custom_spoiler),r=Parser.buildHTMLFromJSON(n,t,!1,!0).lastElementChild,Config.IDColor&&(o=$.cls("posteruid",r)[Main.hasMobileLayout?0:1])&&IDColor.applyRemote(o.firstElementChild));return r},Parser.decodeSpecialChars=function(e){return e.replace(/&/g,"&").replace(/"/g,'"').replace(/'/g,"'").replace(/</g,"<").replace(/>/g,">")},Parser.encodeSpecialChars=function(e){return e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")},Parser.onDateMouseOver=function(e){Parser.tipTimeout&&(clearTimeout(Parser.tipTimeout),Parser.tipTimeout=null),Parser.tipTimeout=setTimeout(Tip.show,500,e,$.ago(+e.getAttribute("data-utc")))},Parser.onTipMouseOut=function(){Parser.tipTimeout&&(clearTimeout(Parser.tipTimeout),Parser.tipTimeout=null)},Parser.onUIDMouseOver=function(e){var t;$.hasClass(e.parentNode,"posteruid")&&(Main.tid||(t=e.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode,$.hasClass(t,"tExpanded")))&&(Parser.tipTimeout&&(clearTimeout(Parser.tipTimeout),Parser.tipTimeout=null),Parser.tipTimeout=setTimeout(Parser.showUIDCount,500,e,e.textContent))},Parser.showUIDCount=function(e,t){var a,i,n,o,r;for(o=0,n=$.qsa(".postInfo .hand"),a=0;i=n[a];++a)i.textContent===t&&++o;r=o+" post"+(1!=o?"s":"")+" by this ID",Tip.show(e,r)},Parser.buildHTMLFromJSON=function(e,t,a,i){var n,o,r,s,d,l,c,p,h,u,g,m,f,b,v,C,w,y,x,k=document.createElement("div"),M=!1,T="",S="",R="",E="",N='"',$="",P="",L="",Q="",H="",D="",A="",I="",F="",q="",B="",_="",W="",O="reply",U="",z="",j="",J="",Y=!1;switch(x="//i.4cdn.org/"+t,e.resto?f=e.resto:(M=!0,a&&(e.replies>0?(y=e.replies+" Repl"+(e.replies>1?"ies":"y"),e.images>0&&(y+=" / "+e.images+" Image"+(e.images>1?"s":""))):y="",W='',O="op",_='  [Reply]'),t!=Main.board&&(J='/'+t+"/ "),f=e.no),Main.tid&&t==Main.board?(h="#p"+e.no,u="javascript:quote('"+e.no+"')"):(h="//boards."+$L.d(t)+"/"+t+"/thread/"+f+"#p"+e.no,u="//boards."+$L.d(t)+"/"+t+"/thread/"+f+"#q"+e.no),n=!e.capcode&&e.id?' (ID: '+e.id+") ":"",e.capcode){case"admin_highlight":F=" highlightPost";case"admin":D=' ## Admin',A=" capcodeAdmin",I=' This user is a 4chan Administrator.';break;case"mod":D=' ## Mod',A=" capcodeMod",I=' This user is a 4chan Moderator.';break;case"developer":D=' ## Developer',A=" capcodeDeveloper",I=' This user is a 4chan Developer.';break;case"manager":D=' ## Manager',A=" capcodeManager",I=' This user is a 4chan Manager.';break;case"founder":D=' ## Founder',A=" capcodeAdmin",I=' This user is 4chan\'s Founder.';break;case"verified":D=' ## Verified',A=" capcodeVerified",I=""}if(e.email&&(q='',B=""),d=e.flag_name?' ':e.country_name?' ':"",e.filedeleted?E='
    File deleted.
    ':e.ext&&(g=Parser.decodeSpecialChars(e.filename),L=Q=e.filename+e.ext,g.length>(M?40:30)&&(L=Parser.encodeSpecialChars(g.slice(0,M?35:25))+"(...)"+e.ext,Y=!0),e.tn_w||e.tn_h||".gif"!=e.ext||(e.tn_w=e.w,e.tn_h=e.h),$=e.fsize>=1048576?(0|e.fsize/1048576*100+.5)/100+" M":e.fsize>1024?(0|e.fsize/1024+.5)+" K":e.fsize+" ",e.spoiler?Config.revealSpoilers?s=L:(s="Spoiler Image",N='" title="'+Q+'"',P=" imgspoiler",o="//s.4cdn.org/image/spoiler"+(Parser.customSpoiler[t]||"")+".png",e.tn_w=100,e.tn_h=100,!0):s=L,o||(o="//i.4cdn.org/"+t+"/"+e.tim+"s.jpg"),T=".pdf"==e.ext?"PDF":e.w+"x"+e.h,"f"!=t?(S=''+$+'B
    '+$+"B "+e.ext.slice(1).toUpperCase()+"
    ",R='
    File: '+s+" ("+$+"B, "+T+")
    "):(r=x+"/"+e.filename+e.ext,T+=", "+e.tag,R='
    File: '+e.filename+".swf ("+$+"B, "+T+")
    "),E='
    '+R+S+"
    "),e.trip&&(H=' '+e.trip+""),l=e.name||"",c=Main.hasMobileLayout&&l.length>30?''+Parser.truncate(l,30)+"(...) ":''+l+" ",M?(e.capcode_replies&&(z=Parser.buildCapcodeReplies(e.capcode_replies,t,e.no)),i&&e.replies&&(m=e.replies+" repl"+(e.replies>1?"ies":"y"),e.images&&(m+=" and "+e.images+" image"+(e.images>1?"s":"")),U=''+m+"."),e.sticky&&(j+='Sticky '),e.closed&&(e.archived?j+='Archived ':j+='Closed '),p=e.sub===undefined?' ':Main.hasMobileLayout&&e.sub.length>30?''+Parser.truncate(e.sub,30)+"(...) ":''+e.sub+" "):p="",k.className="postContainer "+O+"Container",k.id="pc"+e.no,e.xa24&&(k.className+=" p-xa24-"+e.xa24),k.innerHTML=(M?"":'
    >>
    ')+'
    "+(M?E:"")+'"+(M?"":E)+'
    '+(e.com||"")+z+U+"
    "+W,!Main.tid||t!=Main.board)for(w=k.getElementsByClassName("quotelink"),b=0;v=w[b];++b)"/"!=(C=v.getAttribute("href")).charAt(0)&&(v.href="//boards."+$L.d(t)+"/"+t+"/thread/"+f+C);return k},Parser.truncate=function(e,t){return e=e.replace(",",","),e=(e=Parser.decodeSpecialChars(e)).slice(0,t),e=Parser.encodeSpecialChars(e)},Parser.buildCapcodeReplies=function(e,t,a){var i,n,o,r,s,d,l,c;s={admin:"Administrator",mod:"Moderator",developer:"Developer",manager:"Manager"},t!=Main.board?(l="/"+t+"/thread/",c=">>>/"+t+"/"):(l="",c=">>"),r='

    ';for(n in e)for(r+=''+s[n]+" Replies: ",d=e[n],i=0;o=d[i];++i)r+=''+c+o+" ";return r+""},Parser.parseBoard=function(){var e,t=document.getElementsByClassName("thread");for(e=0;t[e];++e)Parser.parseThread(t[e].id.slice(1))},Parser.parseThread=function(e,t,a){var i,n,o,r,s,d,l,c,p,h,u,g;if(r=(o=$.id("t"+e)).getElementsByClassName("post"),!t&&(s=document.getElementById("pi"+e),Main.tid||(Config.filter&&(u=Filter.exec(o,s,document.getElementById("m"+e),e)),Config.threadHiding&&!u&&(Main.hasMobileLayout||((d=document.createElement("span")).innerHTML='H',r[0].insertBefore(d,r[0].firstChild),d.id="sa"+e),ThreadHiding.hidden[e]&&(ThreadHiding.hidden[e]=Main.now,ThreadHiding.hide(e))),ThreadExpansion.enabled&&(c=$.cls("summary",o)[0])&&(l=document.createDocumentFragment(),(p=c.cloneNode(!0)).className="",c.textContent="",(d=document.createElement("img")).className="extButton expbtn",d.title="Expand thread",d.alt="+",d.setAttribute("data-cmd","expand"),d.setAttribute("data-id",e),d.src=Main.icons.plus,l.appendChild(d),l.appendChild(p),(d=document.createElement("span")).style.display="none",d.textContent="Showing all replies.",l.appendChild(d),c.appendChild(l))),Main.tid&&Config.threadWatcher))for(d=document.createElement("img"),ThreadWatcher.watched[h=e+"-"+Main.board]?(d.src=Main.icons.watched,d.setAttribute("data-active","1")):d.src=Main.icons.notwatched,d.className="extButton wbtn wbtn-"+h,d.setAttribute("data-cmd","watch"),d.setAttribute("data-id",e),d.alt="W",d.title="Add to watch list",g=$.cls("navLinks"),i=1;i<3&&(n=g[i]);++i)(l=document.createDocumentFragment()).appendChild(document.createTextNode("[")),l.appendChild(d.cloneNode(!0)),l.appendChild(document.createTextNode("] ")),n.insertBefore(l,n.firstChild);if(n=t?t<0?r.length+t:t:0,a=a?n+a:r.length,Main.isMobileDevice&&Config.quotePreview)for(i=n;i(a?40:30)?o=n[0].slice(0,a?35:25)+"(...)"+n[1]:(o=i.title,i.removeAttribute("title")),i.firstElementChild.innerHTML=o,e.insertBefore(t,e.firstElementChild)},Parser.parsePost=function(e,t){var a,i,n,o,r,s,d,l;a=Main.hasMobileLayout,t?o=document.getElementById("pi"+e):e=(o=e.getElementsByClassName("postInfo")[0]).id.slice(2),Parser.needMsg&&(s=document.getElementById("m"+e)),(n=document.createElement("a")).href="#",n.className="postMenuBtn",n.title="Post menu",n.setAttribute("data-cmd","post-menu"),n.textContent=Parser.postMenuIcon,a?(i=document.getElementById("pim"+e)).insertBefore(n,i.firstElementChild):o.appendChild(n),t&&(e!=t&&(Config.filter&&(d=Filter.exec(o.parentNode,o,s)),!d&&ReplyHiding.hidden[e]&&(ReplyHiding.hidden[e]=Main.now,ReplyHiding.hide(e))),Config.backlinks&&Parser.parseBacklinks(e,t),Main.isOekakiBoard&&Main.tid&&Parser.addOekakiEditLink(e,t)),IDColor.enabled&&(l=$.cls("posteruid",o.parentNode)[a?0:1])&&IDColor.apply(l.firstElementChild),Config.linkify&&Linkify.exec(s),Config.embedSoundCloud&&Media.parseSoundCloud(s),(Config.embedYouTube||a)&&Media.parseYouTube(s),Config.revealSpoilers&&(r=document.getElementById("f"+e))&&(r=r.children[1])&&$.hasClass(r,"imgspoiler")&&Parser.revealImageSpoiler(r),Config.localTime&&(a?(n=o.parentNode.getElementsByClassName("dateTime")[0]).firstChild.nodeValue=Parser.getLocaleDate(new Date(1e3*n.getAttribute("data-utc")))+" ":(n=o.getElementsByClassName("dateTime")[0]).textContent=Parser.getLocaleDate(new Date(1e3*n.getAttribute("data-utc"))))},Parser.addOekakiEditLink=function(e,t){var a,i,n;(a=$.id("fT"+e))&&(n=a.firstElementChild).href&&/\.(png|jpg)$/.test(n.href)&&((i=$.el("small")).innerHTML=' Edit',a.appendChild(i))},Parser.getLocaleDate=function(e){return("0"+(1+e.getMonth())).slice(-2)+"/"+("0"+e.getDate()).slice(-2)+"/"+("0"+e.getFullYear()).slice(-2)+"("+this.weekdays[e.getDay()]+")"+("0"+e.getHours()).slice(-2)+":"+("0"+e.getMinutes()).slice(-2)+":"+("0"+e.getSeconds()).slice(-2)},Parser.parseBacklinks=function(e,t){var a,i,n,o,r,s,d,l,c;if(n=document.getElementById("m"+e).getElementsByClassName("quotelink"))for(o={},a=0;i=n[a];++a)(r=i.getAttribute("href").split("#p"))[1]&&(r[1]==t&&(i.textContent+=" (OP)"),(s=document.getElementById("pi"+r[1]))?o[r[1]]||(o[r[1]]=!0,d=document.createElement("span"),c=Main.tid?"#p"+e:"thread/"+t+"#p"+e,Main.hasMobileLayout?d.innerHTML='>>'+e+' # ':d.innerHTML='>>'+e+" ",(l=document.getElementById("bl_"+r[1]))||((l=document.createElement("div")).id="bl_"+r[1],l.className="backlink",Main.hasMobileLayout&&(l.className="backlink mobile",s=document.getElementById("p"+r[1])),s.appendChild(l)),l.appendChild(d)):Main.tid&&">"!=i.textContent.charAt(2)&&(i.textContent+=" \u2192"))},Parser.buildSummary=function(e,t,a){var i;return t?(t=t+" repl"+(t>1?"ies":"y"),a=a?" and "+a+" image"+(a>1?"s":""):"",(i=document.createElement("span")).className="summary desktop",i.innerHTML=t+a+' omitted. Click here to view.',i):null};var OgvCtrl={ogv:null,cnt:null,ctrl:{},seeking:!1,visible:!1,tick:null,attach:function(e){this.detach(),e.parentNode.appendChild(this.cnt),$.on(e,"mouseup",this.toggleCtrl),this.ogv=e},detach:function(){this.ogv&&(this.ogv.stop(),$.off(this.ogv,"mouseup",this.toggleCtrl),this.ctrl.play.classList.remove("ogv-toggled"),this.ctrl.mute.classList.remove("ogv-toggled"),this.hideCtrl(),this.ogv=null,this.seeking=!1,this.cnt.remove())},init:function(){if(this.cnt)return;let e=$.el("div");e.className="ogv-ctrl";let t=$.el("div");t.className="ogv-btn",t.innerHTML='',$.on(t,"click",this.togglePlay,!1),this.ctrl.play=t,e.appendChild(t),(t=$.el("input")).className="ogv-seek",t.type="range",t.min=0,t.value=0,t.max=100,t.step=.1,$.on(t,"change",this.onSeek,!1),$.on(t,"mousedown",this.toggleSeek,!1),$.on(t,"mouseup",this.toggleSeek,!1),this.ctrl.seek=t,e.appendChild(t),(t=$.el("div")).className="ogv-ts",t.textContent="0:00 / 0:00",this.ctrl.ts=t,e.appendChild(t),(t=$.el("div")).className="ogv-btn",t.innerHTML='',$.on(t,"click",this.toggleMute,!1),this.ctrl.mute=t,e.appendChild(t),(t=$.el("input")).className="ogv-vol",t.type="range",t.min=0,t.value=50,t.step=.1,t.max=100,$.on(t,"input",this.onVolInput,!1),this.ctrl.vol=t,e.appendChild(t),(t=$.el("div")).className="ogv-btn",t.innerHTML='',$.on(t,"click",this.toggleFullscreen,!1),this.ctrl.fs=t,e.appendChild(t),this.cnt=e},onPlayEnd:function(){OgvCtrl.ogv.seekable.length?OgvCtrl.ogv.currentTime=0:OgvCtrl.ogv.stop(),OgvCtrl.ogv.play()},toggleCtrl:function(){OgvCtrl.visible?OgvCtrl.hideCtrl():(OgvCtrl.cnt.style.display="flex",OgvCtrl.setTickTimeout(),OgvCtrl.updateTimes(),OgvCtrl.visible=!0)},hideCtrl:function(){OgvCtrl.cnt.style.display="none",OgvCtrl.clearTickTimeout(),OgvCtrl.visible=!1},toggleSeek:function(){OgvCtrl.seeking=!OgvCtrl.seeking},seekTick:function(){OgvCtrl.setTickTimeout(),OgvCtrl.updateTimes()},setTickTimeout:function(){OgvCtrl.tick=setTimeout(OgvCtrl.seekTick,500)},clearTickTimeout:function(){clearTimeout(OgvCtrl.tick),OgvCtrl.tick=null},updateTimes:function(){if(!OgvCtrl.ogv.duration)return;OgvCtrl.seeking||(OgvCtrl.ctrl.seek.value=(OgvCtrl.ogv.currentTime/OgvCtrl.ogv.duration*100).toFixed(2));let e=Math.floor(OgvCtrl.ogv.duration/60),t=Math.floor(OgvCtrl.ogv.duration-60*e),a=Math.floor(OgvCtrl.ogv.currentTime/60),i=Math.floor(OgvCtrl.ogv.currentTime-60*a);OgvCtrl.ctrl.ts.textContent=`${a}:${i.toString().padStart(2,"0")} / ${e}:${t.toString().padStart(2,"0")}`},togglePlay:function(){OgvCtrl.ogv.paused?OgvCtrl.ogv.play():OgvCtrl.ogv.pause(),OgvCtrl.ctrl.play.classList.toggle("ogv-toggled")},onSeek:function(){OgvCtrl.ogv.currentTime=this.value/100*OgvCtrl.ogv.duration},toggleMute:function(){OgvCtrl.ogv.muted=!OgvCtrl.ogv.muted,OgvCtrl.ctrl.mute.classList.toggle("ogv-toggled")},onVolInput:function(){OgvCtrl.ogv.volume=this.value/100},toggleFullscreen:function(){document.fullscreenElement?document.exitFullscreen():OgvCtrl.ogv.parentNode.requestFullscreen(),OgvCtrl.ctrl.fs.classList.toggle("ogv-toggled")}},PostMenu={activeBtn:null};PostMenu.open=function(e){var t,a,i,n,o,r,s,d,l,c,p;PostMenu.activeBtn!=e?(PostMenu.close(),i=e.parentNode.id.replace(/^[0-9]*[^0-9]+/,""),c=!(n=e.parentNode.getAttribute("data-board"))&&!!$.id("t"+i),a='
    • Report post
    • ',c?(Config.threadHiding&&!Main.tid&&(a+='
    • '+($.hasClass($.id("t"+i),"post-hidden")?"Unhide":"Hide")+" thread
    • "),Config.threadWatcher&&(a+='
    • '+(ThreadWatcher.watched[i+"-"+Main.board]?"Remove from":"Add to")+" watch list
    • ")):(r=$.id("pc"+i))&&(a+='
    • '+($.hasClass(r,"post-hidden")?"Unhide":"Hide")+" post
    • "),Main.hasMobileLayout&&(a+='
    • Delete post
    • '),(p=$.id("fT"+i))&&(r=$.cls("fileThumb",p.parentNode)[0])&&(s=/(gif|jpg|png)$/.test(r.href)?r.href:"http://i.4cdn.org/"+Main.board+"/"+r.href.match(/\/([0-9]+)m?\..+$/)[1]+"s.jpg",Main.hasMobileLayout?a+='
    • Delete file
    • Open original file
    • Search image on Google
    • Search image on Yandex
    • Search image on SauceNAO
    • ':a+='
    • Image search »
    • '),Config.filter&&(a+='
    • Filter selected text
    • '),(t=document.createElement("div")).id="post-menu",t.className="dd-menu",t.innerHTML=a+"
    ",o=e.getBoundingClientRect(),t.style.top=o.bottom+3+window.pageYOffset+"px",document.addEventListener("click",PostMenu.close,!1),$.addClass(e,"menuOpen"),PostMenu.activeBtn=e,UA.dispatchEvent("4chanPostMenuReady",{postId:i,isOP:c,node:t.firstElementChild}),document.body.appendChild(t),(d=o.left+window.pageXOffset)>(l=$.docEl.clientWidth-t.offsetWidth)-75&&(t.className+=" dd-menu-left"),d>l&&(d=l),t.style.left=d+"px"):PostMenu.close()},PostMenu.close=function(){var e;(e=$.id("post-menu"))&&(e.parentNode.removeChild(e),document.removeEventListener("click",PostMenu.close,!1),$.removeClass(PostMenu.activeBtn,"menuOpen"),PostMenu.activeBtn=null)};var Search={xhr:null,pageSize:10,maxPages:10,init:function(){var e;(e=$.id("g-search-form"))&&($.on(e,"submit",Search.onSearch),$.on(window,"hashchange",Search.onHashChanged),"boards.4channel.org"==location.host&&Search.initSelector(),Search.initFromURL(!0))},initSelector:function(){var e,t,a,i;for(e=(a=(i=$.id("js-sf-bf")).options).length-1;e>=0;e--)""!==(t=a[e]).value&&"4chan.org"!==$L.d(t.value)||i.removeChild(t)},initFromURL:function(e){var t,a,i,n,o,r;if((t=Search).query="",t.board="",t.offset=0,""!==(a=window.location.hash)&&a.length<=512&&((a=a.split("/").slice(1))[0]?t.query=decodeURIComponent(a[0]):t.query="",t.board=a[1]||"",a[1]&&("all"===a[1]?t.board="":t.board=a[1]),t.offset=t.pageToOffset(0|a[2])),!e||""!==t.query){for($.id("js-sf-qf").value=t.query,(n=$.id("js-sf-bf")).selectedIndex=0,i=0,o=n.options;r=o[i];++i)if(r.value===t.board){n.selectedIndex=i;break}0===n.selectedIndex&&""!==t.board&&(t.board=""),Search.exec(t.query,t.board,t.offset)}},onHashChanged:function(){Search.initFromURL()},pageToOffset:function(e){return(e<1||e>Search.maxPages)&&(e=1),(e-1)*Search.pageSize},offsetToPage:function(e){var t=e/Search.pageSize+1;return(t<1||t>Search.maxPages)&&(t=1),t},updateURL:function(){var e,t=[];""!==(e=Search).query&&(t.push(encodeURIComponent(e.query)),e.offset>0?(e.board&&""!==e.board?t.push(e.board):t.push("all"),t.push(Search.offsetToPage(e.offset))):e.board&&t.push(e.board)),t.length?window.history.replaceState(null,"","#/"+t.join("/")):window.history.replaceState(null,"",window.location.href.replace(/#.*$/,""))},onSearch:function(e){var t,a;e&&e.preventDefault(),Search.query=t=$.id("js-sf-qf").value,Search.board=a=$.id("js-sf-bf").value,Search.exec(t,a,0)},exec:function(e,t,a){var i,n=[];(i=Search).toggleSpinner(!1),i.updateCtrl(!1),i.xhr&&(i.xhr.abort(),i.xhr=null),""!==e&&(n.push("q="+encodeURIComponent(e)),""!==t&&n.push("b="+t),a&&n.push("o="+(0|a)),n=n.join("&"),i.query=e,i.board=t,i.offset=a,i.updateURL(),i.toggleSpinner(!0),i.xhr=$.get("https://find."+location.host.replace(/^boards\./,"")+"/api?"+n,{onload:i.onLoad,onerror:i.onError,withCredentials:!0}))},onPageClick:function(){var e;e=+this.getAttribute("data-o"),Search.exec(Search.query,Search.board,e)},onLoad:function(){var e;Search.toggleSpinner(!1);try{e=JSON.parse(this.responseText)}catch(t){return Search.showError("Something went wrong."),void console.log(t)}Search.buildResults(e)},onError:function(){Search.toggleSpinner(!1),Search.showError("Connection error.")},updateCtrl:function(e,t){var a,i,n,o,r;!1!==e?((a=$.id("js-sf-pl"))&&a.parentNode.removeChild(a),(a=$.el("div")).id="js-sf-pl",Main.hasMobileLayout?a.className="mPagelist mobile":a.className="pagelist desktop",t=+t,e=+e,(o=Math.ceil(t/Search.pageSize))>Search.maxPages&&(o=Search.maxPages),(r=e/Search.pageSize+1)>1&&((i=$.el("div")).className="prev",Main.hasMobileLayout?((n=$.el("a")).className="button",n.textContent="Previous"):((n=$.el("input")).type="button",n.value="Previous"),n.setAttribute("data-o",e-Search.pageSize),$.on(n,"click",Search.onPageClick),i.appendChild(n),a.appendChild(i)),(i=$.el("div")).className="pages",i.textContent="Page "+r+" / "+o,a.appendChild(i),r'+e+"
    ":""},showError:function(e){Search.showStatus(e,"error")},toggleSpinner:function(e){var t=$.id("js-sf-btn");e?(t.disabled=!0,Search.showStatus("Searching\u2026","spnr")):(t.disabled=!1,Search.showStatus(!1))},buildResults:function(e){var t,a,i,n,o,r,s,d;if((o=e.threads).length<1)return Search.showError("Nothing found."),void Search.updateCtrl(!1);for((s=$.cls("board")[0]).textContent="",t=0;r=o[t];++t){for(i=r.posts[0], +(n=$.el("div")).id="t"+i.no,n.className="thread",n.appendChild(Parser.buildHTMLFromJSON(i,r.board,!0)),a=1;d=r.posts[a];++a)n.appendChild(Parser.buildHTMLFromJSON(d,r.board));s.appendChild(n),s.appendChild($.el("hr"))}Search.updateCtrl(e.offset,e.nhits)}},Depager={};Depager.init=function(){var e,t,a;if(this.isLoading=!1,this.isEnabled=!1,this.isComplete=!1,this.threadsLoaded=!1,this.threadQueue=[],this.debounce=100,this.threshold=350,this.adId="azk53379",this.adZones=[16258,16260],this.boardHasAds=!!$.id(this.adId),this.boardHasAds&&(e=$.cls("ad-plea"),this.adPlea=e[e.length-1]),Main.hasMobileLayout){if(!(e=$.cls("next")[1]))return;(e=e.firstElementChild).textContent="Load More",e.className+=" m-depagelink",e.setAttribute("data-cmd","depage")}else{if(!(e=$.cls("prev")[0]))return;e.innerHTML='[All]',e=e.firstElementChild}Config.alwaysDepage?(this.isEnabled=!0,e.parentNode.parentNode.className+=" depagerEnabled",Depager.bindHandlers(),!Main.hasMobileLayout&&(a=$.cls("board")[0])&&((t=document.createElement("span")).className="depageNumber",t.textContent="Page 1",a.insertBefore(t,a.firstElementChild))):e.setAttribute("data-cmd","depage")},Depager.onScroll=function(){document.documentElement.scrollHeight<=Math.ceil(window.innerHeight+window.pageYOffset)+Depager.threshold&&(Depager.threadsLoaded?Depager.renderNext():Depager.depage())},Depager.trackPageview=function(e){var t;try{window._gat&&(t="/"+Main.board+"/"+e,window._gat._getTrackerByName()._trackPageview(t)),window.__qc&&(window.__qc.qpixelsent=[],window._qevents.push({qacct:window.__qc.qopts.qacct}),window.__qc.firepixels())}catch(a){console.log(a)}},Depager.insertAd=function(e,t,a,i){var n,o,r;Depager.boardHasAds&&window.ados_add_placement&&(i&&(n=(r=$.cls("bottomad"))[r.length-1],(o=document.createElement("div")).id="azkDepage"+(e+1),n.appendChild(o),window.ados_add_placement(3536,18130,o.id,4).setZone(a)),(n=document.createElement("div")).className="bottomad center depaged-ad",2==e?o=$.id(Depager.adId):(o=document.createElement("div")).id="azkDepage"+e,n.appendChild(o),t.appendChild(n),Depager.adPlea&&t.appendChild(Depager.adPlea.cloneNode(!0)),t.appendChild(document.createElement("hr")),2!=e&&window.ados_add_placement(3536,18130,o.id,4).setZone(a))},Depager.loadAds=function(){Depager.boardHasAds&&window.ados_load&&window.ados_load()},Depager.renderNext=function(){var e,t,a,i,n,o,r,s,d,l,c,p,h,u,g,m,f;if(c=[],p=window.pageYOffset,t=document.createDocumentFragment(),m=Depager.threadQueue.shift()){for(o=m.threads,g=m.page,f=!Depager.threadQueue.length,Depager.insertAd(g,t,m.adZone,f),(e=document.createElement("span")).className="depageNumber",e.textContent="Page "+g,t.appendChild(e),i=0;r=o[i];++i)if(!$.id("t"+r.no)){if((d=document.createElement("div")).id="t"+r.no,d.className="thread",d.appendChild(Parser.buildHTMLFromJSON(r,Main.board,!0)),(s=Parser.buildSummary(r.no,r.omitted_posts,r.omitted_images))&&d.appendChild(s),r.replies)for(h=r.last_replies,n=0;l=h[n];++n)d.appendChild(Parser.buildHTMLFromJSON(l,Main.board));t.appendChild(d),t.appendChild(document.createElement("hr")),c.push(r.no)}for(f&&(Depager.unbindHandlers(),Depager.isComplete=!0,Depager.setStatus("disabled")),(u=$.cls("board")[0]).insertBefore(t,u.lastElementChild),Depager.trackPageview(g),Depager.loadAds(),a=0;r=c[a];++a)Parser.parseThread(r);window.scrollTo(0,p)}},Depager.bindHandlers=function(){window.addEventListener("scroll",Depager.onScroll,!1),window.addEventListener("resize",Depager.onScroll,!1)},Depager.unbindHandlers=function(){window.removeEventListener("scroll",Depager.onScroll,!1),window.removeEventListener("resize",Depager.onScroll,!1)},Depager.setStatus=function(e){var t,a,i,n,o;if(Main.hasMobileLayout?(i=$.cls("m-depagelink"),o="Load More"):(i=$.cls("depagelink"),o="All"),i.length)if("enabled"==e)for(t=0;a=i[t];++t)a.textContent=o,n=a.parentNode.parentNode,$.hasClass(n,"depagerEnabled")||$.addClass(n,"depagerEnabled");else if("loading"==e)for(t=0;a=i[t];++t)a.textContent="Loading\u2026";else if("disabled"==e)for(t=0;a=i[t];++t)Main.hasMobileLayout?a.parentNode.parentNode.removeChild(a.parentNode):(a.textContent=o,$.removeClass(a.parentNode.parentNode,"depagerEnabled"));else if("error"==e)for(t=0;a=i[t];++t)a.textContent="Error",a.removeAttribute("title"),a.removeAttribute("data-cmd"),$.removeClass(a.parentNode.parentNode,"depagerEnabled")},Depager.toggle=function(){Depager.isLoading||Depager.isComplete||(Depager.isEnabled?Depager.disable():Depager.enable(),Depager.isEnabled=!Depager.isEnabled)},Depager.enable=function(){Depager.bindHandlers(),Depager.setStatus("enabled"),Depager.onScroll()},Depager.disable=function(){Depager.unbindHandlers(),Depager.setStatus("disabled")},Depager.depage=function(){Depager.isLoading||(Depager.isLoading=!0,$.get("//a.4cdn.org/"+Main.board+"/catalog.json",{onload:Depager.onLoad,onerror:Depager.onError}),Depager.setStatus("loading"))},Depager.onLoad=function(){var e,t,a,i,n;if(Depager.isLoading=!1,Depager.threadsLoaded=!0,200==this.status){for(Depager.setStatus("enabled"),Config.alwaysDepage||Depager.bindHandlers(),e=Parser.parseCatalogJSON(this.responseText),i=Depager.threadQueue,n=0,t=1;a=e[t];++t)a.adZone=Depager.adZones[n],i.push(a),n=n?0:1;Depager.renderNext()}else 404==this.status?(Depager.unbindHandlers(),Depager.setStatus("error")):(Depager.unbindHandlers(),console.log("Error: "+this.status),Depager.setStatus("error"))},Depager.onError=function(){Depager.isLoading=!1,Depager.unbindHandlers(),console.log("Error: "+this.status),Depager.setStatus("error")};var QuoteInline={};QuoteInline.isSelfQuote=function(e,t,a){return(!a||a==Main.board)&&("BLOCKQUOTE"==(e=e.parentNode).nodeName&&e.id.split("m")[1]==t||e.parentNode.id.split("_")[1]==t)},QuoteInline.toggle=function(e,t){var a,i,n,o,r,s,d,l;if((n=e.getAttribute("href").match(/(?:\/([a-z0-9]+)\/thread\/)?([0-9]+)?#p([0-9]+)$/))&&!QuoteInline.isSelfQuote(e,n[3],n[1])){if(t&&t.preventDefault(),o=e.getAttribute("data-pfx")){for(e.removeAttribute("data-pfx"),$.removeClass(e,"linkfade"),s=$.id(o+"p"+n[3]),l=$.cls("expandedWebm",s),a=0;i=l[a];++a)i.pause();return s.parentNode.removeChild(s),void("backlink"==e.parentNode.parentNode.className&&(0===(d=+(s=$.id("pc"+n[3])).getAttribute("data-inline-count")-1)?(s.style.display="",s.removeAttribute("data-inline-count")):s.setAttribute("data-inline-count",d)))}(r=$.id("p"+n[3]))?QuoteInline.inline(e,r,n[3]):QuoteInline.inlineRemote(e,n[1]||Main.board,n[2],n[3])}},QuoteInline.inlineRemote=function(e,t,a,i){var n,o,r,s,d,l;if(!e.hasAttribute("data-loading")){if(s=t+"-"+a,(r=$.cache[s])&&(d=Parser.buildPost(r,t,i)))return Parser.parsePost(d),void QuoteInline.inline(e,d);(l=e.nextElementSibling)&&$.hasClass(l,"spinner")?l.parentNode.removeChild(l):((l=document.createElement("div")).className="preview spinner inlined",l.textContent="Loading...",e.parentNode.insertBefore(l,e.nextSibling),n=function(){var a,n;e.removeAttribute("data-loading"),200===this.status||304===this.status||0===this.status?(n=Parser.parseThreadJSON(this.responseText),$.cache[s]=n,(a=Parser.buildPost(n,t,i))?(l.parentNode&&l.parentNode.removeChild(l),Parser.parsePost(a),QuoteInline.inline(e,a)):($.addClass(e,"deadlink"),l.textContent="This post doesn't exist anymore")):404===this.status?($.addClass(e,"deadlink"),l.textContent="This thread doesn't exist anymore"):this.onerror()},o=function(){l.textContent="Error: "+this.statusText+" ("+this.status+")",e.removeAttribute("data-loading")},e.setAttribute("data-loading","1"),$.get("//a.4cdn.org/"+t+"/thread/"+a+".json",{onload:n,onerror:o}))}},QuoteInline.inline=function(e,t,a){var i,n,o,r,s,d,l,c,p,h,u,g;if(o=Date.now(),a)for("backlink"==(s=e.parentNode.parentNode).className?(r=s.parentNode.parentNode.parentNode,d=!0):r=s.parentNode;r.parentNode!==document;){if(r.id.split("m")[1]==a)return;r=r.parentNode}if(e.className+=" linkfade",e.setAttribute("data-pfx",o),QuotePreview.stopMedia(t),(r=t.cloneNode(!0)).id=o+r.id,r.setAttribute("data-pfx",o),r.className+=" preview inlined",$.removeClass(r,"highlight"),$.removeClass(r,"highlight-anti"),(l=$.cls("inlined",r))[0]){for(;n=l[0];)n.parentNode.removeChild(n);for(l=$.cls("quotelink",r),i=0;n=l[i];++i)n.removeAttribute("data-pfx"),$.removeClass(n,"linkfade")}for(i=0;n=r.children[i];++i)n.id=o+n.id;if((c=$.cls("backlink",r)[0])&&(c.id=o+c.id),d)p=s.parentNode.parentNode.getAttribute("data-pfx")||"",(h=$.id(p+"m"+s.id.split("_")[1])).insertBefore(r,h.firstChild),(u=t.parentNode.getAttribute("data-inline-count"))?u=+u+1:(u=1,t.parentNode.style.display="none"),t.parentNode.setAttribute("data-inline-count",u);else{for(g=$.hasClass(e.parentNode,"quote")?(e=e.parentNode).parentNode:e.parentNode;"S"===g.nodeName;)e=g,g=g.parentNode;g.insertBefore(r,e.nextSibling)}};var QuotePreview={};QuotePreview.init=function(){this.regex=/(?:\/([a-z0-9]+)\/thread\/)?([0-9]+)?#p([0-9]+)$/,this.highlight=null,this.highlightAnti=null,this.cur=null},QuotePreview.resolve=function(e){var t,a,i,n,o;if((t=QuotePreview).cur=null,a=e.getAttribute("href").match(t.regex))if(o=e.getAttribute("data-pfx")||"",i=document.getElementById(o+"p"+a[3])){if((n=i.getBoundingClientRect()).top>0&&n.bottom .quotelink",t))[1])for(h=">>"+e.parentNode.parentNode.id.split("_")[1],c=0;p=l[c];++c)if(p.textContent==h){$.addClass(p,"dotted");break}i=e.getBoundingClientRect(),r=(o=document.documentElement).offsetWidth,s=t.style,document.body.appendChild(t),Main.isMobileDevice?(s.top=i.top+e.offsetHeight+window.pageYOffset+"px",r-i.right<(0|.3*r)?s.right=r-i.right+"px":s.left=i.left+"px"):(r-i.right<(0|.3*r)?(d=r-i.left,s.right=d+5+"px"):(d=i.left+i.width,s.left=d+5+"px"),u=i.top+e.offsetHeight+window.pageYOffset-t.offsetHeight/2-i.height/2,n=t.getBoundingClientRect().height,u<(g=o.scrollTop!=document.body.scrollTop?o.scrollTop+document.body.scrollTop:document.body.scrollTop)?s.top=g+"px":u+n>g+o.clientHeight?s.top=g+o.clientHeight-n+"px":s.top=u+"px")},QuotePreview.remove=function(e){var t,a;(t=QuotePreview).cur=null,t.highlight?($.removeClass(t.highlight,"highlight"),t.highlight=null):t.highlightAnti&&($.removeClass(t.highlightAnti,"highlight-anti"),t.highlightAnti=null),e&&(e.style.cursor=""),(a=$.id("quote-preview"))&&document.body.removeChild(a)},QuotePreview.stopMedia=function(e){var t,a;if((a=$.tag("VIDEO",e))[0])for(t=0;e=a[t];++t)e.autoplay=!1;if((a=$.tag("AUDIO",e))[0])for(t=0;e=a[t];++t)e.autoplay=!1};var ImageExpansion={activeVideos:[],timeout:null,pendingTarget:null};ImageExpansion.loadOgv=function(e){if(ImageExpansion.pendingTarget=e,$.id("js-ogv-scr"))return;let t=$.el("script");t.id="js-ogv-scr",t.onload=ImageExpansion.onOgvLoaded,t.src="https://s.4cdn.org/js/ogv/ogv.js",document.body.appendChild(t)},ImageExpansion.onOgvLoaded=function(){let e=ImageExpansion;e.pendingTarget&&e.expandWebm(e.pendingTarget)},ImageExpansion.expand=function(e){var t,a,i,n;return Config.imageHover&&ImageHover.hide(),(i=(a=(n=e.parentNode).getAttribute("href")).match(/\.(?:webm|mp4|pdf)$/))?(".webm"==i[0]||".mp4"==i[0])&&ImageExpansion.expandWebm(e):(Main.hasMobileLayout&&n.hasAttribute("data-m")&&(a=ImageExpansion.setMobileSrc(n)),e.setAttribute("data-expanding","1"),(t=document.createElement("img")).alt="Image",t.setAttribute("src",a),t.className="expanded-thumb",t.style.display="none",t.onerror=this.onError,e.parentNode.insertBefore(t,e.nextElementSibling),UA.hasCORS?(e.style.opacity="0.75",this.timeout=this.checkLoadStart(t,e)):this.onLoadStart(t,e),!0)},ImageExpansion.contract=function(e){var t,a;clearTimeout(this.timeout),t=(a=e.parentNode).parentNode.parentNode,$.removeClass(a.parentNode,"image-expanded"),Config.centeredThreads&&($.removeClass(t.parentNode,"centre-exp"),t.parentNode.style.marginLeft=""),!Main.tid&&Config.threadHiding&&$.removeClass(a,"image-expanded-anti"),a.firstChild.style.display="",a.removeChild(e),t.offsetTopClose',a.parentNode.appendChild(t)):(i=e.parentNode.previousElementSibling,(t=document.createElement("span")).className="collapseWebm",t.innerHTML='-[Close]',i.appendChild(t)),$.addClass(a.parentNode,"image-expanded"),t.firstElementChild.addEventListener("click",r.collapseWebm,!1),!0},ImageExpansion.fitWebm=function(){var e,t,a,i,n,o,r,s,d,l,c;l=this,OgvCtrl.ogv?(c=l.parentNode,$.addClass(c,"ogv-loaded")):c=l,Config.centeredThreads&&(s=$.cls("opContainer")[0].offsetWidth,r=c.parentNode.parentNode.parentNode,$.addClass(r,"centre-exp")),o=l.getBoundingClientRect().left,a=document.documentElement.clientWidth-o,i=document.documentElement.clientHeight,Main.hasMobileLayout||(a-=25),e=l.videoWidth,t=l.videoHeight,e>a&&(n=a/e,e=a,t*=n),Config.fitToScreenExpansion&&t>i&&(n=i/t,t=i,e*=n),c.style.width=(0|e)+"px",c.style.height=(0|t)+"px",l!==c&&(l.style.width=c.style.width,l.style.height=c.style.height),Config.centeredThreads&&(o=c.getBoundingClientRect().left,(d=c.offsetWidth+2*o)>s?(o=Math.floor(($.docEl.clientWidth-d)/2))>0&&(r.style.marginLeft=o+"px"):$.removeClass(r,"centre-exp"))},ImageExpansion.onWebmPlay=function(){var e=ImageExpansion;e.activeVideos.length||document.addEventListener("scroll",e.onScroll,!1),e.activeVideos.push(this)},ImageExpansion.collapseWebm=function(e){var t,a,i;e.preventDefault(),this.removeEventListener("click",ImageExpansion.collapseWebm,!1),t=this.parentNode,$.removeClass(t.parentNode,"image-expanded"),(a=Main.hasMobileLayout?t.previousElementSibling:t.parentNode.parentNode.getElementsByClassName("expandedWebm")[0]).classList.contains("ogv-cnt")?a.classList.contains("ogv-detached")||(a.firstElementChild.stop(),OgvCtrl.detach()):a.pause(),Config.centeredThreads&&(i=a.parentNode.parentNode.parentNode,$.removeClass(i,"centre-exp"),i.style.marginLeft=""),a.previousElementSibling.style.display="",a.parentNode.removeChild(a),t.parentNode.removeChild(t)},ImageExpansion.detachOgv=function(e){let t=e.parentNode;t.style.width=e.style.width,t.style.height=e.style.height,t.classList.add("ogv-detached"),e.remove()},ImageExpansion.onScroll=function(){clearTimeout(ImageExpansion.timeout),ImageExpansion.timeout=setTimeout(ImageExpansion.pauseVideos,500)},ImageExpansion.pauseVideos=function(){var e,t,a,i,n,o,r;for(e=ImageExpansion,r=[],n=window.pageYOffset,o=window.pageYOffset+$.docEl.clientHeight,t=0;a=e.activeVideos[t];++t)(i=a.getBoundingClientRect()).top+window.pageYOffset>o||i.bottom+window.pageYOffsetn&&(r=n/a,a=n,i*=r),Config.fitToScreenExpansion&&i>o&&(r=o/i,i=o,a*=r),e.style.maxWidth=a+"px",e.style.maxHeight=i+"px",$.addClass(d,"image-expanded"),!Main.tid&&Config.threadHiding&&$.addClass(t.parentNode,"image-expanded-anti"),e.style.display="",t.style.display="none",Config.centeredThreads?(s=e.getBoundingClientRect().left,(p=e.offsetWidth+2*s)>c?(s=Math.floor(($.docEl.clientWidth-p)/2))>0&&(l.style.marginLeft=s+"px"):$.removeClass(l,"centre-exp")):Main.hasMobileLayout&&((l=t.parentNode.lastElementChild).firstElementChild||((d=document.createElement("div")).className="mFileName",(h=t.parentNode.parentNode.getElementsByClassName("fileText")[0])&&(h=h.firstElementChild,d.innerHTML=h.getAttribute("title")||h.innerHTML),l.insertBefore(d,l.firstChild)))},ImageExpansion.checkLoadStart=function(e,t){if(!e.naturalWidth)return setTimeout(ImageExpansion.checkLoadStart,15,e,t);ImageExpansion.onLoadStart(e,t),t.style.opacity=""};var ImageHover={};ImageHover.show=function(e){var t,a,i;(i=(a="A"!==e.nodeName?e.parentNode.getAttribute("href"):e.getAttribute("href")).match(/\.(?:webm|mp4|pdf)$/))?".webm"!=i[0]&&".mp4"!=i[0]||ImageHover.showWebm(e):((t=document.createElement("img")).id="image-hover",t.alt="Image",t.onerror=ImageHover.onLoadError,t.src=a,Config.imageHoverBg&&(t.style.backgroundColor="inherit"),document.body.appendChild(t),UA.hasCORS?(t.style.display="none",this.timeout=ImageHover.checkLoadStart(t,e)):t.style.left=e.getBoundingClientRect().right+10+"px")},ImageHover.hide=function(){var e;clearTimeout(this.timeout),(e=$.id("image-hover"))&&(e.play&&(e.pause(),Tip.hide()),document.body.removeChild(e))},ImageHover.showWebm=function(e){var t;(t=document.createElement("video")).id="image-hover",Config.imageHoverBg&&(t.style.backgroundColor="inherit"),"A"!==e.nodeName?t.src=e.parentNode.getAttribute("href"):t.src=e.getAttribute("href"),t.loop=!0,t.muted=!Config.unmuteWebm,t.autoplay=!0,t.onerror=ImageHover.onLoadError,t.onloadedmetadata=function(){ImageHover.showWebMDuration(this,e)},t.onvolumechange=Main.getWebmVolumeChangeCb(),document.body.appendChild(t),Config.unmuteWebm&&(t.volume=Main.getWebmVolume())},ImageHover.showWebMDuration=function(e,t){var a,i,n;if(e.parentNode){var o,r=$.prettySeconds(e.duration);o=!0===e.mozHasAudio||e.webkitAudioDecodedByteCount>0||e.audioTracks&&e.audioTracks.length?" (audio)":"",n=t.getBoundingClientRect(),[a,i]=$.fit(e.videoWidth,e.videoHeight,window.innerWidth-n.right-20,window.innerHeight),e.style.width=a+"px",e.style.height=i+"px",Tip.show(t,r[0]+":"+("0"+r[1]).slice(-2)+o)}},ImageHover.onLoadError=function(){Feedback.error("File no longer exists (404).",2e3)},ImageHover.onLoadStart=function(e,t){var a,i;a=t.getBoundingClientRect(),i=window.innerWidth-a.right-20,e.naturalWidth>i&&(e.style.maxWidth=i+"px"),e.style.display=""},ImageHover.checkLoadStart=function(e,t){if(!e.naturalWidth)return setTimeout(ImageHover.checkLoadStart,15,e,t);ImageHover.onLoadStart(e,t)};var QR={};QR.init=function(){var e;if(UA.hasFormData){this.enabled=!!document.forms.post,this.currentTid=null,this.cooldown=null,this.timestamp=null,this.auto=!1,this.btn=null,this.comField=null,this.comLength=window.comlen,this.comCheckTimeout=null,this.cdElapsed=0,this.activeDelay=0,this.ctTTL=290,this.ctTimeout=null,this.cooldowns={};for(e in window.cooldowns)this.cooldowns[e]=1e3*window.cooldowns[e];if(this.noCaptcha)for(e in this.cooldowns)this.cooldowns[e]=Math.ceil(this.cooldowns[e]/2);this.painterTime=0,this.painterData=null,this.painterSrc=null,this.replayBlob=null,this.canvasLoading=!1,this.captchaWidgetCnt=null,this.captchaWidgetId=null,this.pulse=null,this.xhr=null,this.fileDisabled=!!window.imagelimit,this.tracked={},!Main.tid||Main.hasMobileLayout||Main.threadClosed||QR.addReplyLink(),window.addEventListener("storage",this.syncStorage,!1)}},QR.openTeXPreview=function(){var e;QR.closeTeXPreview(),window.MathJax||window.loadMathJax(),(e=document.createElement("div")).id="tex-preview-cnt",e.className="UIPanel",e.setAttribute("data-cmd","close-tex-preview"),e.innerHTML='
    PreviewClose
    Use [math][/math] tags for inline, and [eqn][/eqn] tags for block equations.
    ',document.body.appendChild(e),e=$.id("input-tex-preview"),$.on(e,"keyup",QR.onTeXChanged)},QR.closeTeXPreview=function(){var e;(e=$.id("input-tex-preview"))&&($.off(e,"keyup",QR.onTeXChanged),(e=$.id("tex-preview-cnt")).parentNode.removeChild(e))},QR.onTeXChanged=function(){clearTimeout(QR.timeoutTeX),QR.timeoutTeX=setTimeout(QR.processTeX,50)},QR.processTeX=function(){var e,t;!QR.processingTeX&&window.MathJax&&(e=$.id("input-tex-preview"))&&((t=$.id("output-tex-preview")).textContent=e.value,QR.processingTeX=!0,MathJax.Hub.Queue(["Typeset",MathJax.Hub,t],["onTeXReady",QR]))},QR.onTeXReady=function(){QR.processingTeX=!1},QR.validateCT=function(){var e,t;QR.captchaWidgetCnt&&((e=Main.getCookie("_ct"))?QR.ctTimeout||(e=e.split(".")[1],(t=(0|Date.now()/1e3)-e)>=QR.ctTTL?QR.setCTTag():QR.setCTTag(QR.ctTTL-t)):QR.ctTimeout&&QR.setCTTag())},QR.setCTTag=function(e){if(!window.t_captcha){var t=QR.captchaWidgetCnt;QR.clearCTTimeout(),e&&e>0?(QR.ctTimeout=setTimeout(QR.setCTTag,1e3*e),t.style.opacity="0.25",$.on(t,"mouseover",QR.onCTMouseOver),$.on(t,"mouseout",QR.onCTMouseOut)):(t.style.opacity="",$.off(t,"mouseover",QR.onCTMouseOver),$.off(t,"mouseout",QR.onCTMouseOut))}},QR.onCTMouseOver=function(){QR.onCTMouseOut(),QR.ctTipTimeout=setTimeout(Tip.show,Tip.delay,QR.captchaWidgetCnt,"Verification not required for your next post.")},QR.onCTMouseOut=function(){clearTimeout(QR.ctTipTimeout),Tip.hide()},QR.clearCTTimeout=function(){clearTimeout(QR.ctTimeout),QR.ctTimeout=null},QR.addReplyLink=function(){var e,t;e=$.cls("navLinks")[2],(t=document.createElement("div")).className="open-qr-wrap",t.innerHTML='[Post a Reply]',e.insertBefore(t,e.firstChild)},QR.lock=function(){QR.showPostError("This thread is closed.","closed",!0)},QR.unlock=function(){QR.hidePostError("closed")},QR.syncStorage=function(e){var t;e.key&&"4chan"==(t=e.key.split("-"))[0]&&"cd"==t[1]&&e.newValue&&Main.board==t[2]&&QR.startCooldown()},QR.onOpenInPainterClick=function(e){var t,a,i,n;if(QR.canvasLoading)Feedback.error("An image is already being loaded.");else{if(n=+e.getAttribute("data-pid"),i=+e.getAttribute("data-tid"),!n||!i)return!1;if(!(a=$.qs("#f"+n+' a[class="fileThumb"]')))return!1;if(!1===/\.(png|jpg)$/.test(a.href))return!1;QR.canvasLoading=!0,(t=new Image).crossOrigin="Anonymous",t.onload=QR.onPainterCanvasLoaded,t.onerror=QR.onPainterCanvasError,t._pid=n,Feedback.notify("Loading\u2026",0),t.src=a.href.replace("is2.4chan.org","i.4cdn.org"),QR.show(i)}},QR.onPainterCanvasError=function(){QR.canvasLoading=!1,Feedback.error("Couldn't load the image.",5e3)},QR.onPainterCanvasLoaded=function(){Feedback.hideMessage(),QR.canvasLoading=!1,QR.currentTid&&(this.naturalWidth<1||this.naturalHeight<1||(Tegaki.startTimeStamp&&Tegaki.destroy(),Keybinds.enabled=!1,QR.painterSrc=this._pid,Tegaki.open({onDone:QR.onPainterDone,onCancel:QR.onPainterCancel,saveReplay:!1,width:1,height:1}),Tegaki.onOpenImageLoaded.call(this)))},QR.openPainter=function(){var e,t,a,i;e=+(a=$.tag("input",$.id("qr-painter-ctrl")))[0].value,t=+a[1].value,e<1||t<1||(i=$.cls("oe-r-cb",$.id("qr-painter-ctrl"))[0],Keybinds.enabled=!1,window.Tegaki.open({onDone:QR.onPainterDone,onCancel:QR.onPainterCancel,saveReplay:i&&i.checked,width:e,height:t}))},QR.onPainterDone=function(){var e,t;Keybinds.enabled=!0,QR.painterData=Tegaki.flatten().toDataURL("image/png"),Tegaki.saveReplay&&(QR.replayBlob=Tegaki.replayRecorder.toBlob()),QR.painterTime=0,Tegaki.startTimeStamp&&(Tegaki.hasCustomCanvas&&!QR.painterSrc||(QR.painterTime=Math.round((Date.now()-Tegaki.startTimeStamp)/1e3))),(e=$.id("qrFile"))&&(e.style.visibility="hidden"),t=$.id("qr-painter-ctrl"),(e=$.tag("button",t)[0])&&(e.textContent="Edit"),(e=$.tag("button",t)[1])&&(e.disabled=!1);for(e of $.tag("input",t))e.disabled=!0},QR.onPainterCancel=function(){var e,t;Keybinds.enabled=!0,QR.painterData=null,QR.painterSrc=null,QR.replayBlob=null,QR.painterTime=0,(e=$.id("qrFile"))&&(e.style.visibility=""),t=$.id("qr-painter-ctrl"),(e=$.tag("button",t)[0])&&(e.textContent="Draw"),(e=$.tag("button",t)[1])&&(e.disabled=!0);for(e of $.tag("input",t))e.disabled=!1},QR.quotePost=function(e,t){QR.noCooldown||!(Main.threadClosed||!Main.tid&&Main.isThreadClosed(e))?(QR.show(e),QR.addQuote(t)):alert("This thread is closed")},QR.addQuote=function(e){var t,a,i,n;a=(n=$.tag("textarea",document.forms.qrPost)[0]).selectionStart,i=UA.getSelection(),t=e?">>"+e+"\n":"",i&&(t+=">"+i.trim().replace(/[\r\n]+/g,"\n>")+"\n"),n.value?n.value=n.value.slice(0,a)+t+n.value.slice(n.selectionEnd):n.value=t,UA.isOpera&&(a+=t.split("\n").length),n.selectionStart=n.selectionEnd=a+t.length,n.selectionStart==n.value.length&&(n.scrollTop=n.scrollHeight),n.focus()},QR.show=function(e){var t,a,i,n,o,r,s,d,l,c,p,h,u,g,m,f;if(window.checkIncognito&&window.checkIncognito(),QR.currentTid)return Main.tid||QR.currentTid==e||($.id("qrTid").textContent=$.id("qrResto").value=QR.currentTid=e,$.byName("com")[1].value="",QR.startCooldown()),void(Main.hasMobileLayout&&($.id("quickReply").style.top=window.pageYOffset+25+"px"));for(QR.currentTid=e,n=$.id("postForm"),(i=document.createElement("div")).id="quickReply",i.className="extPanel reply",i.setAttribute("data-trackpos","QR-position"),Main.hasMobileLayout?i.style.top=window.pageYOffset+28+"px":Config["QR-position"]?i.style.cssText=Config["QR-position"]:(i.style.right="0px",i.style.top="10%"),i.innerHTML='
    '+(window.math_tags?'':"")+'Reply to Thread No.'+e+'X
    ',(o=n.parentNode.cloneNode(!1)).setAttribute("name","qrPost"),o.innerHTML=((f=$.byName("MAX_FILE_SIZE")[0])?'':"")+'',(r=document.createElement("div")).id="qrForm",this.btn=null,t=0,a=(s=n.firstElementChild.children).length-1;t label > input[name="spoiler"]',n))&&((l=document.createElement("span")).id="qrSpoiler",l.innerHTML='',c.parentNode.insertBefore(l,c.nextSibling)),o.appendChild(r),i.appendChild(o),(g=document.createElement("div")).id="qrError",i.appendChild(g),i.addEventListener("click",QR.onClick,!1),document.body.appendChild(i),QR.startCooldown(),Main.threadClosed&&QR.lock(),window.passEnabled||QR.renderCaptcha(),Main.hasMobileLayout||Draggable.set($.id("qrHeader"))},QR.renderCaptcha=function(){if(window.grecaptcha)return QR.captchaWidgetId=grecaptcha.render(QR.captchaWidgetCnt,{sitekey:window.recaptchaKey,theme:"tomorrow"===Main.stylesheet||window.dark_captcha?"dark":"light"}),void QR.validateCT();window.t_captcha&&(TCaptcha.init(QR.captchaWidgetCnt,Main.board,QR.currentTid),TCaptcha.setErrorCb(QR.showPostError))},QR.resetCaptcha=function(){window.grecaptcha?null!==QR.captchaWidgetId&&(grecaptcha.reset(QR.captchaWidgetId),QR.validateCT()):window.t_captcha&&TCaptcha.clearChallenge()},QR.onPassError=function(){var e,t;QR.captchaWidgetCnt||(window.passEnabled=QR.noCaptcha=!1,(e=document.createElement("div")).id="qrCaptchaContainer",(t=$.id("qrForm")).insertBefore(e,t.lastElementChild),QR.captchaWidgetCnt=e,QR.renderCaptcha())},QR.onFileChange=function(){var e,t;this.value?(t=window.maxFilesize,this.files?(e=this.files[0].size,"video/webm"==this.files[0].type&&window.maxWebmFilesize&&(t=window.maxWebmFilesize)):e=0,QR.fileDisabled?QR.showPostError("Image limit reached.","imagelimit",!0):e>t?QR.showPostError("Error: Maximum file size allowed is "+Math.floor(t/1048576)+" MB","filesize",!0):QR.hidePostError()):QR.hidePostError(),QR.painterData=null,QR.replayBlob=null,QR.startCooldown()},QR.onKeyDown=function(e){var t,a,i,n +;if(e.ctrlKey&&83==e.keyCode)e.stopPropagation(),e.preventDefault(),a=(t=e.target).selectionStart,i=t.selectionEnd,t.value?(n="[spoiler]"+t.value.slice(a,i)+"[/spoiler]",t.value=t.value.slice(0,a)+n+t.value.slice(i),t.setSelectionRange(i+19,i+19)):(t.value="[spoiler][/spoiler]",t.setSelectionRange(9,9));else if(!(27!=e.keyCode||e.ctrlKey||e.altKey||e.shiftKey||e.metaKey))return void QR.close();clearTimeout(QR.comCheckTimeout),QR.comCheckTimeout=setTimeout(QR.checkCommentField,500)},QR.checkCommentField=function(){var e;QR.comLength&&((e=encodeURIComponent(QR.comField.value).split(/%..|./).length-1)>QR.comLength?QR.showPostError("Error: Comment too long ("+e+"/"+QR.comLength+").","length",!0):QR.hidePostError("length")),window.sjis_tags&&(/\[sjis\]/.test(QR.comField.value)?$.hasClass(QR.comField,"sjis")||$.addClass(QR.comField,"sjis"):$.hasClass(QR.comField,"sjis")&&$.removeClass(QR.comField,"sjis"))},QR.close=function(){var e,t=$.id("quickReply");QR.comField=null,QR.currentTid=null,QR.painterTime=0,QR.painterData=null,QR.painterSrc=null,QR.replayBlob=null,QR.canvasLoading=!1,clearInterval(QR.pulse),QR.xhr&&(QR.xhr.abort(),QR.xhr=null),t.removeEventListener("click",QR.onClick,!1),(e=$.id("qrFile"))&&e.removeEventListener("change",QR.startCooldown,!1),(e=$.id("qrEmail"))&&e.removeEventListener("change",QR.startCooldown,!1),$.tag("textarea",t)[0].removeEventListener("keydown",QR.onKeyDown,!1),Draggable.unset($.id("qrHeader")),QR.noCaptcha||QR.setCTTag(),window.t_captcha&&TCaptcha.node===QR.captchaWidgetCnt&&TCaptcha.destroy(),document.body.removeChild(t)},QR.onClick=function(e){var t=e.target;if("submit"==t.type)e.preventDefault(),QR.submit(e.shiftKey);else switch(t.id){case"qrFile":e.shiftKey&&(e.preventDefault(),QR.resetFile());break;case"qrDummyFile":case"qrDummyFileButton":case"qrDummyFileLabel":e.preventDefault(),e.shiftKey?QR.resetFile():$.id("qrFile").click();break;case"qrClose":QR.close()}},QR.showPostError=function(e,t,a){var i;(i=$.id("qrError"))&&(e?(i.innerHTML=e,i.style.display="block",i.setAttribute("data-type",t||""),!a&&(document.hidden||document.mozHidden||document.webkitHidden||document.msHidden)&&alert("Posting Error")):QR.hidePostError())},QR.hidePostError=function(e){var t=$.id("qrError");t.hasAttribute("style")&&(e&&t.getAttribute("data-type")!=e||t.removeAttribute("style"))},QR.resetFile=function(){var e,t;QR.painterData=null,QR.replayBlob=null,(t=document.createElement("input")).id="qrFile",t.type="file",t.size="19",t.name="upfile",t.addEventListener("change",QR.onFileChange,!1),(e=$.id("qrFile")).removeEventListener("change",QR.onFileChange,!1),e.parentNode.replaceChild(t,e),QR.hidePostError("imagelimit"),QR.needPreuploadCaptcha=!1,QR.startCooldown()},QR.submit=function(e){var t;QR.hidePostError(),QR.presubmitChecks(e)&&(QR.auto=!1,QR.xhr=new XMLHttpRequest,QR.xhr.open("POST",document.forms.qrPost.action,!0),QR.xhr.withCredentials=!0,QR.xhr.setRequestHeader("Accept","application/json"),QR.xhr.upload.onprogress=function(e){e.loaded>=e.total?QR.btn.value="100%":QR.btn.value=(0|e.loaded/e.total*100)+"%"},QR.xhr.onerror=function(){QR.xhr=null,QR.showPostError("Connection error.")},QR.xhr.onload=function(){var e,t,a,i,n,o,r;if(QR.xhr=null,QR.btn.value="Post",200==this.status){if("application/json"==this.getResponseHeader("Content-Type")){try{e=JSON.parse(this.responseText)}catch(s){console.log("QR resp",s),e={error:"Something went wrong."}}if(e.error)return window.passEnabled&&/4chan Pass/.test(e.error)?QR.onPassError():QR.resetCaptcha(),void QR.showPostError(e.error);e.pid&&(n=e.tid,o=e.pid)}else{if(e=this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/))return window.passEnabled&&/4chan Pass/.test(e)?QR.onPassError():QR.resetCaptcha(),void QR.showPostError(e[1]);(i=this.responseText.match(//))&&(n=i[1],o=i[2])}o&&(a=(t=$.id("qrFile"))&&t.value,QR.setPostTime(),Config.persistentQR?($.byName("com")[1].value="",(t=$.byName("spoiler")[2])&&(t.checked=!1),QR.resetCaptcha(),(a||QR.painterData)&&QR.resetFile(),QR.startCooldown()):QR.close(),Main.tid?(Config.threadWatcher&&ThreadWatcher.setLastRead(o,n),QR.lastReplyId=+o,Parser.trackedReplies[">>"+o]=1,Parser.saveTrackedReplies(n,Parser.trackedReplies)):((r=Parser.getTrackedReplies(Main.board,n)||{})[">>"+o]=1,Parser.saveTrackedReplies(n,r)),Parser.touchTrackedReplies(n),UA.dispatchEvent("4chanQRPostSuccess",{threadId:n,postId:o})),ThreadUpdater.enabled&&setTimeout(ThreadUpdater.forceUpdate,500)}else QR.showPostError("Error: "+this.status+" "+this.statusText)},!(t=new FormData(document.forms.qrPost)).entries||!t["delete"]||t.get("upfile")&&t.get("upfile").size||t["delete"]("upfile"),QR.painterData&&(QR.appendPainter(t),QR.replayBlob&&t.append("oe_replay",QR.replayBlob,"tegaki.tgkr"),t.append("oe_time",QR.painterTime),QR.painterSrc&&t.append("oe_src",QR.painterSrc)),clearInterval(QR.pulse),QR.btn.value="Sending",QR.xhr.send(t))},QR.appendPainter=function(e){var t;if(t=QR.b64toBlob(QR.painterData.slice(QR.painterData.indexOf(",")+1))){if(t.size>window.maxFilesize)return void QR.showPostError("Error: Maximum file size allowed is "+Math.floor(window.maxFilesize/1048576)+" MB","filesize",!0);e.append("upfile",t,"tegaki.png")}},QR.b64toBlob=function(e){var t,a,i,n,o;for(o=(a=atob(e)).length,i=new Array(o),t=0;tThe File field has been cleared."),QR.resetFile(),!1}return!0},QR.getCooldown=function(e){return QR.cooldowns[e]},QR.setPostTime=function(){return localStorage.setItem("4chan-cd-"+Main.board,Date.now())},QR.getPostTime=function(){return localStorage.getItem("4chan-cd-"+Main.board)},QR.removePostTime=function(){return localStorage.removeItem("4chan-cd-"+Main.board)},QR.startCooldown=function(){var e,t,a;if(!QR.noCooldown&&$.id("quickReply")&&!QR.xhr)if(clearInterval(QR.pulse),e=(t=$.id("qrFile"))&&t.value?"image":"reply",a=QR.getPostTime(e)){if(QR.timestamp=parseInt(a,10),QR.activeDelay=QR.getCooldown(e),QR.cdElapsed=Date.now()-QR.timestamp,QR.cooldown=Math.ceil((QR.activeDelay-QR.cdElapsed)/1e3),QR.cooldown<=0||QR.cdElapsed<0)return QR.cooldown=!1,void(QR.btn.value="Post");QR.btn.value=QR.cooldown+"s",QR.pulse=setInterval(QR.onPulse,1e3)}else QR.btn.value="Post"},QR.onPulse=function(){QR.cdElapsed=Date.now()-QR.timestamp,QR.cooldown=Math.ceil((QR.activeDelay-QR.cdElapsed)/1e3),QR.cooldown<=0?(clearInterval(QR.pulse),QR.btn.value="Post",QR.cooldown=!1,QR.auto&&QR.submit()):QR.btn.value=QR.cooldown+(QR.auto?"s (auto)":"s")};var ThreadHiding={};ThreadHiding.init=function(){this.threshold=432e5,this.hidden={},this.load(),this.purge()},ThreadHiding.clear=function(e){var t,a,i,n;this.load(),t=0;for(a in this.hidden)++t;if(i="4chan-hide-t-"+Main.board,e)localStorage.removeItem(i);else{if(!t)return void alert("You don't have any hidden threads on /"+Main.board+"/");if(n="This will unhide "+t+" thread"+(t>1?"s":"")+" on /"+Main.board+"/",!confirm(n))return;localStorage.removeItem(i)}},ThreadHiding.isHidden=function(e){return!!ThreadHiding.hidden[e]},ThreadHiding.toggle=function(e){this.isHidden(e)?this.show(e):this.hide(e),this.save()},ThreadHiding.show=function(e){var t,a;a=$.id("t"+e),t=$.id("sa"+e),Main.hasMobileLayout?(t.parentNode.removeChild(t),a.style.display=null,$.removeClass(a.nextElementSibling,"mobile-hr-hidden")):(t.removeAttribute("data-hidden"),t.firstChild.src=Main.icons.minus,$.removeClass(a,"post-hidden")),delete this.hidden[e]},ThreadHiding.hide=function(e){var t,a;a=$.id("t"+e),Main.hasMobileLayout?(a.style.display="none",$.addClass(a.nextElementSibling,"mobile-hr-hidden"),(t=document.createElement("span")).id="sa"+e,t.setAttribute("data-cmd","hide"),t.setAttribute("data-id",e),t.textContent="Show Hidden Thread",t.className="mobileHideButton button mobile-tu-show",a.parentNode.insertBefore(t,a)):Config.hideStubs&&!$.cls("stickyIcon",a)[0]?a.style.display=a.nextElementSibling.style.display="none":((t=$.id("sa"+e)).setAttribute("data-hidden",e),t.firstChild.src=Main.icons.plus,a.className+=" post-hidden"),this.hidden[e]=Date.now()},ThreadHiding.load=function(){var e;(e=localStorage.getItem("4chan-hide-t-"+Main.board))&&(this.hidden=JSON.parse(e))},ThreadHiding.purge=function(){var e,t,a,i;i="4chan-purge-t-"+Main.board,a=localStorage.getItem(i);for(e in this.hidden){t=!0;break}t&&(!a||a
    "),this.listNode=document.createElement("ul"),this.listNode.id="watchList",this.load(),Config.threadAutoWatcher&&((n=Main.getCookie("4chan_awt"))&&(Main.removeCookie("4chan_awt","."+$L.d(Main.board),"/"+Main.board+"/"),this.add(+n,Main.board),this.save()),document.forms.post&&((i=$.el("input")).type="hidden",i.name="awt",i.value="1",document.forms.post.appendChild(i))),Main.tid&&this.refreshCurrent(),this.build(),e.appendChild(this.listNode),document.body.appendChild(e),e.addEventListener("mouseup",this.onClick,!1),Draggable.set($.id("twHeader")),window.addEventListener("storage",this.syncStorage,!1),Main.hasMobileLayout?Main.tid&&ThreadWatcher.initMobileButtons():!Main.tid&&this.canAutoRefresh()&&this.refresh()},ThreadWatcher.toggleList=function(e){var t=$.id("threadWatcher");e&&e.preventDefault(),!Main.tid&&ThreadWatcher.canAutoRefresh()&&ThreadWatcher.refresh(),"none"==t.style.display?(t.style.top=window.pageYOffset+30+"px",t.style.display=""):t.style.display="none"},ThreadWatcher.syncStorage=function(e){var t;e.key&&("4chan"!=(t=e.key.split("-"))[0]||"watch"!=t[1]||t[2]||e.newValue==e.oldValue||(ThreadWatcher.load(),ThreadWatcher.build(!0)))},ThreadWatcher.load=function(){var e;(e=localStorage.getItem("4chan-watch"))&&(this.watched=JSON.parse(e)),(e=localStorage.getItem("4chan-watch-bl"))&&(this.blacklisted=JSON.parse(e))},ThreadWatcher.build=function(e){var t,a,i,n;t="";for(i in this.watched)t+='
  • × ':(n=[],this.watched[i][3]&&n.push("archivelink"),this.watched[i][4]&&(n.push("hasYouReplies"),t+=' title="This thread has replies to your posts"'),this.watched[i][2]?t+=' class="'+(n[0]?n.join(" ")+" ":"")+'hasNewReplies">('+this.watched[i][2]+") ":t+=(n[0]?'class="'+n.join(" ")+'"':"")+">"),t+="/"+a[1]+"/ - "+this.watched[i][0]+"
  • ";e&&ThreadWatcher.rebuildButtons(),ThreadWatcher.listNode.innerHTML=t},ThreadWatcher.rebuildButtons=function(){var e,t,a,i;for(t=$.cls("wbtn"),e=0;i=t[e];++e)a=i.getAttribute("data-id")+"-"+Main.board,ThreadWatcher.watched[a]?i.hasAttribute("data-active")||(i.src=Main.icons.watched,i.setAttribute("data-active","1")):i.hasAttribute("data-active")&&(i.src=Main.icons.notwatched,i.removeAttribute("data-active"))},ThreadWatcher.initMobileButtons=function(){var e,t,a,i;e=document.createElement("img"),a=Main.tid+"-"+Main.board,ThreadWatcher.watched[a]?(e.src=Main.icons.watched,e.setAttribute("data-active","1")):e.src=Main.icons.notwatched,e.className="extButton wbtn wbtn-"+a,e.setAttribute("data-cmd","watch"),e.setAttribute("data-id",Main.tid),e.alt="W",(t=document.createElement("span")).className="mobileib button",t.appendChild(e),(i=$.cls("navLinks")[0])&&(i.appendChild(document.createTextNode(" ")),i.appendChild(t)),(i=$.cls("navLinks")[3])&&(i.appendChild(document.createTextNode(" ")),i.appendChild(t.cloneNode(!0)))},ThreadWatcher.onClick=function(e){var t=e.target;t.hasAttribute("data-id")?ThreadWatcher.toggle(t.getAttribute("data-id"),t.getAttribute("data-board")):"twPrune"!=t.id||ThreadWatcher.isRefreshing?"twClose"==t.id&&ThreadWatcher.toggleList():ThreadWatcher.refreshWithAutoWatch()},ThreadWatcher.generateLabel=function(e,t,a){var i;return i=(i=e)?i.slice(0,this.charLimit):(i=t)?i.replace(/(?:
    )+/g," ").replace(/<[^>]*?>/g,"").slice(0,this.charLimit):"No."+a},ThreadWatcher.toggle=function(e,t){var a;a=e+"-"+(t||Main.board),this.watched[a]?(this.blacklisted[a]=1,delete this.watched[a]):this.add(e,t,a),this.save(),this.load(),this.build(!0)},ThreadWatcher.add=function(e,t){var a,i,n,o,r,s;a=e+"-"+(t||Main.board),n=$.cls("subject",$.id("pi"+e))[0].textContent,o=$.id("m"+e).innerHTML,i=this.generateLabel(n,o,e),r=(s=$.id("t"+e)).children[1]?s.lastElementChild.id.slice(2):e,this.watched[a]=[i,r,0]},ThreadWatcher.addRaw=function(e,t){var a,i;a=e.no+"-"+(t||Main.board),this.watched[a]||(i=ThreadWatcher.generateLabel(e.sub,e.com,e.no),this.watched[a]=[i,0,0])},ThreadWatcher.save=function(){var e;ThreadWatcher.sortByBoard(),localStorage.setItem("4chan-watch",JSON.stringify(ThreadWatcher.watched));for(e in ThreadWatcher.blacklisted){localStorage.setItem("4chan-watch-bl",JSON.stringify(ThreadWatcher.blacklisted));break}},ThreadWatcher.sortByBoard=function(){var e,t,a,i,n;t=ThreadWatcher,i={},n=[];for(a in t.watched)n.push(a);for(n.sort(function(e,t){return(e=e.split("-")[1])<(t=t.split("-")[1])?-1:e>t?1:0}),e=0;a=n[e];++e)i[a]=t.watched[a];t.watched=i},ThreadWatcher.canAutoRefresh=function(){var e;return!(e=localStorage.getItem("4chan-tw-timestamp"))||Date.now()-+e>=6e4},ThreadWatcher.setRefreshTimestamp=function(){localStorage.setItem("4chan-tw-timestamp",Date.now())},ThreadWatcher.refreshWithAutoWatch=function(){var e,t,a,i,n;if(Config.filter){for(Filter.load(),n={},a=0,e=0;t=Filter.activeFilters[e];++e)if(t.auto&&t.boards)for(i in t.boards)n[i]||(n[i]=!0,++a);a?($.id("twPrune").src=Main.icons.rotate,this.isRefreshing=!0,this.fetchCatalogs(n,a)):this.refresh()}else this.refresh()},ThreadWatcher.fetchCatalogs=function(e,t){var a,i,n,o;n={},o={count:t},a=0;for(i in e)setTimeout(ThreadWatcher.fetchCatalog,a,i,n,o),a+=200},ThreadWatcher.fetchCatalog=function(e,t,a){var i;(i=new XMLHttpRequest).open("GET","//a.4cdn.org/"+e+"/catalog.json"),i.onload=function(){a.count--,t[e]=Parser.parseCatalogJSON(this.responseText),a.count||ThreadWatcher.onCatalogsLoaded(t)},i.onerror=function(){a.count--,a.count||ThreadWatcher.onCatalogsLoaded(t)},i.send(null)},ThreadWatcher.onCatalogsLoaded=function(e){var t,a,i,n,o,r,s,d,l;$.id("twPrune").src=Main.icons.refresh,this.isRefreshing=!1,l={};for(i in e)for(o=e[i],t=0;n=o[t];++t)for(r=n.threads,a=0;s=r[a];++a)d=s.no+"-"+i,this.blacklisted[d]?l[d]=1:Filter.match(s,i)&&this.addRaw(s,i);this.blacklisted=l,this.build(!0),this.refresh()},ThreadWatcher.refresh=function(){var e,t,a,i,n;if(i=$.id("watchList").children.length){e=t=0,(n=$.id("twPrune")).src=Main.icons.rotate,ThreadWatcher.isRefreshing=!0,ThreadWatcher.setRefreshTimestamp();for(a in ThreadWatcher.watched)setTimeout(ThreadWatcher.fetch,t,a,++e==i?n:null),t+=200}},ThreadWatcher.refreshCurrent=function(e){var t,a,i;t=Main.tid+"-"+Main.board,this.watched[t]&&(i=(a=$.id("t"+Main.tid)).children[1]?a.lastElementChild.id.slice(2):Main.tid,this.watched[t][1]=1&&!(o[i].no<=r);i--)if(++n,s){if(d.innerHTML=o[i].com,!(l=$.cls("quotelink",d))[0])continue;for(p=0;c=l[p];++p)if(s[c.textContent]){ThreadWatcher.watched[e][4]=1,s=null;break}}n>ThreadWatcher.watched[e][2]&&(ThreadWatcher.watched[e][2]=n),o[0].archived&&(ThreadWatcher.watched[e][3]=1)}else 404==this.status&&(ThreadWatcher.watched[e][1]=-1);t&&ThreadWatcher.onRefreshEnd(t)},t&&(i.onerror=i.onload),i.open("GET","//a.4cdn.org/"+a[1]+"/thread/"+a[0]+".json"),i.send(null)};var ThreadExpansion={};ThreadExpansion.init=function(){this.enabled=UA.hasCORS,this.fetchXhr=null},ThreadExpansion.expandComment=function(e){var t,a,i,n;(t=e.getAttribute("href").match(/^(?:thread\/)([0-9]+)#p([0-9]+)$/))&&(a=t[1],i=t[2],(n=e.parentNode).textContent="Loading...",$.get("//a.4cdn.org/"+Main.board+"/thread/"+a+".json",{onload:function(){var e,t,o,r;if(200==this.status){if(t=$.id("m"+i),r=Parser.parseThreadJSON(this.responseText),a==i)o=r[0];else for(e=r.length-1;e>0;e--)if(r[e].no==i){o=r[e];break}o?(o=Parser.buildHTMLFromJSON(o,Main.board),t.innerHTML=$.cls("postMessage",o)[0].innerHTML,Parser.prettify&&Parser.parseMarkup(t),window.math_tags&&Parser.parseMathOne(t)):n.textContent="This post doesn't exist anymore."}else 404==this.status?n.textContent="This thread doesn't exist anymore.":(n.textContent="Connection Error",console.log("ThreadExpansion: "+this.status+" "+this.statusText))},onerror:function(){n.textContent="Connection Error",console.log("ThreadExpansion: xhr failed")}}))},ThreadExpansion.toggle=function(e){var t,a,i,n,o;n=(t=$.id("t"+e)).children[1],t.hasAttribute("data-truncated")&&(i=(a=$.id("m"+e)).nextSibling),$.hasClass(t,"tExpanded")?(t.className=t.className.replace(" tExpanded"," tCollapsed"),n.children[0].src=Main.icons.plus,n.children[1].style.display="inline",n.children[2].style.display="none",a&&(o=a.innerHTML,a.innerHTML=i.textContent,i.textContent=o)):$.hasClass(t,"tCollapsed")?(t.className=t.className.replace(" tCollapsed"," tExpanded"),n.children[0].src=Main.icons.minus,n.children[1].style.display="none",n.children[2].style.display="inline",a&&(o=a.innerHTML,a.innerHTML=i.textContent,i.textContent=o)):(n.children[0].src=Main.icons.rotate,ThreadExpansion.fetchXhr||ThreadExpansion.fetch(e))},ThreadExpansion.fetch=function(e){ThreadExpansion.fetchXhr=$.get("//a.4cdn.org/"+Main.board+"/thread/"+e+".json",{onload:function(){var t,a,i,n,o,r,s,d,l,c,p,h;if(ThreadExpansion.fetchXhr=null,p=(o=$.id("t"+e)).children[1],200==this.status){if(r=$.cls("reply",o),s=Parser.parseThreadJSON(this.responseText),!Config.revealSpoilers&&s[0].custom_spoiler&&Parser.setCustomSpoiler(Main.board,s[0].custom_spoiler),n=document.createDocumentFragment(),r[0])for(r=+r[0].id.slice(1),t=1;(a=s[t])&&a.no
    ",d.appendChild(l)):d.innerHTML=s[0].com,Parser.prettify&&Parser.parseMarkup(d),window.math_tags&&Parser.parseMathOne(d)),o.insertBefore(n,p.nextSibling),Parser.parseThread(e,1,t-1),o.className+=" tExpanded",p.children[0].src=Main.icons.minus,p.children[1].style.display="none",p.children[2].style.display="inline"}else 404==this.status?(p.children[0].src=Main.icons.plus,p.children[0].display="none",p.children[1].textContent="This thread doesn't exist anymore."):(p.children[0].src=Main.icons.plus,console.log("ThreadExpansion: "+this.status+" "+this.statusText))},onerror:function(){ThreadExpansion.fetchXhr=null,$.id("t"+e).children[1].children[0].src=Main.icons.plus,console.log("ThreadExpansion: xhr failed")}})};var ThreadUpdater={};ThreadUpdater.init=function(){UA.hasCORS&&!window.thread_archived&&(this.enabled=!0,this.pageTitle=document.title,this.unreadCount=0,this.auto=this.hadAuto=!1,this.delayId=0,this.delayIdHidden=4,this.delayRange=[10,15,20,30,60,90,120,180,240,300],this.timeLeft=0,this.interval=null,this.tailSize=window.tailSize||0,this.lastUpdated=Date.now(),this.lastModified="0",this.lastModifiedTail="0",this.lastReply=null,this.currentIcon=null,this.iconPath="//s.4cdn.org/image/",this.iconNode=$.qs('link[rel="shortcut icon"]',document.head),this.iconNode.type="image/x-icon",this.defaultIcon=this.iconNode.getAttribute("href").replace(this.iconPath,""),this.deletionQueue={},Config.updaterSound&&(this.audioEnabled=!1,this.audio=document.createElement("audio"),this.audio.src="//s.4cdn.org/media/beep.ogg"),this.hidden="hidden",this.visibilitychange="visibilitychange",this.adsTTL=9e5,this.adsReloadTs=0,"undefined"==typeof document.hidden&&("mozHidden"in document?(this.hidden="mozHidden",this.visibilitychange="mozvisibilitychange"):"webkitHidden"in document?(this.hidden="webkitHidden",this.visibilitychange="webkitvisibilitychange"):"msHidden"in document&&(this.hidden="msHidden",this.visibilitychange="msvisibilitychange")),this.initControls(),document.addEventListener("scroll",this.onScroll,!1),(Config.alwaysAutoUpdate||sessionStorage.getItem("4chan-auto-"+Main.tid))&&this.start())},ThreadUpdater.apiUrlFilter=null,ThreadUpdater.buildMobileControl=function(e,t){var a,i,n,o,r,s,d;t=t?"Bot":"",(a=document.createElement("div")).className="btn-row",(d=(s=e.parentNode).cloneNode(!0)).innerHTML='',a.appendChild(d),i=e.parentNode.parentNode,(n=document.createElement("span")).className="mobileib button",r=document.createElement("label"),(o=document.createElement("input")).type="checkbox",o.setAttribute("data-cmd","auto"),this["autoNode"+t]=o,r.appendChild(o),r.appendChild(document.createTextNode("Auto")),n.appendChild(r),a.appendChild(document.createTextNode(" ")),a.appendChild(n),(r=document.createElement("div")).className="mobile-tu-status",a.appendChild(this["statusNode"+t]=r),i.appendChild(a),s.parentNode.removeChild(s),(i=$.id("mpostform"))&&(i.parentNode.style.marginTop="")},ThreadUpdater.buildDesktopControl=function(e){var t,a,i,n;e=e?"Bot":"",(t=document.createDocumentFragment()).appendChild(document.createTextNode(" [")),(a=document.createElement("a")).href="",a.textContent="Update",a.setAttribute("data-cmd","update"),t.appendChild(a),t.appendChild(document.createTextNode("]")),t.appendChild(document.createTextNode(" [")),i=document.createElement("label"),(a=document.createElement("input")).type="checkbox",a.title="Fetch new replies automatically",a.setAttribute("data-cmd","auto"),this["autoNode"+e]=a,i.appendChild(a),i.appendChild(document.createTextNode("Auto")),t.appendChild(i),t.appendChild(document.createTextNode("] ")),Config.updaterSound&&(t.appendChild(document.createTextNode(" [")),i=document.createElement("label"),(a=document.createElement("input")).type="checkbox",a.title="Play a sound on new replies to your posts",a.setAttribute("data-cmd","sound"),this["soundNode"+e]=a,i.appendChild(a),i.appendChild(document.createTextNode("Sound")),t.appendChild(i),t.appendChild(document.createTextNode("] "))),t.appendChild(this["statusNode"+e]=document.createElement("span")),(n=e?$.cls("navLinks"+e)[0]:$.cls("navLinks")[1])&&n.appendChild(t)},ThreadUpdater.initControls=function(){Main.hasMobileLayout?(this.buildMobileControl($.id("refresh_top")),this.buildMobileControl($.id("refresh_bottom"),!0)):(this.buildDesktopControl(),this.buildDesktopControl(!0))},ThreadUpdater.start=function(){this.dead||(this.auto=this.hadAuto=!0,this.autoNode.checked=this.autoNodeBot.checked=!0,this.force=this.updating=!1,this.hidden&&document.addEventListener(this.visibilitychange,this.onVisibilityChange,!1),this.delayId=0,this.timeLeft=this.delayRange[0],this.pulse(),sessionStorage.setItem("4chan-auto-"+Main.tid,1))},ThreadUpdater.stop=function(e){clearTimeout(this.interval),this.auto=this.updating=this.force=!1,this.autoNode.checked=this.autoNodeBot.checked=!1,this.hidden&&document.removeEventListener(this.visibilitychange,this.onVisibilityChange,!1),e&&(this.setStatus(""),this.setIcon(null)),sessionStorage.removeItem("4chan-auto-"+Main.tid)},ThreadUpdater.pulse=function(){var e=ThreadUpdater;0===e.timeLeft?e.update():(e.setStatus(e.timeLeft--),e.interval=setTimeout(e.pulse,1e3))},ThreadUpdater.adjustDelay=function(e){0===e?this.force||this.delayId(0|(Date.now()-e.lastUpdated)/1e3)))},ThreadUpdater.checkTailValid=function(e){var t=e[0];return t&&t.tail_id?!!$.id("p"+t.tail_id):(ThreadUpdater.tailSize=0,!1)},ThreadUpdater.reloadAds=function(){if(!window.Danbo||!window.reloadAdsDanbo)return;let e=ThreadUpdater,t=Date.now();e.adsReloadTs?t-e.adsReloadTs=0&&!(o[e].no<=d);e--)i.push(o[e]);if(1==(h=i.length)&&QR.lastReplyId==i[0].no&&(u=!0,QR.lastReplyId=null),h){for(c=document.documentElement,p=Config.autoScroll&&document[a.hidden]&&c.scrollHeight==Math.ceil(window.innerHeight+window.pageYOffset),window.chrome&&document.activeElement&&(document.activeElement.href||"checkbox"===document.activeElement.type)&&document.activeElement.blur(),r=document.createDocumentFragment(), +e=i.length-1;e>=0;e--)r.appendChild(Parser.buildHTMLFromJSON(i[e],Main.board));n.appendChild(r),g=s.offsetTop,Parser.hasYouMarkers=!1,Parser.hasHighlightedPosts=!1,Parser.parseThread(n.id.slice(1),-i.length),g!=s.offsetTop&&window.scrollBy(0,s.offsetTop-g),u||(!a.force&&c.scrollHeight>window.innerHeight?(a.lastReply||d==Main.tid||((a.lastReply=s.lastChild).className+=" newPostsMarker"),Parser.hasYouMarkers?(a.setIcon("rep"),a.audioEnabled&&document[a.hidden]&&a.audio.play()):Parser.hasHighlightedPosts&&"rep"!==a.currentIcon?a.setIcon("hl"):0===a.unreadCount&&a.setIcon("new"),a.unreadCount+=h,document.title="("+a.unreadCount+") "+a.pageTitle):a.setStatus(h+" new post"+(h>1?"s":""))),p&&window.scrollTo(0,document.documentElement.scrollHeight),Config.threadWatcher&&ThreadWatcher.refreshCurrent(!0),Config.threadStats&&(l=o[0],ThreadStats.update(l.replies,l.images,l.unique_ips,l.bumplimit,l.imagelimit)),a.force&&a.reloadAds(),UA.dispatchEvent("4chanThreadUpdated",{count:h})}else a.setStatus("No new posts");o[0].archived&&(a.setError("This thread is archived"),a.dead||(a.setIcon("dead"),window.thread_archived=!0,a.dead=!0,a.stop()))}else if(304===this.status||0===this.status)a.setStatus("No new posts");else if(404===this.status)return this.istail?(a.updating=!1,void a.update(!0)):(a.setIcon("dead"),a.setError("This thread has been pruned or deleted"),a.dead=!0,void a.stop());a.lastUpdated=Date.now(),a.adjustDelay(i.length),a.updating=a.force=!1},ThreadUpdater.onerror=function(){var e=ThreadUpdater;UA.isOpera&&!this.statusText&&0===this.status?e.setStatus("No new posts"):e.setError("Connection Error"),e.lastUpdated=Date.now(),e.adjustDelay(0),e.updating=e.force=!1},ThreadUpdater.setStatus=function(e){this.statusNode.textContent=this.statusNodeBot.textContent=e},ThreadUpdater.setError=function(e){this.statusNode.innerHTML=this.statusNodeBot.innerHTML=''+e+""},ThreadUpdater.setIcon=function(e){var t;t=null===e?this.defaultIcon:this.icons[Main.type+e],this.currentIcon=e,this.iconNode.href=this.iconPath+t,document.head.appendChild(this.iconNode)},ThreadUpdater.icons={wsnew:"favicon-ws-newposts.ico",nwsnew:"favicon-nws-newposts.ico",wsrep:"favicon-ws-newreplies.ico",nwsrep:"favicon-nws-newreplies.ico",wsdead:"favicon-ws-deadthread.ico",nwsdead:"favicon-nws-deadthread.ico",wshl:"favicon-ws-newfilters.ico",nwshl:"favicon-nws-newfilters.ico"};var ThreadStats={};ThreadStats.init=function(){var e;this.nodeTop=document.createElement("div"),this.nodeTop.className="thread-stats",Main.hasMobileLayout?(this.nodeBot={},(e=$.cls("navLinks"))[0]&&(e=e[e.length-1].nextElementSibling).parentNode.insertBefore(this.nodeTop,e)):(this.nodeBot=this.nodeTop.cloneNode(!1),(e=$.cls("navLinks"))[1]&&e[1].appendChild(this.nodeTop),e[2]&&e[2].appendChild(this.nodeBot)),this.pageNumber=null,this.update(null,null,null,window.bumplimit,window.imagelimit),window.thread_archived||(this.updatePageNumber(),this.pageInterval=setInterval(this.updatePageNumber,18e4))},ThreadStats.update=function(e,t,a,i,n){var o;null===e&&(e=$.cls("replyContainer").length,t=$.cls("fileText").length-($.id("fT"+Main.tid)?1:0)),o=[],Main.threadSticky&&o.push("Sticky"),window.thread_archived?o.push("Archived"):Main.threadClosed&&o.push("Closed"),i?o.push(''+e+""):o.push(''+e+""),n?o.push(''+t+""):o.push(''+t+""),window.thread_archived||(window.unique_ips&&o.push(''+(a||window.unique_ips)+""),o.push(''+(this.pageNumber||"?")+"")),this.nodeTop.innerHTML=this.nodeBot.innerHTML=o.join(" / ")},ThreadStats.updatePageNumber=function(){$.get("//a.4cdn.org/"+Main.board+"/threads.json",{onload:ThreadStats.onCatalogLoad,onerror:ThreadStats.onCatalogError})},ThreadStats.onCatalogLoad=function(){var e,t,a,i,n,o,r,s,d,l,c;if(e=ThreadStats,200==this.status){for(l=+Main.tid,d=JSON.parse(this.responseText),t=0;o=d[t];++t)for(s=o.threads,a=0;r=s[a];++a)if(r.no==l){for(c=$.cls("ts-page"),i=0;n=c[i];++i)n.textContent=o.page;return void(e.pageNumber=o.page)}clearInterval(e.pageInterval)}else ThreadStats.onCatalogError()},ThreadStats.onCatalogError=function(){console.log("ThreadStats: couldn't get the catalog ("+this.status+")")};var Filter={};Filter.init=function(){this.entities=document.createElement("div"),Filter.load()},Filter.onClick=function(e){var t;if(t=e.target.getAttribute("data-cmd"))switch(t){case"filters-add":Filter.add();break;case"filters-save":Filter.save(),Filter.close();break;case"filters-close":Filter.close();break;case"filters-palette":Filter.openPalette(e.target);break;case"filters-palette-close":Filter.closePalette();break;case"filters-palette-clear":Filter.clearPalette();break;case"filters-up":Filter.moveUp(e.target.parentNode.parentNode);break;case"filters-del":Filter.remove(e.target.parentNode.parentNode);break;case"filters-help-open":Filter.openHelp();break;case"filters-help-close":Filter.closeHelp()}},Filter.onPaletteClick=function(e){var t;if(t=e.target.getAttribute("data-cmd"))switch(t){case"palette-pick":Filter.pickColor(e.target);break;case"palette-clear":Filter.pickColor(e.target,!0);break;case"palette-close":Filter.closePalette()}},Filter.match=function(e,t){var a,i,n,o,r;for(r=!1,o=Filter.activeFilters,a=0;n=o[a];++a)if(n.boards[t])if(0===n.type){if(n.pattern===e.trip){r=!0;break}}else if(1===n.type){if(n.pattern===e.name){r=!0;break}}else if(2===n.type&&e.com){if(i===undefined&&(this.entities.innerHTML=e.com.replace(/
    /g,"\n").replace(/[<[^>]+>/g,""),i=this.entities.textContent),n.pattern.test(i)){r=!0;break}}else if(4===n.type){if(n.pattern===e.id){r=!0;break}}else if(5===n.type){if(n.pattern.test(e.sub)){r=!0;break}}else if(6===n.type&&n.pattern.test(e.filename)){r=!0;break}return r},Filter.exec=function(e,t,a,i){var n,o,r,s,d,l,c,p,h,u,g,m;if(Parser.trackedReplies&&Parser.trackedReplies[">>"+t.id.slice(2)])return!1;for(m=Main.board,u=Filter.activeFilters,g=!1,n=0;h=u[n];++n)if(!h.boards||h.boards[m])if(0===h.type){if((r!==undefined||(r=t.getElementsByClassName("postertrip")[0]))&&h.pattern==r.textContent){g=!0;break}}else if(1===h.type){if((s||(s=t.getElementsByClassName("name")[0]))&&h.pattern==s.textContent){g=!0;break}}else if(2===h.type){if(d===undefined&&(this.entities.innerHTML=a.innerHTML.replace(/
    /g,"\n").replace(/[<[^>]+>/g,""),d=this.entities.textContent),h.pattern.test(d)){g=!0;break}}else if(4===h.type){if((l||(l=t.getElementsByClassName("posteruid")[0])&&(l=l.firstElementChild.textContent))&&h.pattern==l){g=!0;break}}else if(Main.tid||5!==h.type){if(6===h.type&&(p===undefined&&(p=(p=t.parentNode.getElementsByClassName("fileText")[0])?p.firstElementChild.textContent:""),h.pattern.test(p))){g=!0;break}}else if((c||(c=t.getElementsByClassName("subject")[0])&&(c=c.textContent))&&h.pattern.test(c)){g=!0;break}if(g){if(h.hide)return i&&Config.hideStubs&&!$.cls("stickyIcon",e)[0]?e.style.display=e.nextElementSibling.style.display="none":(e.className+=" post-hidden",o=document.createElement("span"),i?o.innerHTML='[View]':(o.textContent="[View]",o.setAttribute("data-filtered","1"),o.setAttribute("data-cmd","unfilter")),o.className="filter-preview",t.appendChild(o)),!0;e.className+=" filter-hl",e.style.boxShadow="-3px 0 "+h.color,Parser.hasHighlightedPosts=!0}return!1},Filter.unfilter=function(e){var t=e.parentNode.parentNode;QuotePreview.remove(),$.removeClass(t,"post-hidden"),e.parentNode.removeChild(e)},Filter.load=function(){var e,t,a,i,n,o,r,s,d,l,c,p,h,u,g,m,f;if(this.activeFilters=[],i=localStorage.getItem("4chan-filters")){i=JSON.parse(i),r=new RegExp("(\\"+["/",".","*","+","?","(",")","[","]","{","}","\\","^","$"].join("|\\")+")","g"),s=/^\/(.*)\/(i?)$/,d="(?=.*\\b",l="\\b)",p=/\\\*/g,h="[^\\s]*";try{for(o=0;a=i[o];++o)if(a.active&&""!==a.pattern){if(a.boards)for(u=a.boards.split(/[^a-z0-9]+/i),g={},e=0;t=u[e];++e)g[t]=!0;else g=!1;if(n=a.pattern,a.type&&1!=a.type&&4!=a.type)if(f=n.match(s))m=new RegExp(f[1],f[2]);else if('"'==n[0]&&'"'==n[n.length-1])m=new RegExp(n.slice(1,-1).replace(r,"\\$1"));else{for(m="",e=0,t=(c=n.split(" ")).length;e
    Filters & Highlights HelpClose

    Tripcode, Name and ID filters:

    • Those use simple string comparison.
    • Type them exactly as they appear on 4chan, including the exclamation mark for tripcode filters.
    • Example: !Ep8pui8Vw2

    Comment, Subject and E-mail filters:

    • Matching whole words:
    • feel — will match "feel" but not "feeling". This search is case-insensitive.
    • AND operator:
    • feel girlfriend — will match "feel" AND "girlfriend" in any order.
    • Exact match:
    • "that feel when" — place double quotes around the pattern to search for an exact string
    • Wildcards:
    • feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc\u2026
    • idolm*ster — this can match "idolmaster" or "idolm@ster", etc\u2026
    • Regular expressions:
    • /feel when no (girl|boy)friend/i
    • /^(?!.*touhou).*$/i — NOT operator.
    • /^>/ — comments starting with a quote.
    • /^$/ — comments with no text.

    Colors:

    • The color field can accept any valid CSS color:
    • red, #0f0, #00ff00, rgba( 34, 12, 64, 0.3), etc\u2026

    Boards:

    • A space separated list of boards on which the filter will be active. Leave blank to apply to all boards.

    Auto-watching:

    • Enabling the "Auto" option will automatically add matched threads to the Thread Watcher when it is manually refreshed. This only works when the "Boards" field is not empty, and searches catalog JSON for the selected boards(s).

    Shortcut:

    • If you have Keyboard shortcuts enabled, pressing F will add the selected text to your filters.
    ',document.body.appendChild(e),e.addEventListener("click",this.onClick,!1))},Filter.closeHelp=function(){var e;(e=$.id("filtersHelp"))&&(e.removeEventListener("click",this.onClick,!1),document.body.removeChild(e))},Filter.open=function(){var e,t,a,i,n;if($.id("filtersMenu"))return!1;if((a=document.createElement("div")).id="filtersMenu",a.className="UIPanel",a.style.display="none",a.setAttribute("data-cmd","filters-close"),a.innerHTML='
    Filters & HighlightsHelpClose
    OnPatternBoardsTypeColorAutoHideDel
    ',document.body.appendChild(a),a.addEventListener("click",this.onClick,!1),n=$.id("filter-list"),i=localStorage.getItem("4chan-filters"))for(i=JSON.parse(i),e=0;t=i[e];++e)n.appendChild(this.buildEntry(t,e));a.style.display=""},Filter.close=function(){var e;(e=$.id("filtersMenu"))&&(this.closePalette(),e.removeEventListener("click",this.onClick,!1),document.body.removeChild(e))},Filter.moveUp=function(e){var t;(t=e.previousElementSibling)&&e.parentNode.insertBefore(e,t)},Filter.add=function(e,t,a){var i,n,o;i={active:!0,type:t||0,pattern:e||"",boards:a||"",color:"",auto:!1,hide:!1},n=this.getNextFilterId(),o=this.buildEntry(i,n),$.id("filter-list").appendChild(o),$.cls("fPattern",o)[0].focus()},Filter.remove=function(e){$.id("filter-list").removeChild(e)},Filter.save=function(){var e,t,a,i,n,o,r;for(t=[],a=$.id("filter-list").children,e=0;i=a[e];++e)r=i.children[4].firstChild,n={active:i.children[1].firstChild.checked,pattern:i.children[2].firstChild.value,boards:i.children[3].firstChild.value,type:+r.options[r.selectedIndex].value,auto:i.children[6].firstChild.checked,hide:i.children[7].firstChild.checked},(o=i.children[5].firstChild).hasAttribute("data-nocolor")||(n.color=o.style.backgroundColor),t.push(n);t[0]?localStorage.setItem("4chan-filters",JSON.stringify(t)):localStorage.removeItem("4chan-filters")},Filter.getNextFilterId=function(){var e,t,a,i=$.id("filter-list").children;if(i.length){for(a=0,e=0;t=i[e];++e)(t=+t.id.slice(7))>a&&(a=t);return a+1}return 0},Filter.buildEntry=function(e,t){var a,i,n;return(a=document.createElement("tr")).id="filter-"+t,i="",i+='',i+='':">"),i+='',i+='',3===e.type&&(e.type=4),(n=["","","","","","",""])[e.type]=' selected="selected"',i+='",i+='':i+=' data-nocolor="1">∕',i+="",i+='':">"),i+='':">"),i+='×',a.innerHTML=i,a},Filter.buildPalette=function(e){var t,a,i,n,o,r,s;for(r=(o=[["#E0B0FF","#F2F3F4","#7DF9FF","#FFFF00"],["#FBCEB1","#FFBF00","#ADFF2F","#0047AB"],["#00A550","#007FFF","#AF0A0F","#B5BD68"]]).length,s=o[0].length,n='
    ',t=0;t",a=0;a
    ';n+=""}return n+='
    Custom
    [Close][Clear]
    ',(i=document.createElement("div")).id="filter-palette",i.setAttribute("data-target",e),i.setAttribute("data-cmd","palette-close"),i.className="UIMenu",i.innerHTML=n,i},Filter.openPalette=function(e){var t,a,i,n;Filter.closePalette(),a=e.getBoundingClientRect(),i=e.parentNode.parentNode.id.slice(7),t=Filter.buildPalette(i),document.body.appendChild(t),$.id("filter-palette").addEventListener("click",Filter.onPaletteClick,!1),$.id("palette-custom-input").addEventListener("keyup",Filter.setCustomColor,!1),(n=t.firstElementChild).style.cssText="top:"+a.top+"px;left:"+(a.left-n.clientWidth-10)+"px;"},Filter.closePalette=function(){var e;(e=$.id("filter-palette"))&&($.id("filter-palette").removeEventListener("click",Filter.onPaletteClick,!1),$.id("palette-custom-input").removeEventListener("keyup",Filter.setCustomColor,!1),e.parentNode.removeChild(e))},Filter.pickColor=function(e,t){var a,i;a=$.id("filter-palette").getAttribute("data-target"),(i=$.id("filter-"+a))&&(i=$.cls("colorbox",i)[0],!0===t?(i.setAttribute("data-nocolor","1"),i.innerHTML="∕",i.style.background=""):(i.removeAttribute("data-nocolor"),i.innerHTML="",i.style.background=e.style.backgroundColor),Filter.closePalette())},Filter.setCustomColor=function(){var e;e=$.id("palette-custom-input"),$.id("palette-custom-ok").style.backgroundColor=e.value};var IDColor={css:"padding: 0 5px; border-radius: 6px; font-size: 0.8em;",ids:{}};IDColor.init=function(){var e;window.user_ids&&(this.enabled=!0,(e=document.createElement("style")).setAttribute("type","text/css"),e.textContent=".posteruid .hand {"+this.css+"}",document.head.appendChild(e))},IDColor.compute=function(e){var t,a;return t=[],a=$.hash(e),t[0]=a>>24&255,t[1]=a>>16&255,t[2]=a>>8&255,t[3]=.299*t[0]+.587*t[1]+.114*t[2]>125,this.ids[e]=t,t},IDColor.apply=function(e){var t;t=IDColor.ids[e.textContent]||IDColor.compute(e.textContent),e.style.cssText=" background-color: rgb("+t[0]+","+t[1]+","+t[2]+"); color: "+(t[3]?"black;":"white;")},IDColor.applyRemote=function(e){this.apply(e),e.style.cssText+=this.css};var SWFEmbed={};SWFEmbed.init=function(){Main.tid?this.processThread():Main.hasMobileLayout||this.processIndex()},SWFEmbed.processThread=function(){var e,t;(e=$.id("fT"+Main.tid))&&((t=document.createElement("a")).href="javascript:;",t.textContent="Embed",t.addEventListener("click",SWFEmbed.toggleThread,!1),e.appendChild(document.createTextNode("-[")),e.appendChild(t),e.appendChild(document.createTextNode("]")))},SWFEmbed.processIndex=function(){var e,t,a,i,n,o,r;if(o=2,(i=$.cls("postblock")[0])&&(t=i.parentNode,(a=document.createElement("td")).className="postblock",t.insertBefore(a,t.children[o].nextElementSibling),i=$.cls("flashListing")[0]))for(n=$.tag("tr",i),e=1;t=n[e];++e)r=t.children[o].firstElementChild,(a=document.createElement("td")).innerHTML='[Embed]',a.firstElementChild.addEventListener("click",SWFEmbed.embedIndex,!1),t.insertBefore(a,t.children[o].nextElementSibling)},SWFEmbed.toggleThread=function(e){var t,a,i,n,o,r,s,d;if(t=$.id("swf-embed"))return t.parentNode.removeChild(t),void(e.target.textContent="Embed");a=$.tag("a",e.target.parentNode)[0],o=document.documentElement.clientWidth-(Main.hasMobileLayout?30:100),s=+a.getAttribute("data-width"),d=+a.getAttribute("data-height"),s>o&&(r=s/d,s=o,d=Math.round(o/r)),(t=document.createElement("div")).id="swf-embed",i=SWFEmbed.getFrameNode(a.href,s,d),t.appendChild(i),(n=$.id("m"+Main.tid)).insertBefore(t,n.firstChild),$.cls("thread")[0].scrollIntoView(!0),e.target.textContent="Remove"},SWFEmbed.embedIndex=function(e){var t,a,i,n,o,r,s,d,l,c,p,h,u,g,m,f;e.preventDefault(),u=10,g=20,m=(t=e.target.parentNode.parentNode.children[2].firstElementChild).getAttribute("title")||t.textContent,d=r=+t.getAttribute("data-width"),l=s=+t.getAttribute("data-height"),h=document.documentElement.clientWidth,p=document.documentElement.clientHeight-u-g,f=r/s,d>(c=h-u)&&(d=c,l=Math.round(c/f)),l>p&&(l=p,d=Math.round(p*f)),t=SWFEmbed.getFrameNode(e.target.href,d,l),(a=document.createElement("div")).style.position="fixed",a.style.width=d+"px",a.style.height=l+"px",a.style.top="50%",a.style.left="50%",a.style.marginTop=-l/2-g/2+"px",a.style.marginLeft=-d/2+"px",a.style.background="white",(i=document.createElement("div")).id="swf-embed-header",i.className="postblock",i.textContent=m+", "+r+"x"+s,(n=document.createElement("img")).id="swf-embed-close",n.className="pointer",n.src=Main.icons.cross,i.appendChild(n),a.appendChild(i),a.appendChild(t),(o=document.createElement("div")).id="swf-embed",o.style.cssText="width: 100%; height: 100%; position: fixed; top: 0; left: 0; background: rgba(128, 128, 128, 0.5)",o.appendChild(a),o.addEventListener("click",SWFEmbed.onBackdropClick,!1),document.body.appendChild(o)},SWFEmbed.getFrameNode=function(e,t,a){var i,n;return n=e.replace(/^https:\/\/i\.4cdn\.org\/f\//,""),(i=document.createElement("iframe")).setAttribute("allow","autoplay; fullscreen"),i.setAttribute("sandbox","allow-scripts allow-same-origin"),i.setAttribute("scrolling","no"),i.setAttribute("frameborder","0"),i.setAttribute("width",+t),i.setAttribute("height",+a),i.src=`//s.4cdn.org/media/flash/embed.html?4#${+t},${+a},${n},1`,i},SWFEmbed.onBackdropClick=function(e){var t=$.id("swf-embed");e.target!==t&&"swf-embed-close"!=e.target.id||(t.removeEventListener("click",SWFEmbed.onBackdropClick,!1),t.parentNode.removeChild(t))};var Linkify={init:function(){this.probeRe=/(?:^|[^\B"])https?:\/\/[-.a-z0-9]+\.[a-z]{2,4}/,this.linkRe=/(^|[^\B"])(https?:\/\/[-.a-z0-9\u200b]+\.[a-z\u200b]{2,15}(?:\/[^\s<>]*)?)/gi,this.punct=/[:!?,.'"]+$/g,this.derefer="//sys."+$L.d(Main.board)+"/derefer?url="},exec:function(e){this.probeRe.test(e.innerHTML)&&(e.innerHTML=e.innerHTML.replace(//g,"\u200b").replace(this.linkRe,this.funk).replace(/\u200b/g,""))},funk:function(e,t,a,i,n){var o,r,s,d,l;return o=i+e.length,""===n.slice(o,o+4)?e:(s=d=a.length,(o=a.match(Linkify.punct))&&(s-=o[0].length),(o=a.match(/\)+$/g))&&(r=o[0].length,(o=a.match(/\(/g))?(r-=o.length)>0&&(s-=r):s-=r),s'+a+""+l)}},Media={};Media.init=function(){this.matchSC=/(?:soundcloud\.com|snd\.sc)\/[^\s<]+(?:)?[^\s<]*/g,this.matchYT=/(?:youtube\.com\/watch\?[^\s]*?v=|youtu\.be\/)[^\s<]+(?:)?[^\s<]*(?:)?[^\s<]*(?:)?[^\s<]*/g,this.toggleYT=/(?:v=|\.be\/)([a-zA-Z0-9_-]{11})/,this.timeYT=/[\?#&]t=([ms0-9]+)/,this.map={yt:this.toggleYouTube,sc:this.toggleSoundCloud}},Media.parseSoundCloud=function(e){e.innerHTML=e.innerHTML.replace(this.matchSC,this.replaceSoundCloud)},Media.replaceSoundCloud=function(e,t,a){var i;if(Config.linkify){if('"'===a[t+e.length-1])return e;i=e+""}else i=""+e+"";return i+' [Embed]'},Media.toggleSoundCloud=function(e){var t,a;"Remove"==e.textContent?(e.parentNode.removeChild(e.nextElementSibling),e.textContent="Embed"):"Embed"==e.textContent&&(a=e.previousElementSibling.textContent,(t=new XMLHttpRequest).open("GET","//soundcloud.com/oembed?show_artwork=false&maxwidth=500px&show_comments=false&format=json&url=http://"+a.replace(/^https?:\/\//i,"")),t.onload=function(){var t;200==this.status||304==this.status?((t=document.createElement("div")).className="media-embed",t.innerHTML=JSON.parse(this.responseText).html,e.parentNode.insertBefore(t,e.nextElementSibling),e.textContent="Remove"):(e.textContent="Error",console.log("SoundCloud Error (HTTP "+this.status+")"))},e.textContent="Loading...",t.send(null))},Media.parseYouTube=function(e){e.innerHTML=e.innerHTML.replace(this.matchYT,this.replaceYouTube)},Media.replaceYouTube=function(e,t,a){var i;if(Config.linkify){if('"'===a[t+e.length-1])return e;i=e+""}else i=""+e+"";return i+' ['+(Main.hasMobileLayout?"Open":"Embed")+"]"},Media.showYTPreview=function(e){var t,a,i,n,o,r,s,d,l;s=320,d=180,l=5,n=e.getBoundingClientRect(),i=e.previousElementSibling.textContent.match(this.toggleYT)[1],o=n.right+s+l>$.docEl.clientWidth?n.left-s-l:n.right+l,r=n.top-d/2+n.height/2,(a=document.createElement("img")).width=s,a.height=d,a.alt="",a.src="//i1.ytimg.com/vi/"+encodeURIComponent(i)+"/mqdefault.jpg",(t=document.createElement("div")).id="yt-preview",t.className="reply",t.style.left=o+window.pageXOffset+"px",t.style.top=r+window.pageYOffset+"px",t.appendChild(a),document.body.appendChild(t)},Media.removeYTPreview=function(){var e;(e=$.id("yt-preview"))&&document.body.removeChild(e)},Media.toggleYouTube=function(e){var t,a,i,n;if("Remove"==e.textContent)e.parentNode.removeChild(e.nextElementSibling),e.textContent="Embed";else if(t=(n=e.previousElementSibling.textContent).match(this.toggleYT),a=n.match(this.timeYT),t&&(t=t[1])){if(t=encodeURIComponent(t),a&&(a=a[1])&&(t+="?start="+encodeURIComponent(a)),Main.hasMobileLayout)return void window.open("//www.youtube.com/watch?v="+t);(i=document.createElement("div")).className="media-embed",i.innerHTML='',e.parentNode.insertBefore(i,e.nextElementSibling),e.textContent="Remove"}else e.textContent="Error"},Media.toggleEmbed=function(e){var t,a=e.getAttribute("data-type");a&&(t=Media.map[a])&&t.call(this,e)};var StickyNav={thres:5,pos:0,timeout:null,el:null,init:function(){this.el=Config.classicNav?$.id("boardNavDesktop"):$.id("boardNavMobile"),$.addClass(this.el,"autohide-nav"),window.addEventListener("scroll",this.onScroll,!1)},onScroll:function(){clearTimeout(StickyNav.timeout),StickyNav.timeout=setTimeout(StickyNav.checkScroll,50)},checkScroll:function(){var e;e=window.pageYOffset,Math.abs(StickyNav.pos-e)<=StickyNav.thres||(e
    Custom CSSClose
    ',document.body.appendChild(e),e.addEventListener("click",this.onClick,!1),t=$.id("customCSSBox"),(a=localStorage.getItem("4chan-css"))&&(t.textContent=a),t.focus())},CustomCSS.save=function(){var e,t;(e=$.id("customCSSBox"))&&(localStorage.setItem("4chan-css",e.value),Config.customCSS&&(t=$.id("customCSS"))&&(document.head.removeChild(t),CustomCSS.init()))},CustomCSS.close=function(){var e;(e=$.id("customCSSMenu"))&&(e.removeEventListener("click",this.onClick,!1),document.body.removeChild(e))},CustomCSS.onClick=function(e){var t;if(t=e.target.getAttribute("data-cmd"))switch(t){case"css-close":CustomCSS.close();break;case"css-save":CustomCSS.save(),CustomCSS.close()}};var Keybinds={enabled:!1};Keybinds.init=function(){this.enabled=!0,this.map={65:function(){ThreadUpdater.enabled&&ThreadUpdater.toggleAuto()},70:function(){Config.filter&&Filter.addSelection()},81:function(){QR.enabled&&Main.tid&&QR.quotePost(Main.tid)},82:function(){ThreadUpdater.enabled&&ThreadUpdater.forceUpdate()},87:function(){Config.threadWatcher&&Main.tid&&ThreadWatcher.toggle(Main.tid)},66:function(){var e;(e=$.cls("prev")[0])&&(e=$.tag("form",e)[0])&&e.submit()},67:function(){location.href="/"+Main.board+"/catalog"},78:function(){var e;(e=$.cls("next")[0])&&(e=$.tag("form",e)[0])&&e.submit()},73:function(){location.href="/"+Main.board+"/"}},document.addEventListener("keydown",this.resolve,!1)},Keybinds.resolve=function(e){var t,a=e.target;Keybinds.enabled&&"TEXTAREA"!=a.nodeName&&"INPUT"!=a.nodeName&&(!(t=Keybinds.map[e.keyCode])||e.altKey||e.shiftKey||e.ctrlKey||e.metaKey||(e.preventDefault(),e.stopPropagation(),t()))},Keybinds.open=function(){var e;$.id("keybindsHelp")||((e=document.createElement("div")).id="keybindsHelp",e.className="UIPanel",e.setAttribute("data-cmd","keybinds-close"),e.innerHTML='
    Keyboard ShortcutsClose
    • Global
    • A — Toggle auto-updater
    • Q — Open Quick Reply
    • R — Update thread
    • W — Watch/Unwatch thread
    • B — Previous page
    • N — Next page
    • I — Return to index
    • C — Open catalog
    • F — Filter selected text
    • Quick Reply (always enabled)
    • Ctrl + Click the post number — Quote without linking
    • Ctrl + S — Spoiler tags
    • Esc — Close the Quick Reply
    ',document.body.appendChild(e),e.addEventListener("click",this.onClick,!1))},Keybinds.close=function(){var e;(e=$.id("keybindsHelp"))&&(e.removeEventListener("click",this.onClick,!1),document.body.removeChild(e))},Keybinds.onClick=function(e){var t;(t=e.target.getAttribute("data-cmd"))&&"keybinds-close"==t&&Keybinds.close()};var Del={deletePost:function(e,t){var a;confirm("Delete "+(t?"file":"post")+"?")&&((a={mode:window.thread_archived?"arcdel":"usrdel"})[e]="delete",t&&(a.onlyimgdel="on"),$.xhr("POST","https://sys."+$L.d(Main.board)+"/"+Main.board+"/imgboard.php",{onload:Del.onPostDeleted,onerror:Del.onError,withCredentials:!0,pid:e,file_only:t},a))},onPostDeleted:function(){var e;if(this.file_only){if((e=$.id("f"+this.pid))&&!(e=$.tag("img",e)[0]).hasAttribute("data-md5"))return}else e=$.id("pc"+this.pid);e&&(/Updating index|Can't find the post/.test(this.responseText)?$.hasClass(e,"deleted")||$.addClass(e,"deleted"):Del.onError())},onError:function(){Feedback.error("Something went wrong.")}},Report={init:function(){window.addEventListener("message",Report.onMessage,!1)}};Report.onMessage=function(e){var t;if(e.origin==="https://sys."+$L.d(Main.board)&&/^done-report/.test(e.data)){if(t=e.data.split("-")[2],Config.threadHiding&&$.id("t"+t))return void(ThreadHiding.isHidden(t)||(ThreadHiding.hide(t),ThreadHiding.save()));if($.id("p"+t))return void(ReplyHiding.isHidden(t)||(ReplyHiding.hide(t),ReplyHiding.save()))}},Report.open=function(e,t){var a,i;QR.noCaptcha?(a=205,i=""):(a=510,i=""),window.open("https://sys."+$L.d(Main.board)+"/"+(t||Main.board)+"/imgboard.php?mode=report&no="+e+i,Date.now(),"toolbar=0,scrollbars=1,location=0,status=1,menubar=0,resizable=1,width=380,height="+a)};var CustomMenu={};CustomMenu.initCtrl=function(){var e,t;(e=document.createElement("span")).className="custom-menu-ctrl",e.innerHTML='[Edit]',!Config.dropDownNav||Config.classicNav||Main.hasMobileLayout?((t=$.cls("boardList"))[0]&&t[0].appendChild(e),t[1]&&t[1].appendChild(e.cloneNode(!0))):(t=$.id("boardSelectMobile").parentNode).insertBefore(e,t.lastChild)},CustomMenu.reset=function(){var e,t,a,i,n;for(a=$.cls("boardList"),i=$.cls("customBoardList"),n=$.cls("show-all-boards"),e=0;t=n[e];++e)t.removeEventListener("click",CustomMenu.reset,!1);for(e=i.length-1;t=i[e];e--)a[e].style.display=null,t.parentNode.removeChild(t)},CustomMenu.apply=function(e){var t,a,i,n,o,r,s;if(e){for(r=e.split(/[^0-9a-z]/i),(s=document.createElement("span")).className="customBoardList",t=0;n=r[t];++t)t?s.appendChild(document.createTextNode(" / ")):s.appendChild(document.createTextNode("[")),(a=document.createElement("a")).textContent=n,a.href="//boards."+$L.d(n)+"/"+n+"/",s.appendChild(a);if(s.appendChild(document.createTextNode("]")),!Config.dropDownNav||Config.classicNav||Main.hasMobileLayout){for(s.appendChild(document.createTextNode(" [")),(a=document.createElement("a")).textContent="\u2026",a.title="Show all",a.className="show-all-boards pointer",s.appendChild(a),s.appendChild(document.createTextNode("] ")),i=s.cloneNode(!0),o=$.cls("boardList"),t=0;a=o[t];++t)a.style.display="none", +a.parentNode.insertBefore(t?i:s,a);for(o=$.cls("show-all-boards"),t=0;a=o[t];++t)a.addEventListener("click",CustomMenu.reset,!1)}else(a=$.cls("customBoardList")[0])&&a.parentNode.removeChild(a),(o=$.id("boardSelectMobile"))&&o.parentNode.insertBefore(s,o.nextSibling)}else!Config.dropDownNav||Config.classicNav||Main.hasMobileLayout||(a=$.cls("customBoardList")[0])&&a.parentNode.removeChild(a)},CustomMenu.onClick=function(e){var t;(t=e.target)!=document&&(t.hasAttribute("data-close")?CustomMenu.closeEditor():t.hasAttribute("data-save")&&CustomMenu.save($.id("customMenu").hasAttribute("data-standalone")))},CustomMenu.showEditor=function(e){var t;(t=document.createElement("div")).id="customMenu",t.className="UIPanel",t.setAttribute("data-close","1"),!0===e&&t.setAttribute("data-standalone","1"),t.innerHTML='
    Custom Board ListClose
    ',document.body.appendChild(t),Config.customMenuList&&($.id("customMenuBox").value=Config.customMenuList),t.addEventListener("click",CustomMenu.onClick,!1)},CustomMenu.closeEditor=function(){var e;(e=$.id("customMenu"))&&(e.removeEventListener("click",CustomMenu.onClick,!1),document.body.removeChild(e))},CustomMenu.save=function(e){var t;(t=$.id("customMenuBox"))&&(Config.customMenuList=t.value,!0===e&&(CustomMenu.apply(Config.customMenuList),Config.customMenu=!0,Config.save())),CustomMenu.closeEditor()};var Draggable={el:null,key:null,scrollX:null,scrollY:null,dx:null,dy:null,right:null,bottom:null,offsetTop:null,set:function(e){e.addEventListener("mousedown",Draggable.startDrag,!1)},unset:function(e){e.removeEventListener("mousedown",Draggable.startDrag,!1)},startDrag:function(e){var t,a,i;this.parentNode.hasAttribute("data-shiftkey")&&!e.shiftKey||(e.preventDefault(),t=Draggable,a=document.documentElement,t.el=this.parentNode,t.key=t.el.getAttribute("data-trackpos"),i=t.el.getBoundingClientRect(),t.dx=e.clientX-i.left,t.dy=e.clientY-i.top,t.right=a.clientWidth-i.width,t.bottom=a.clientHeight-i.height,"fixed"!=getComputedStyle(t.el,null).position?(t.scrollX=window.pageXOffset,t.scrollY=window.pageYOffset):t.scrollX=t.scrollY=0,t.offsetTop=Main.getDocTopOffset(),document.addEventListener("mouseup",t.endDrag,!1),document.addEventListener("mousemove",t.onDrag,!1))},endDrag:function(){document.removeEventListener("mouseup",Draggable.endDrag,!1),document.removeEventListener("mousemove",Draggable.onDrag,!1),Draggable.key&&(Config[Draggable.key]=Draggable.el.style.cssText,Config.save()),delete Draggable.el},onDrag:function(e){var t,a,i;t=e.clientX-Draggable.dx+Draggable.scrollX,a=e.clientY-Draggable.dy+Draggable.scrollY,i=Draggable.el.style,t<1?(i.left="0",i.right=""):Draggable.rightitalics signify bump/image limit has been met",!0]},"Filters & Post Hiding":{filter:['Filter and highlight specific threads/posts [Edit]',"Enable pattern-based filters"],threadHiding:['Thread hiding [Clear History]',"Hide entire threads by clicking the minus button",!0],hideStubs:["Hide thread stubs","Don't display stubs of hidden threads"]},Navigation:{threadExpansion:["Thread expansion","Expand threads inline on board indexes",!0],dropDownNav:["Use persistent drop-down navigation bar",""],classicNav:["Use traditional board list","",!1,!0],autoHideNav:["Auto-hide on scroll","",!1,!0],customMenu:['Custom board list [Edit]',"Only show selected boards in top and bottom board lists"],alwaysDepage:["Always use infinite scroll","Enable infinite scroll by default, so reaching the bottom of the board index will load subsequent pages",!0],topPageNav:["Page navigation at top of page","Show the page switcher at the top of the page, hold Shift and drag to move"],stickyNav:["Navigation arrows","Show top and bottom navigation arrows, hold Shift and drag to move"],keyBinds:['Use keyboard shortcuts [Show]',"Enable handy keyboard shortcuts for common actions"]},"Images & Media":{imageExpansion:["Image expansion","Enable inline image expansion, limited to browser width",!0],fitToScreenExpansion:["Fit expanded images to screen","Limit expanded images to both browser width and height"],imageHover:["Image hover","Mouse over images to view full size, limited to browser size"],imageHoverBg:["Set a background color for transparent images","",!1,!0],revealSpoilers:["Don't spoiler images","Show image thumbnail and original filename instead of spoiler placeholders",!0],unmuteWebm:["Un-mute WebM audio","Un-mute sound automatically for WebM playback",!0],noPictures:["Hide thumbnails","Don't display thumbnails while browsing",!0],embedYouTube:["Embed YouTube links","Embed YouTube player into replies"],embedSoundCloud:["Embed SoundCloud links","Embed SoundCloud player into replies"]},Miscellaneous:{linkify:["Linkify URLs","Make user-posted links clickable",!0],darkTheme:["Use a dark theme","Use the Tomorrow theme for nighttime browsing",!0,!1,!0],customCSS:['Custom CSS [Edit]',"Include your own CSS rules",!0],IDColor:["Color user IDs","Assign unique colors to user IDs on boards that use them",!0],compactThreads:["Force long posts to wrap","Long posts will wrap at 75% browser width"],centeredThreads:["Center threads","Align threads to the center of page",!1],localTime:["Convert dates to local time","Convert 4chan server time (US Eastern Time) to your local time",!0],forceHTTPS:["Always use HTTPS","Rewrite 4chan URLs to always use HTTPS",!0]}},SettingsMenu.save=function(){var e,t,a,i,n;for(n={},$.extend(n,Config),t=$.id("settingsMenu").getElementsByClassName("menuOption"),e=0;a=t[e];++e)i=a.getAttribute("data-option"),Config[i]="checkbox"==a.type?a.checked:a.value;Config.save(n),UA.dispatchEvent("4chanSettingsSaved"),SettingsMenu.close(),location.href=location.href.replace(/#.+$/,"")},SettingsMenu.toggle=function(){$.id("settingsMenu")?SettingsMenu.close():SettingsMenu.open()},SettingsMenu.open=function(){var e,t,a,i,n,o,r,s,d;if(Main.firstRun&&((d=$.id("settingsTip"))&&d.parentNode.removeChild(d),(d=$.id("settingsTipBottom"))&&d.parentNode.removeChild(d),Config.save()),(o=document.createElement("div")).id="settingsMenu",o.className="UIPanel",n='
    SettingsClose
      ',n+='',Main.hasMobileLayout){a={};for(t in SettingsMenu.options){s={},r=SettingsMenu.options[t];for(i in r)r[i][2]&&(s[i]=r[i]);for(e in s){a[t]=s;break}}}else a=SettingsMenu.options;for(t in a){r=a[t],n+='
      • '+t+'
        • ';for(i in r)r[i][4]&&!Main.hasMobileLayout||(n+="':">")+'"+(!1!==r[i][1]?'
        • ':'">')+r[i][1]:"")+"
        • ");n+="
      "}n+='
    ',o.innerHTML=n,o.addEventListener("click",SettingsMenu.onClick,!1),document.body.appendChild(o),Main.firstRun&&SettingsMenu.expandAll(),(d=$.cls("menuOption",o)[0])&&d.focus()},SettingsMenu.showExport=function(){var e,t,a;$.id("exportSettings")||(t=location.href.replace(location.hash,"").replace(/^http:/,"https:")+"#cfg="+Config.toURL(),(e=document.createElement("div")).id="exportSettings",e.className="UIPanel",e.setAttribute("data-cmd","export-close"),e.innerHTML='
    Export SettingsClose

    Copy and save the URL below, and visit it from another browser or computer to restore your extension and catalog settings.

    Alternatively, you can drag the link below into your bookmarks bar and click it to restore.

    [Restore 4chan Settings]

    ',document.body.appendChild(e),e.addEventListener("click",this.onExportClick,!1),(a=$.cls("export-field",e)[0]).focus(),a.select())},SettingsMenu.closeExport=function(){var e;(e=$.id("exportSettings"))&&(e.removeEventListener("click",this.onExportClick,!1),document.body.removeChild(e))},SettingsMenu.onExportClick=function(e){"exportSettings"==e.target.id&&(e.preventDefault(),e.stopPropagation(),SettingsMenu.closeExport())},SettingsMenu.expandAll=function(){var e,t,a=$.cls("settings-expand");for(e=0;t=a[e];++e)t.src=Main.icons.minus,t.parentNode.nextElementSibling.style.display="block"},SettingsMenu.toggleCat=function(e){var t,a,i=e.parentNode.nextElementSibling;i.style.display?(a="",t="plus"):(a="block",t="minus"),i.style.display=a,e.parentNode.firstElementChild.src=Main.icons[t]},SettingsMenu.onClick=function(e){var t,a;a=e.target,$.hasClass(a,"settings-expand")?SettingsMenu.toggleCat(a):"settings-exp-all"==a.getAttribute("data-cmd")?(e.preventDefault(),SettingsMenu.expandAll()):"settingsMenu"==a.id&&(t=$.id("settingsMenu"))&&(e.preventDefault(),SettingsMenu.close(t))},SettingsMenu.close=function(e){(e=e||$.id("settingsMenu"))&&(e.removeEventListener("click",SettingsMenu.onClick,!1),document.body.removeChild(e))};var Feedback={messageTimeout:null,showMessage:function(e,t,a,i){var n;Feedback.hideMessage(),(n=document.createElement("div")).id="feedback",n.title="Dismiss",n.innerHTML='",$.on(n,"click",i||Feedback.hideMessage),document.body.appendChild(n),a&&(Feedback.messageTimeout=setTimeout(Feedback.hideMessage,a))},hideMessage:function(){var e=$.id("feedback");e&&(Feedback.messageTimeout&&(clearTimeout(Feedback.messageTimeout),Feedback.messageTimeout=null),$.off(e,"click",Feedback.hideMessage),document.body.removeChild(e))},error:function(e,t){t===undefined&&(t=5e3),Feedback.showMessage(e||"Something went wrong","error",t)},notify:function(e,t){t===undefined&&(t=3e3),Feedback.showMessage(e,"notify",t)}},Main={};Main.addTooltip=function(e,t,a){var i,n;return(i=document.createElement("div")).className="click-me",a&&(i.id=a),i.innerHTML=t||"Change your settings",e.parentNode.appendChild(i),n=(e.offsetWidth-i.offsetWidth+e.offsetLeft-i.offsetLeft)/2,i.style.marginLeft=n+"px",i},Main.getDocTopOffset=function(){return Config.dropDownNav&&!Config.autoHideNav?$.id(Config.classicNav?"boardNavDesktop":"boardNavMobile").offsetHeight:0},Main.init=function(){var e;document.addEventListener("DOMContentLoaded",Main.run,!1),Main.now=Date.now(),Main.is_4channel="boards.4channel.org"===location.host,UA.init(),Config.load(),Config.forceHTTPS&&"https:"!=location.protocol?location.href=location.href.replace(/^http:/,"https:"):(Main.firstRun&&Config.loadFromURL()&&(Main.firstRun=!1),(Main.stylesheet=Main.getCookie(style_group))?Main.stylesheet=Main.stylesheet.toLowerCase().replace(/ /g,"_"):Main.stylesheet="nws_style"==style_group?"yotsuba_new":"yotsuba_b_new",QR.noCaptcha=QR.noCaptcha||window.passEnabled,Main.initIcons(),Main.addCSS(),Main.type=style_group.split("_")[0],e=location.pathname.split(/\//),Main.board=e[1],Main.page=e[2],Main.tid=e[3],UA.dispatchEvent("4chanMainInit"))},Main.initPersistentNav=function(){var e,t,a;t=$.id("boardNavDesktop"),a=$.id("boardNavDesktopFoot"),Config.classicNav?((e=document.createElement("div")).className="pageJump",e.innerHTML='SettingsHome
    ',t.appendChild(e),$.id("settingsWindowLinkClassic").addEventListener("click",SettingsMenu.toggle,!1),$.addClass(t,"persistentNav")):(t.style.display="none",$.removeClass($.id("boardNavMobile"),"mobile")),a.style.display="none",$.addClass(document.body,"hasDropDownNav")},Main.checkMobileLayout=function(){var e,t;return window.matchMedia?window.matchMedia("(max-width: 480px)").matches&&"true"!=localStorage.getItem("4chan_never_show_mobile"):(e=$.id("boardNavMobile"),t=$.id("boardNavDesktop"),e&&t&&e.offsetWidth>0&&0===t.offsetWidth)},Main.disableDarkTheme=function(){Config.darkTheme=!1,localStorage.setItem("4chan-settings",JSON.stringify(Config))},Main.run=function(){var e,t;document.removeEventListener("DOMContentLoaded",Main.run,!1),document.addEventListener("click",Main.onclick,!1),$.id("settingsWindowLink").addEventListener("click",SettingsMenu.toggle,!1),$.id("settingsWindowLinkBot").addEventListener("click",SettingsMenu.toggle,!1),$.id("settingsWindowLinkMobile").addEventListener("click",SettingsMenu.toggle,!1),Main.isOekakiBoard="i"===Main.board,Main.hasMobileLayout=Main.checkMobileLayout(),Main.isMobileDevice=/Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent),Search.init(),Config.disableAll||(Report.init(),Main.hasMobileLayout?$.extend(Config,ConfigMobile):((e=$.id("bottomReportBtn"))&&(e.style.display="none"),Main.isMobileDevice&&$.addClass(document.body,"isMobileDevice")),Config.linkify&&Linkify.init(),Config.IDColor&&IDColor.init(),Config.customCSS&&CustomCSS.init(),Config.keyBinds&&Keybinds.init(),Main.firstRun&&Main.isMobileDevice&&(Config.topPageNav=!1,Config.dropDownNav=!0),Config.dropDownNav&&!Main.hasMobileLayout&&Main.initPersistentNav(),$.addClass(document.body,Main.stylesheet),$.addClass(document.body,Main.type),Config.darkTheme&&($.addClass(document.body,"m-dark"),Main.hasMobileLayout||$.cls("stylechanger")[0].addEventListener("change",Main.disableDarkTheme,!1)),Config.compactThreads?$.addClass(document.body,"compact"):Config.centeredThreads&&$.addClass(document.body,"centeredThreads"),Config.noPictures&&$.addClass(document.body,"noPictures"),Config.customMenu&&CustomMenu.apply(Config.customMenuList),CustomMenu.initCtrl(),(Config.quotePreview||Config.imageHover||Config.filter)&&((t=$.id("delform")||$.id("arc-list")).addEventListener("mouseover",Main.onThreadMouseOver,!1),t.addEventListener("mouseout",Main.onThreadMouseOut,!1)),Config.stickyNav&&Main.setStickyNav(),Main.hasMobileLayout?StickyNav.init():(Main.initGlobalMessage(),Config.autoHideNav&&StickyNav.init()),Config.threadExpansion&&ThreadExpansion.init(),Config.filter&&Filter.init(),Config.threadWatcher&&ThreadWatcher.init(),(Main.hasMobileLayout||Config.embedSoundCloud||Config.embedYouTube)&&Media.init(),ReplyHiding.init(),Config.quotePreview&&QuotePreview.init(),Parser.init(),Main.tid?(Main.threadClosed=!document.forms.post||!!$.cls("closedIcon")[0],Main.threadSticky=!!$.cls("stickyIcon",$.id("pi"+Main.tid))[0],Config.threadStats&&ThreadStats.init(),Parser.parseThread(Main.tid),Config.threadUpdater&&ThreadUpdater.init()):(Main.page||Depager.init(),Config.topPageNav&&Main.setPageNav(),Config.threadHiding?(ThreadHiding.init(),Parser.parseBoard()):Parser.parseBoard()),"f"===Main.board&&SWFEmbed.init(),Config.quickReply&&QR.init(),ReplyHiding.purge(),Config.alwaysDepage&&!Main.hasMobileLayout&&$.docEl.scrollHeight<=$.docEl.clientHeight&&Depager.depage())},Main.isThreadClosed=function(e){var t;return window.thread_archived||(t=$.id("pi"+e))&&$.cls("closedIcon",t)[0]},Main.setThreadState=function(e,t){var a,i,n,o;o=e.charAt(0).toUpperCase()+e.slice(1),t?(a=$.cls("postNum",$.id("pi"+Main.tid))[0],(i=document.createElement("img")).className=e+"Icon retina",i.title=o,i.src=Main.icons2[e],"sticky"==e&&(n=$.cls("closedIcon",a)[0])?(a.insertBefore(i,n),a.insertBefore(document.createTextNode(" "),n)):(a.appendChild(document.createTextNode(" ")),a.appendChild(i))):(i=$.cls(e+"Icon",$.id("pi"+Main.tid))[0])&&(i.parentNode.removeChild(i.previousSibling),i.parentNode.removeChild(i)),Main["thread"+o]=t},Main.icons={up:"arrow_up.png",down:"arrow_down.png",right:"arrow_right.png",download:"arrow_down2.png",refresh:"refresh.png",cross:"cross.png",gis:"gis.png",iqdb:"iqdb.png",minus:"post_expand_minus.png",plus:"post_expand_plus.png",rotate:"post_expand_rotate.gif",quote:"quote.png",report:"report.png",notwatched:"watch_thread_off.png",watched:"watch_thread_on.png",help:"question.png"},Main.icons2={archived:"archived.gif",closed:"closed.gif",sticky:"sticky.gif",trash:"trash.gif"},Main.initIcons=function(){var e,t,a;if(t={yotsuba_new:"futaba/",futaba_new:"futaba/",yotsuba_b_new:"burichan/",burichan_new:"burichan/",tomorrow:"tomorrow/",photon:"photon/"},a="//s.4cdn.org/image/",window.devicePixelRatio>=2){for(e in Main.icons)Main.icons[e]=Main.icons[e].replace(".","@2x.");for(e in Main.icons2)Main.icons2[e]=Main.icons2[e].replace(".","@2x.")}for(e in Main.icons2)Main.icons2[e]=a+Main.icons2[e];a+="buttons/"+t[Main.stylesheet];for(e in Main.icons)Main.icons[e]=a+Main.icons[e]},Main.setPageNav=function(){var e,t;(t=document.createElement("div")).setAttribute("data-shiftkey","1"),t.setAttribute("data-trackpos","TN-position"),t.className="topPageNav",Config["TN-position"]?t.style.cssText=Config["TN-position"]:(t.style.left="10px",t.style.top="50px"),(e=$.cls("pagelist")[0])&&(e=e.cloneNode(!0),t.appendChild(e),Draggable.set(e),document.body.appendChild(t))},Main.getWebmVolume=function(){let e=parseFloat(localStorage.getItem("4chan-volume"));return isNaN(e)?.5:e},Main.getWebmVolumeChangeCb=function(){let e;return t=>{clearTimeout(e),e=setTimeout(()=>{localStorage.setItem("4chan-volume",t.target.volume)},200)}},Main.initGlobalMessage=function(){var e,t,a,i;(e=$.id("globalMessage"))&&e.textContent&&(e.nextElementSibling.style.clear="both",(t=document.createElement("img")).id="toggleMsgBtn",t.className="extButton",t.setAttribute("data-cmd","toggleMsg"),t.alt="Toggle",t.title="Toggle announcement",i=localStorage.getItem("4chan-global-msg"),a=e.getAttribute("data-utc"),i&&a<=i?(e.style.display="none",t.style.opacity="0.5",t.src=Main.icons.plus):t.src=Main.icons.minus,e.parentNode.insertBefore(t,e))},Main.toggleGlobalMessage=function(){var e,t;e=$.id("globalMessage"),t=$.id("toggleMsgBtn"),"none"==e.style.display?(e.style.display="",t.src=Main.icons.minus,t.style.opacity="1",localStorage.removeItem("4chan-global-msg")):(e.style.display="none",t.src=Main.icons.plus,t.style.opacity="0.5",localStorage.setItem("4chan-global-msg",e.getAttribute("data-utc")))},Main.setStickyNav=function(){var e,t;(e=document.createElement("div")).id="stickyNav",e.className="extPanel reply",e.setAttribute("data-shiftkey","1"),e.setAttribute("data-trackpos","SN-position"),Config["SN-position"]?e.style.cssText=Config["SN-position"]:(e.style.right="10px",e.style.top="50px"),(t=document.createElement("div")).innerHTML='\u25b2\u25bc',Draggable.set(t),e.appendChild(t),document.body.appendChild(e)},Main.getCookie=function(e){var t,a,i,n;for(n=e+"=",i=document.cookie.split(";"),t=0;a=i[t];++t){for(;" "==a.charAt(0);)a=a.substring(1,a.length);if(0===a.indexOf(n))return decodeURIComponent(a.substring(n.length,a.length))}return null},Main.setCookie=function(e,t,a){var i=new Date;i.setTime(i.getTime()+31536e6),a||(a=location.host),document.cookie=e+"="+t+"; expires="+i.toGMTString()+"; path=/; domain="+a},Main.removeCookie=function(e,t,a){t||(t=location.host),a||(a="/"),document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:01 GMT;; path="+a+"; domain="+t},Main.onclick=function(e){var t,a,i,n;if((t=e.target)!=document){if(a=t.getAttribute("data-cmd"))switch(n=t.getAttribute("data-id"),a){case"update":e.preventDefault(),ThreadUpdater.forceUpdate();break;case"post-menu":e.preventDefault(),PostMenu.open(t);break;case"auto":ThreadUpdater.toggleAuto();break;case"totop":case"tobottom":e.shiftKey||(location.href="#"+a.slice(2));break;case"hide":ThreadHiding.toggle(n);break;case"watch":ThreadWatcher.toggle(n);break;case"hide-r":t.hasAttribute("data-recurse")?ReplyHiding.toggleR(n):ReplyHiding.toggle(n);break;case"expand":ThreadExpansion.toggle(n);break;case"open-qr":e.preventDefault(),QR.show(Main.tid),$.tag("textarea",document.forms.qrPost)[0].focus();break;case"qr-painter-draw":QR.openPainter();break;case"qr-painter-clear":QR.onPainterCancel();break;case"qr-painter-edit":e.preventDefault(),QR.onOpenInPainterClick(t);break;case"unfilter":Filter.unfilter(t);break;case"depage":e.preventDefault(),Depager.toggle();break;case"report":Report.open(n,t.getAttribute("data-board"));break;case"filter-sel":e.preventDefault(),Filter.addSelection();break;case"embed":Media.toggleEmbed(t);break;case"sound":ThreadUpdater.toggleSound();break;case"toggleMsg":Main.toggleGlobalMessage();break;case"settings-toggle":SettingsMenu.toggle();break;case"settings-save":SettingsMenu.save();break;case"keybinds-open":Keybinds.open();break;case"filters-open":Filter.open();break;case"thread-hiding-clear":ThreadHiding.clear();break;case"css-open":CustomCSS.open();break;case"settings-export":SettingsMenu.showExport();break;case"export-close":SettingsMenu.closeExport();break;case"custom-menu-edit":e.preventDefault(),CustomMenu.showEditor($.hasClass(t.parentNode,"custom-menu-ctrl"));break;case"del-post":case"del-file":e.preventDefault(),Del.deletePost(n,"del-file"===a);break;case"open-tex-preview":QR.openTeXPreview();break;case"close-tex-preview":QR.closeTeXPreview()}else if(!Config.disableAll)if(QR.enabled&&"Reply to this post"==t.title)e.preventDefault(),i=Main.tid||t.previousElementSibling.getAttribute("href").split("#")[0].split("/").slice(-1)[0],QR.quotePost(i,!e.ctrlKey&&t.textContent);else{if(Config.imageExpansion&&1==e.which&&t.parentNode&&$.hasClass(t.parentNode,"fileThumb")&&"A"==t.parentNode.nodeName&&!$.hasClass(t.parentNode,"deleted")&&!$.hasClass(t,"mFileInfo"))return void(ImageExpansion.toggle(t)&&e.preventDefault());Config.inlineQuotes&&1==e.which&&$.hasClass(t,"quotelink")&&"archive"!==Main.page?e.shiftKey?(e.preventDefault(),window.location=t.href):QuoteInline.toggle(t,e):Config.threadExpansion&&t.parentNode&&$.hasClass(t.parentNode,"abbr")?(e.preventDefault(),ThreadExpansion.expandComment(t)):Main.isMobileDevice&&Config.quotePreview&&$.hasClass(t,"quotelink")&&t.getAttribute("href").match(QuotePreview.regex)?e.preventDefault():$.hasClass(t,"mFileInfo")&&(e.preventDefault(),e.stopPropagation())}!Main.hasMobileLayout||!Config.disableAll&&Config.imageExpansion||t.parentNode&&t.parentNode.hasAttribute("data-m")&&ImageExpansion.setMobileSrc(t.parentNode)}},Main.onThreadMouseOver=function(e){var t=e.target;Config.quotePreview&&$.hasClass(t,"quotelink")&&!$.hasClass(t,"deadlink")&&!$.hasClass(t,"linkfade")?QuotePreview.resolve(e.target):Config.imageHover&&(t.hasAttribute("data-md5")&&!$.hasClass(t.parentNode,"deleted")||t.href&&!$.hasClass(t.parentNode,"fileText")&&/(i\.4cdn|is\.4chan)\.org\/[a-z0-9]+\/[0-9]+\.(gif|jpg|png|webm)$/.test(t.href))?ImageHover.show(t):$.hasClass(t,"dateTime")?Parser.onDateMouseOver(t):$.hasClass(t,"hand")?Parser.onUIDMouseOver(t):Config.embedYouTube&&"yt"===t.getAttribute("data-type")&&!Main.hasMobileLayout?Media.showYTPreview(t):Config.filter&&t.hasAttribute("data-filtered")&&QuotePreview.show(t,t.href?t.parentNode.parentNode.parentNode:t.parentNode.parentNode)},Main.onThreadMouseOut=function(e){var t=e.target;Config.quotePreview&&$.hasClass(t,"quotelink")?QuotePreview.remove(t):Config.imageHover&&(t.hasAttribute("data-md5")||t.href&&!$.hasClass(t.parentNode,"fileText")&&/(i\.4cdn|is\.4chan)\.org\/[a-z0-9]+\/[0-9]+\.(gif|jpg|png|webm)$/.test(t.href))?ImageHover.hide():$.hasClass(t,"dateTime")||$.hasClass(t,"hand")?Parser.onTipMouseOut(t):Config.embedYouTube&&"yt"===t.getAttribute("data-type")&&!Main.hasMobileLayout?Media.removeYTPreview():Config.filter&&t.hasAttribute("data-filtered")&&QuotePreview.remove(t)},Main.linkToThread=function(e,t,a){return"//"+location.host+"/"+(t||Main.board)+"/thread/"+e+(a>0?"#p"+a:"")},Main.addCSS=function(){ +var e,t='body.hasDropDownNav { margin-top: 45px;}.extButton.threadHideButton { float: left; margin-right: 5px; margin-top: -1px;}.extButton.replyHideButton { margin-top: 1px;}div.op > span .postHideButtonCollapsed { margin-right: 1px;}.dropDownNav #boardNavMobile, { display: block !important;}.extPanel { border: 1px solid rgba(0, 0, 0, 0.20);}.tomorrow .extPanel { border: 1px solid #111;}.extButton,img.pointer { width: 18px; height: 18px;}.extControls { display: inline; margin-left: 5px;}.extButton { cursor: pointer; margin-bottom: -4px;}.trashIcon { width: 16px; height: 16px; margin-bottom: -2px; margin-left: 5px;}.threadUpdateStatus { margin-left: 0.5ex;}.futaba_new .stub,.burichan_new .stub { line-height: 1; padding-bottom: 1px;}.stub .extControls,.stub .wbtn,.stub input { display: none;}.stub .threadHideButton { float: none; margin-right: 2px;}.right { float: right;}.center { display: block; margin: auto;}.pointer { cursor: pointer;}.drag { cursor: move !important; user-select: none !important; -moz-user-select: none !important; -webkit-user-select: none !important;}#quickReply { display: block; position: fixed; padding: 2px; font-size: 10pt;}#qrepHeader,#qrHeader { font-size: 10pt; text-align: center; margin-bottom: 1px; padding: 0; height: 18px; line-height: 18px;}#qrHeader .left { float: left; margin-left: 3px; }#qrepClose,#qrClose { float: right;}#qrCaptchaContainer { width: 300px; background-color: #eee; overflow: hidden; margin-bottom: 3px }.tomorrow #qrCaptchaContainer.t-qr-root { background-color: #323232; }.tomorrow #qrCaptchaContainer #t-cnt { filter: invert(85%); }#qrForm > div { clear: both;}#quickReply input[type="text"],#quickReply textarea,#quickReply #recaptcha_response_field { border: 1px solid #aaa; font-family: arial,helvetica,sans-serif; font-size: 10pt; outline: medium none; width: 296px; padding: 2px; margin: 0 0 1px 0;}.tomorrow #quickReply input[type="text"],.tomorrow #quickReply textarea,.tomorrow #quickReply #recaptcha_response_field { border: 1px solid #515151; background-color: #282a2e; color: #c5c8c6;}.tomorrow #quickReply input[type="text"]:focus,.tomorrow #quickReply textarea:focus { border: 1px solid #757575;}#quickReply textarea { min-width: 296px; float: left;}.tomorrow #quickReply input::placeholder { color: 919191 !important;}#quickReply input[type="submit"] { width: 75px; margin: 0; float: right;}#quickReply #qrCapField { display: block; margin-top: 1px;}#quickReply input.presubmit { margin-right: 1px; width: 212px; float: left;}#qrFile { width: 130px; margin-right: 5px;}.yotsuba_new #qrFile { color:black;}#qrSpoiler { display: inline;}#qrError { width: 292px; display: none; font-family: monospace; background-color: #E62020; font-size: 12px; color: white; padding: 3px 5px; text-shadow: 0 1px rgba(0, 0, 0, 0.20); clear: both;}#qrError a:hover,#qrError a { color: white !important; text-decoration: underline;}#twHeader { font-weight: bold; text-align: center; height: 17px;}.futaba_new #twHeader,.burichan_new #twHeader { line-height: 1;}#twPrune { margin-left: 3px; margin-top: -1px;}#twClose { float: left; margin-top: -1px;}#threadWatcher { max-width: 265px; display: block; position: absolute; padding: 3px;}#watchList { margin: 0; padding: 0; user-select: none; -moz-user-select: none; -webkit-user-select: none;}#watchList li:first-child { margin-top: 3px; padding-top: 2px; border-top: 1px solid rgba(0, 0, 0, 0.20);}.photon #watchList li:first-child { border-top: 1px solid #ccc;}.yotsuba_new #watchList li:first-child { border-top: 1px solid #d9bfb7;}.yotsuba_b_new #watchList li:first-child { border-top: 1px solid #b7c5d9;}.tomorrow #watchList li:first-child { border-top: 1px solid #111;}#watchList a { text-decoration: none;}#watchList li { overflow: hidden; white-space: nowrap; text-overflow: ellipsis;}div.post div.image-expanded { display: table;}div.op div.file .image-expanded-anti { margin-left: -3px;}#quote-preview { display: block; position: absolute; top: 0; padding: 3px 6px 6px 3px; margin: 0;}#quote-preview .dateTime { white-space: nowrap;}#quote-preview.reveal-spoilers s { background-color: #aaa !important; color: inherit !important; text-decoration: none !important;}#quote-preview.reveal-spoilers s a { background: transparent !important; text-decoration: underline;}.yotsuba_b_new #quote-preview.reveal-spoilers s a,.burichan_new #quote-preview.reveal-spoilers s a { color: #D00 !important;}.yotsuba_new #quote-preview.reveal-spoilers s a,.futaba_new #quote-preview.reveal-spoilers s a { color: #000080 !important;}.tomorrow #quote-preview.reveal-spoilers s { color: #000 !important; }.tomorrow #quote-preview.reveal-spoilers s a { color: #5F89AC !important; }.photon #quote-preview.reveal-spoilers s a { color: #FF6600 !important;}.yotsuba_new #quote-preview.highlight,.yotsuba_b_new #quote-preview.highlight { border-width: 1px 2px 2px 1px !important; border-style: solid !important;}.yotsuba_new #quote-preview.highlight { border-color: #D99F91 !important;}.yotsuba_b_new #quote-preview.highlight { border-color: #BA9DBF !important;}.yotsuba_b_new .highlight-anti,.burichan_new .highlight-anti { border-width: 1px !important; background-color: #bfa6ba !important;}.yotsuba_new .highlight-anti,.futaba_new .highlight-anti { background-color: #e8a690 !important;}.tomorrow .highlight-anti { background-color: #111 !important; border-color: #111;}.photon .highlight-anti { background-color: #bbb !important;}.op.inlined { display: block;}#quote-preview .inlined,#quote-preview .postMenuBtn,#quote-preview .extButton,#quote-preview .extControls { display: none;}.hasNewReplies { font-weight: bold; }.hasYouReplies { font-style: italic; }.archivelink { opacity: 0.5;}.deadlink { text-decoration: line-through !important;}div.backlink { font-size: 0.8em !important; display: inline; padding: 0; padding-left: 5px;}.backlink.mobile { padding: 3px 5px; display: block; clear: both !important; line-height: 2;}.op .backlink.mobile,#quote-preview .backlink.mobile { display: none !important;}.backlink.mobile .quoteLink { padding-right: 2px;}.backlink span { padding: 0;}.burichan_new .backlink a,.yotsuba_b_new .backlink a { color: #34345C !important;}.burichan_new .backlink a:hover,.yotsuba_b_new .backlink a:hover { color: #dd0000 !important;}.expbtn { margin-right: 3px; margin-left: 2px;}.tCollapsed .rExpanded { display: none;}#stickyNav { position: fixed; font-size: 0;}#stickyNav img { vertical-align: middle;}.tu-error { color: red;}.topPageNav { position: absolute;}.yotsuba_b_new .topPageNav { border-top: 1px solid rgba(255, 255, 255, 0.25); border-left: 1px solid rgba(255, 255, 255, 0.25);}.newPostsMarker:not(#quote-preview) { box-shadow: 0 3px red;}#toggleMsgBtn { float: left; margin-bottom: 6px;}.panelHeader { font-weight: bold; font-size: 16px; text-align: center; margin-bottom: 5px; margin-top: 5px; padding-bottom: 5px; border-bottom: 1px solid rgba(0, 0, 0, 0.20);}.yotsuba_new .panelHeader { border-bottom: 1px solid #d9bfb7;}.yotsuba_b_new .panelHeader { border-bottom: 1px solid #b7c5d9;}.tomorrow .panelHeader { border-bottom: 1px solid #111;}.panelHeader .panelCtrl { position: absolute; right: 5px; top: 5px;}.UIMenu,.UIPanel { position: fixed; width: 100%; height: 100%; top: 0; left: 0; z-index: 100002;}.UIPanel { line-height: 14px; font-size: 14px; background-color: rgba(0, 0, 0, 0.25);}.UIPanel:after { display: inline-block; height: 100%; vertical-align: middle; content: "";}.UIPanel > div { -moz-box-sizing: border-box; box-sizing: border-box; display: inline-block; height: auto; max-height: 100%; position: relative; width: 400px; left: 50%; margin-left: -200px; overflow: auto; box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); vertical-align: middle;}#settingsMenu > div { top: 25px;; vertical-align: top; max-height: 85%;}.extPanel input[type="text"],.extPanel textarea { border: 1px solid #AAA; outline: none;}.UIPanel .center { margin-bottom: 5px;}.UIPanel button { display: inline-block; margin-right: 5px;}.UIPanel code { background-color: #eee; color: #000000; padding: 1px 4px; font-size: 12px;}.UIPanel ul { list-style: none; padding: 0; margin: 0 0 10px;}.UIPanel .export-field { width: 385px;}#settingsMenu label input { margin-right: 5px;}.tomorrow #settingsMenu ul { border-bottom: 1px solid #282a2e;}.settings-off { padding-left: 3px;}.settings-cat-lbl { font-weight: bold; margin: 10px 0 5px; padding-left: 5px;}.settings-cat-lbl img { vertical-align: text-bottom; margin-right: 5px; cursor: pointer; width: 18px; height: 18px;}.settings-tip { font-size: 0.85em; margin: 2px 0 5px 0; padding-left: 23px;}#settings-exp-all { padding-left: 7px; text-align: center;}#settingsMenu .settings-cat { display: none; margin-left: 3px;}#tex-preview-cnt .extPanel { width: 600px; margin-left: -300px; }#tex-preview-cnt textarea,#customCSSMenu textarea { display: block; max-width: 100%; min-width: 100%; -moz-box-sizing: border-box; box-sizing: border-box; height: 200px; margin: 0 0 5px; font-family: monospace;}#tex-preview-cnt textarea { height: 75px; }#output-tex-preview { min-height: 75px; white-space: pre; padding: 0 3px; -moz-box-sizing: border-box; box-sizing: border-box;}#tex-protip { font-size: 11px; margin: 5px 0; text-align: center; }a.tex-logo sub { pointer-events: none; }#customCSSMenu .right,#settingsMenu .right { margin-top: 2px;}#settingsMenu label { display: inline-block; user-select: none; -moz-user-select: none; -webkit-user-select: none;}#filtersHelp > div { width: 600px; left: 50%; margin-left: -300px;}#filtersHelp h4 { font-size: 15px; margin: 20px 0 0 10px;}#filtersHelp h4:before { content: "\xbb"; margin-right: 3px;}#filtersHelp ul { padding: 0; margin: 10px;}#filtersHelp li { padding: 3px 0; list-style: none;}#filtersMenu table { width: 100%;}#filtersMenu th { font-size: 12px;}#filtersMenu tbody { text-align: center;}#filtersMenu select,#filtersMenu .fPattern,#filtersMenu .fBoards,#palette-custom-input { padding: 1px; font-size: 11px;}#filtersMenu select { width: 75px;}#filtersMenu tfoot td { padding-top: 10px;}#keybindsHelp li { padding: 3px 5px;}.fPattern { width: 110px;}.fBoards { width: 25px;}.fColor { width: 60px;}.fDel { font-size: 16px;}.filter-preview { cursor: pointer; margin-left: 3px;}#quote-preview iframe,#quote-preview .filter-preview { display: none;}.post-hidden .extButton,.post-hidden:not(#quote-preview) .postInfo { opacity: 0.5;}.post-hidden:not(.thread) .postInfo { padding-left: 5px;}.post-hidden:not(#quote-preview) input,.post-hidden:not(#quote-preview) .replyContainer,.post-hidden:not(#quote-preview) .summary,.post-hidden:not(#quote-preview) .op .file,.post-hidden:not(#quote-preview) .file,.post-hidden .wbtn,.post-hidden .postNum span,.post-hidden:not(#quote-preview) .backlink,div.post-hidden:not(#quote-preview) div.file,div.post-hidden:not(#quote-preview) blockquote.postMessage { display: none;}.click-me { border-radius: 5px; margin-top: 5px; padding: 2px 5px; position: absolute; font-weight: bold; z-index: 2; white-space: nowrap;}.yotsuba_new .click-me,.futaba_new .click-me { color: #800000; background-color: #F0E0D6; border: 2px solid #D9BFB7;}.yotsuba_b_new .click-me,.burichan_new .click-me { color: #000; background-color: #D6DAF0; border: 2px solid #B7C5D9;}.tomorrow .click-me { color: #C5C8C6; background-color: #282A2E; border: 2px solid #111;}.photon .click-me { color: #333; background-color: #ddd; border: 2px solid #ccc;}.click-me:before { content: ""; border-width: 0 6px 6px; border-style: solid; left: 50%; margin-left: -6px; position: absolute; width: 0; height: 0; top: -6px;}.yotsuba_new .click-me:before,.futaba_new .click-me:before { border-color: #D9BFB7 transparent;}.yotsuba_b_new .click-me:before,.burichan_new .click-me:before { border-color: #B7C5D9 transparent;}.tomorrow .click-me:before { border-color: #111 transparent;}.photon .click-me:before { border-color: #ccc transparent;}.click-me:after { content: ""; border-width: 0 4px 4px; top: -4px; display: block; left: 50%; margin-left: -4px; position: absolute; width: 0; height: 0;}.yotsuba_new .click-me:after,.futaba_new .click-me:after { border-color: #F0E0D6 transparent; border-style: solid;}.yotsuba_b_new .click-me:after,.burichan_new .click-me:after { border-color: #D6DAF0 transparent; border-style: solid;}.tomorrow .click-me:after { border-color: #282A2E transparent; border-style: solid;}.photon .click-me:after { border-color: #DDD transparent; border-style: solid;}#image-hover { position: fixed; max-width: 100%; max-height: 100%; top: 0px; right: 0px; z-index: 9002;}.thread-stats { float: right; margin-right: 5px; cursor: default;}.compact .thread { max-width: 75%;}.dotted { text-decoration: none; border-bottom: 1px dashed;}.linkfade { opacity: 0.5;}#quote-preview .linkfade { opacity: 1.0;}kbd { background-color: #f7f7f7; color: black; border: 1px solid #ccc; border-radius: 3px 3px 3px 3px; box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset; font-family: monospace; font-size: 11px; line-height: 1.4; padding: 0 5px;}.deleted { opacity: 0.66;}div.collapseWebm { text-align: center; margin-top: 10px; }.noPictures a.fileThumb img:not(.expanded-thumb) { opacity: 0;}.noPictures.futaba_new a.fileThumb,.noPictures.yotsuba_new a.fileThumb { border: 1px solid #800;}.noPictures.burichan_new a.fileThumb,.noPictures.yotsuba_b_new a.fileThumb { border: 1px solid #34345C;}.noPictures.tomorrow a.fileThumb:not(.expanded-thumb) { border: 1px solid #C5C8C6;}.noPictures.photon a.fileThumb:not(.expanded-thumb) { border: 1px solid #004A99;}.spinner { margin-top: 2px; padding: 3px; display: table;}#settings-presets { position: relative; top: -1px;}#colorpicker { position: fixed; text-align: center;}.colorbox { font-size: 10px; width: 16px; height: 16px; line-height: 17px; display: inline-block; text-align: center; background-color: #fff; border: 1px solid #aaa; text-decoration: none; color: #000; cursor: pointer; vertical-align: top;}#palette-custom-input { vertical-align: top; width: 45px; margin-right: 2px;}#qrDummyFile { float: left; margin-right: 5px; width: 220px; cursor: default; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;}#qrDummyFileLabel { margin-left: 3px;}.depageNumber { position: absolute; right: 5px;}.depagerEnabled .depagelink { font-weight: bold;}.depagerEnabled strong { font-weight: normal;}.depagelink { display: inline-block; padding: 4px 0; cursor: pointer; text-decoration: none;}.burichan_new .depagelink,.futaba_new .depagelink { text-decoration: underline;}#customMenuBox { margin: 0 auto 5px auto; width: 385px; display: block;}.preview-summary { display: block;}#swf-embed-header { padding: 0 0 0 3px; font-weight: normal; height: 20px; line-height: 20px;}.yotsuba_new #swf-embed-header,.yotsuba_b_new #swf-embed-header { height: 18px; line-height: 18px;}#swf-embed-close { position: absolute; right: 0; top: 1px;}#qr-painter-ctrl { text-align: center; }#qr-painter-ctrl label { margin-right: 4px; }.open-qr-wrap { text-align: center; width: 200px; position: absolute; margin-left: 50%; left: -100px;}.postMenuBtn { margin-left: 5px; text-decoration: none; line-height: 1em; display: inline-block; -webkit-transition: -webkit-transform 0.1s; -moz-transition: -moz-transform 0.1s; transition: transform 0.1s; width: 1em; height: 1em; text-align: center; outline: none; opacity: 0.8;}.postMenuBtn:hover{ opacity: 1;}.yotsuba_new .postMenuBtn,.futaba_new .postMenuBtn { color: #000080;}.tomorrow .postMenuBtn { color: #5F89AC;}.tomorrow .postMenuBtn:hover { color: #81a2be;}.photon .postMenuBtn { color: #FF6600;}.photon .postMenuBtn:hover { color: #FF3300;}.menuOpen { -webkit-transform: rotate(90deg); -moz-transform: rotate(90deg); -ms-transform: rotate(90deg); transform: rotate(90deg);}.settings-sub label:before { border-bottom: 1px solid; border-left: 1px solid; content: " "; display: inline-block; height: 8px; margin-bottom: 5px; width: 8px;}.settings-sub { margin-left: 25px;}.settings-tip.settings-sub { padding-left: 32px;}.centeredThreads .opContainer { display: block;}.centeredThreads .postContainer { margin: auto; width: 75%;}.centeredThreads .sideArrows { display: none;}.centre-exp { width: auto !important; clear: both;}.centeredThreads .summary { margin-left: 12.5%; display: block;}.centre-exp div.op{ display: table;}#yt-preview { position: absolute; }#yt-preview img { display: block; }.autohide-nav { transition: top 0.2s ease-in-out }#feedback { position: fixed; top: 10px; text-align: center; width: 100%; z-index: 9999;}.feedback-notify,.feedback-error { border-radius: 5px; cursor: pointer; color: #fff; padding: 3px 6px; font-size: 16px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); text-shadow: 0 1px rgba(0, 0, 0, 0.2);}.feedback-error { background-color: #C41E3A; }.feedback-notify { background-color: #00A550; }.boardSelect .custom-menu-ctrl, .boardSelect .customBoardList { margin-left: 10px; }.boardSelect .custom-menu-ctrl { display: none; }.boardSelect:hover .custom-menu-ctrl { display: inline; }.persistentNav .boardList a, .persistentNav .customBoardList a, #boardNavMobile .boardSelect a { text-decoration: none; }@media only screen and (max-width: 480px) {.thread-stats { float: none; text-align: center; }.ts-replies:before { content: "Replies: "; }.ts-images:before { content: "Images: "; }.ts-ips:before { content: "Posters: "; }.ts-page:before { content: "Page: "; }#threadWatcher { max-width: none; padding: 3px 0; left: 0; width: 100%; border-left: none; border-right: none;}#watchList { padding: 0 10px;}div.post div.postInfoM span.nameBlock { clear: none }.btn-row { margin-top: 5px;}.image-expanded .mFileName { display: block; margin-bottom: 2px;}.mFileName { display: none }.mobile-report { float: right; font-size: 11px; margin-bottom: 3px; margin-left: 10px;}.mobile-report:after { content: "]";}.mobile-report:before { content: "[";}.nws .mobile-report:after { color: #800000;}.nws .mobile-report:before { color: #800000;}.ws .mobile-report { color: #34345C;}.nws .mobile-report { color:#0000EE;}.reply .mobile-report { margin-right:5px;}.postLink .mobileHideButton { margin-right: 3px;}.board .mobile-hr-hidden { margin-top: 10px !important;}.board > .mobileHideButton { margin-top: -20px !important;}.board > .mobileHideButton:first-child { margin-top: 10px !important;}.extButton.threadHideButton { float: none; margin: 0; margin-bottom: 5px;}.mobile-post-hidden { display: none;}#toggleMsgBtn { display: none;}.mobile-tu-status { height: 20px; line-height: 20px;}.mobile-tu-show { width: 150px; margin: auto; display: block; text-align: center;}.button input { margin: 0 3px 0 0; position: relative; top: -2px; border-radius: 0; height: 10px; width: 10px;}.UIPanel > div { width: 320px; margin-left: -160px;}.UIPanel .export-field { width: 300px;}.yotsuba_new #quote-preview.highlight,#quote-preview { border-width: 1px !important;}.yotsuba_new #quote-preview.highlight { border-color: #D9BFB7 !important;}#quickReply input[type="text"],#quickReply textarea,.extPanel input[type="text"],.extPanel textarea { font-size: 16px;}#quickReply { position: absolute; left: 50%; margin-left: -154px;}.m-dark .button { background-color: rgb(27,28,30); background-image: url("//s.4cdn.org/image/buttonfade-dark.png"); background-repeat: repeat-x; border: 1px solid #282A2E;}.depaged-ad { margin-top: -25px; margin-bottom: -25px; }.depageNumber { font-size: 10px; margin-top: -21px; }.m-dark a, .m-dark div#absbot a { color: #81A2BE !important; }.m-dark a:hover { color: #5F89AC !important; }.m-dark .button a, .m-dark .button:hover, .m-dark .button { color: #707070 !important; }.m-dark #boardNavMobile { background-color: #1D1F21; border-bottom: 2px solid #282A2E; }body.m-dark { background: #1D1F21 none; color: #C5C8C6; }.m-dark #globalToggle { background-color: #FFADAD; background-image: url("//s.4cdn.org/image/buttonfade-red.png"); border: 1px solid #C45858; color: #880000 !important;}.m-dark .boardTitle { color: #C5C8C6; }.m-dark .mobile-report { color: #81a2be !important; }.m-dark .mobile-report:after,.m-dark .mobile-report:before { color: #1d1f21 !important; }.m-dark hr, .m-dark div.board > hr { border-top: 1px solid #282A2E; }.m-dark div.opContainer,.m-dark div.reply { background-color: #282A2E; }.m-dark .preview { background-color: #282A2E; border: 1px solid #333 !important; }.m-dark div.post div.postInfoM { background-color: #212326; border-bottom: 1px solid #2D2F33; }.m-dark div.postLink,.m-dark .backlink.mobile { background-color: #212326; border-top: 1px solid #2D2F33; }.m-dark div.post div.postInfoM span.dateTime,.m-dark div.postLink span.info,.m-dark div.post div.postInfoM span.dateTime a { color: #707070 !important; }.m-dark span.subject { color: #B294BB !important; }.m-dark .highlightPost:not(.op) { background: #3A171C !important; }.m-dark .reply:target, .m-dark .reply.highlight { background: #1D1D21 !important; padding: 2px; }.m-dark .reply:target, .m-dark .reply.highlight { background: #1D1D21 !important; padding: 2px; }}';(e=document.createElement("style")).setAttribute("type","text/css"),e.textContent=t,document.head.appendChild(e)},Main.init(); \ No newline at end of file diff --git a/js/extension.min.map b/js/extension.min.map new file mode 100644 index 0000000..ffdd943 --- /dev/null +++ b/js/extension.min.map @@ -0,0 +1 @@ +{"version":3,"file":"extension.min.1073.js","sources":["extension.1073.js"],"names":["$","id","document","getElementById","cls","klass","root","getElementsByClassName","byName","name","getElementsByName","tag","getElementsByTagName","qs","sel","querySelector","qsa","selector","querySelectorAll","extend","destination","source","key","on","n","e","h","addEventListener","off","removeEventListener","documentElement","classList","hasClass","el","contains","addClass","add","removeClass","remove","className","indexOf","replace","get","url","callbacks","headers","xhr","XMLHttpRequest","open","setRequestHeader","send","method","data","form","FormData","append","ago","timestamp","delta","count","head","tail","Date","now","hash","str","i","j","msg","length","charCodeAt","prettySeconds","fs","m","s","Math","floor","round","docEl","cache","Parser","tipTimeout","init","o","a","staticPath","Config","filter","linkify","embedSoundCloud","embedYouTube","Main","hasMobileLayout","this","needMsg","window","devicePixelRatio","icons","admin","founder","mod","dev","manager","del","prettify","prettyPrint","customSpoiler","localTime","getTimezoneOffset","abs","utcOffset","weekdays","tid","trackedReplies","getTrackedReplies","board","touchTrackedReplies","pruneTrackedReplies","postMenuIcon","tracked","localStorage","getItem","JSON","parse","saveTrackedReplies","replies","setItem","stringify","thres","ttl","pfx","flag","removeItem","parseThreadJSON","thread","posts","console","log","parseCatalogJSON","catalog","setCustomSpoiler","val","parseInt","firstChild","src","match","random","buildPost","pid","uid","no","revealSpoilers","custom_spoiler","buildHTMLFromJSON","lastElementChild","IDColor","applyRemote","firstElementChild","decodeSpecialChars","encodeSpecialChars","onDateMouseOver","clearTimeout","setTimeout","Tip","show","getAttribute","onTipMouseOut","onUIDMouseOver","p","parentNode","showUIDCount","textContent","t","nodes","standalone","fromQuote","userId","fileThumb","filePath","fileName","mName","subject","noLink","quoteLink","noFilename","decodedFilename","postCountStr","resto","q","href","quotes","tmp","imgDir","container","createElement","isOP","fileDims","imgSrc","fileInfo","fileHtml","fileSpoilerTip","size","fileClass","shortFile","longFile","tripcode","capcodeStart","capcodeClass","capcode","highlight","emailStart","emailEnd","replySpan","mobileLink","postType","summary","capcode_replies","threadIcons","needFileTip","images","semantic_url","email","country_name","troll_country","toLowerCase","country","filedeleted","ext","filename","slice","tn_w","tn_h","w","fsize","spoiler","tim","m_img","md5","toUpperCase","trip","truncate","buildCapcodeReplies","sticky","icons2","closed","archived","undefined","sub","innerHTML","time","since4pass","com","charAt","len","html","map","post_ids","prelink","pretext","developer","parseBoard","threads","parseThread","offset","limit","pi","frag","omitted","filtered","cnt","Filter","exec","threadHiding","minus","insertBefore","ThreadHiding","hidden","hide","ThreadExpansion","enabled","createDocumentFragment","cloneNode","title","alt","setAttribute","plus","appendChild","style","display","threadWatcher","ThreadWatcher","watched","notwatched","createTextNode","isMobileDevice","quotePreview","parseMobileQuotelinks","parseTrackedReplies","parsePost","parseMarkup","math_tags","MathJax","postHasMath","cleanWbr","Hub","Queue","loadMathJax","UA","dispatchEvent","threadId","test","parseMathOne","node","post","link","quotelinks","hasYouMarkers","nextSibling","pre","prettyPrintOne","revealImageSpoiler","img","finfo","txt","removeChild","removeAttribute","maxWidth","maxHeight","pathname","previousElementSibling","split","file","ReplyHiding","backlinks","parseBacklinks","apply","Linkify","Media","parseSoundCloud","parseYouTube","children","nodeValue","getLocaleDate","date","getMonth","getDate","getFullYear","getDay","getHours","getMinutes","getSeconds","linklist","ids","target","bl","buildSummary","oRep","oImg","PostMenu","activeBtn","btn","div","btnPos","left","close","getBoundingClientRect","top","bottom","pageYOffset","postId","body","pageXOffset","clientWidth","offsetWidth","Depager","el2","isLoading","isEnabled","isComplete","threadsLoaded","threadQueue","debounce","threshold","adId","adZones","boardHasAds","adPlea","alwaysDepage","bindHandlers","onScroll","scrollHeight","ceil","innerHeight","renderNext","depage","trackPageview","pageId","_gat","_getTrackerByName","_trackPageview","__qc","qpixelsent","_qevents","push","qacct","qopts","firepixels","insertAd","zone","isLastPage","wrap","ados_add_placement","setZone","loadAds","ados_load","k","op","reply","parseList","scroll","last_replies","boardDiv","shift","page","adZone","omitted_posts","omitted_images","unbindHandlers","setStatus","scrollTo","type","links","caption","toggle","disable","enable","onload","onLoad","onerror","onError","queue","status","responseText","QuoteInline","isSelfQuote","nodeName","media","preventDefault","pause","inline","inlineRemote","cached","dummy","hasAttribute","nextElementSibling","statusText","blcnt","isBl","inner","tblcnt","dest","autoplay","QuotePreview","regex","highlightAnti","cur","resolve","self","clientHeight","location","hasCORS","showRemote","cursor","remote","rect","postHeight","doc","docWidth","pos","qid","scrollTop","imageExpansion","ImageExpansion","contract","controls","offsetHeight","right","width","height","activeVideos","timeout","expand","thumb","imageHover","ImageHover","expandWebm","setMobileSrc","opacity","checkLoadStart","onLoadStart","centeredThreads","marginLeft","offsetTop","scrollIntoView","fileText","navigator","userAgent","muted","unmuteWebm","loop","onloadedmetadata","fitWebm","onplay","onWebmPlay","volume","collapseWebm","imgWidth","imgHeight","ratio","cntEl","centerWidth","ofs","videoWidth","videoHeight","fitToScreenExpansion","pauseVideos","min","max","paused","Feedback","error","fileEl","naturalWidth","naturalHeight","showWebm","onLoadError","imageHoverBg","backgroundColor","play","bounds","showWebMDuration","innerWidth","sound","ms","duration","mozHasAudio","webkitAudioDecodedByteCount","audioTracks","QR","item","hasFormData","currentTid","cooldown","auto","comField","comLength","comlen","comCheckTimeout","cdElapsed","activeDelay","cooldowns","noCaptcha","painterData","captchaWidgetCnt","captchaWidgetId","hasCaptchaAltJs","pulse","fileDisabled","imagelimit","threadClosed","addReplyLink","syncStorage","openTeXPreview","closeTeXPreview","cross","onTeXChanged","timeoutTeX","processTeX","processingTeX","value","onTeXReady","lock","showPostError","unlock","hidePostError","newValue","startCooldown","openPainter","dims","Tegaki","onDone","onPainterDone","onCancel","onPainterCancel","flatten","toDataURL","disabled","quotePost","noCooldown","isThreadClosed","alert","addQuote","ta","forms","qrPost","selectionStart","getSelection","trim","selectionEnd","isOpera","focus","postForm","qrForm","fields","row","placeholder","qrError","cookie","mfs","cssText","altCaptcha","tabIndex","onFileChange","lastChild","previousSibling","getCookie","onKeyDown","onClick","passEnabled","renderCaptchaAlt","renderCaptcha","Draggable","set","grecaptcha","render","sitekey","recaptchaKey","theme","stylesheet","Recaptcha","create","tabindex","initCaptchaAlt","loadOnly","resetCaptchaAlt","RecaptchaState","reload","resetCaptcha","reset","onPassError","maxFilesize","files","maxWebmFilesize","ctrlKey","keyCode","start","end","stopPropagation","setSelectionRange","altKey","shiftKey","metaKey","checkCommentField","byteLength","encodeURIComponent","sjis_tags","clearInterval","abort","unset","destroy","submit","resetFile","click","silent","mozHidden","webkitHidden","msHidden","replaceChild","needPreuploadCaptcha","force","formdata","presubmitChecks","action","withCredentials","upload","onprogress","loaded","total","resp","hasFile","setPostTime","persistentQR","checked","setLastRead","lastReplyId","ThreadUpdater","forceUpdate","appendPainter","blob","b64toBlob","bytes","ary","bary","atob","Array","Uint8Array","Blob","getCooldown","getPostTime","removePostTime","setInterval","onPulse","load","purge","clear","confirm","isHidden","save","sa","th","hideStubs","storage","hasHidden","lastPurged","pages","alive","hiddenR","hiddenRMap","hasR","toggleR","rid","parentPid","showR","hideR","shouldToggleR","ql","hit","hasHiddenR","clr","jumpTo","listNode","charLimit","blacklisted","isRefreshing","toggleList","scrollBy","history","replaceState","position","fixedThreadWatcher","refresh","refreshCurrent","build","initMobileButtons","canAutoRefresh","oldValue","rebuildButtons","tuid","linkToThread","join","buttons","ref","refreshWithAutoWatch","generateLabel","label","lastReply","addRaw","sortByBoard","sorted","keys","sort","b","setRefreshTimestamp","f","boards","activeFilters","rotate","fetchCatalogs","to","catalogs","meta","fetchCatalog","onCatalogsLoaded","fetch","rebuild","onRefreshEnd","li","newReplies","fetchXhr","expandComment","abbr","expmsg","metacap","thread_archived","pageTitle","unreadCount","hadAuto","delayId","delayIdHidden","delayRange","timeLeft","interval","tailSize","lastUpdated","lastModified","lastModifiedTail","currentIcon","iconPath","iconNode","defaultIcon","deletionQueue","updaterSound","audioEnabled","audio","visibilitychange","adRefreshDelay","adDebounce","ads","initAds","initControls","alwaysAutoUpdate","sessionStorage","buildMobileControl","ctrl","cb","oldBtn","marginTop","buildDesktopControl","navlinks","dead","autoNode","autoNodeBot","updating","onVisibilityChange","stop","manual","setIcon","update","adjustDelay","postCount","refreshAds","clearUnread","toggleAuto","toggleSound","soundNode","soundNodeBot","full","isTail","checkTailUpdate","istail","If-Modified-Since","dtr","dtp","checkTailValid","tail_id","adIds","seenOnce","isStale","invalidateAds","ad","docHeight","D","ados_refresh","state","newposts","lastrep","lastid","autoscroll","fromQR","lastRepPos","getResponseHeader","tail_size","childElementCount","setThreadState","threadSticky","autoScroll","hasHighlightedPosts","threadStats","ThreadStats","unique_ips","bumplimit","setError","statusNode","statusNodeBot","icon","wsnew","nwsnew","wsrep","nwsrep","wsdead","nwsdead","wshl","nwshl","nodeTop","nodeBot","pageNumber","updatePageNumber","pageInterval","ips","isBumpFull","isImageFull","stats","onCatalogLoad","onCatalogError","entities","cmd","openPalette","closePalette","clearPalette","moveUp","openHelp","closeHelp","onPaletteClick","pickColor","filters","pattern","fname","currentBoard","boxShadow","color","unfilter","rawFilters","rawPattern","fid","regexEscape","regexType","wordSepS","wordSepE","words","regexWildcard","replaceWildcard","RegExp","active","addSelection","text","anchorNode","toString","filterList","help","buildEntry","prev","getNextFilterId","tr","entries","options","selectedIndex","buildPalette","colors","rowCount","colCount","picker","setCustomColor","background","input","box","css","user_ids","compute","rgb","SWFEmbed","processThread","processIndex","toggleThread","srcIndex","embedIndex","header","backdrop","cntWidth","cntHeight","margins","headerHeight","onBackdropClick","probeRe","linkRe","punct","derefer","funk","mm","sfx","matchSC","matchYT","toggleYT","timeYT","yt","toggleYouTube","sc","toggleSoundCloud","replaceSoundCloud","replaceYouTube","showYTPreview","vid","aabb","x","y","tw","pad","removeYTPreview","toggleEmbed","fn","call","StickyNav","classicNav","checkScroll","thisPos","CustomCSS","customCSS","Keybinds",65,70,81,82,87,66,67,78,73,"bind","Del","deletePost","file_only","params","mode","pwd","onPostDeleted","Report","onMessage","origin","altc","CustomMenu","initCtrl","dropDownNav","custom","navs","cntBottom","boardList","closeEditor","showEditor","customMenuList","customMenu","scrollX","scrollY","dx","dy","handle","startDrag","offs","clientX","clientY","getComputedStyle","getDocTopOffset","endDrag","onDrag","Object","prototype","opera","detail","createEvent","initEvent","raw","quickReply","threadUpdater","topPageNav","threadExpansion","stickyNav","keyBinds","inlineQuotes","noPictures","compactThreads","autoHideNav","forceHTTPS","darkTheme","disableAll","ConfigMobile","firstRun","loadFromURL","decodeURIComponent","settings","catalogFilters","catalogSettings","toURL","cfg","old","setCookie","removeCookie","SettingsMenu","Quotes & Replying","Monitoring","Filters & Post Hiding","Navigation","Images & Media","Miscellaneous","cat","categories","opts","mobileOpts","expandAll","showExport","onExportClick","select","closeExport","toggleCat","disp","messageTimeout","showMessage","hideMessage","notify","addTooltip","message","offsetLeft","run","protocol","style_group","initIcons","addCSS","initPersistentNav","checkMobileLayout","mobile","desktop","matchMedia","matches","disableDarkTheme","onclick","onThreadMouseOver","onThreadMouseOut","setStickyNav","initGlobalMessage","setPageNav","cap","up","down","download","gis","iqdb","quote","report","trash","paths","yotsuba_new","futaba_new","yotsuba_b_new","burichan_new","tomorrow","photon","thisTs","oldTs","toggleGlobalMessage","hdr","c","ca","substring","domain","setTime","getTime","toGMTString","which","host"],"mappings":"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"} \ No newline at end of file diff --git a/js/janitor-unminified.js b/js/janitor-unminified.js new file mode 100644 index 0000000..c0e4f77 --- /dev/null +++ b/js/janitor-unminified.js @@ -0,0 +1,1244 @@ +/** + * Janitor Extension + */ + +(function() { +/** + * Admin tools + */ +var AdminTools = { + cacheTTL: 60000, + autoRefreshDelay: 120000, + autoRefreshTimeout: null +}; + +// FIXME, put it as a helper in extension.js +AdminTools.initVisibilityAPI = function() { + this.hidden = 'hidden'; + this.visibilitychange = 'visibilitychange'; + + if (typeof document.hidden === 'undefined') { + if ('mozHidden' in document) { + this.hidden = 'mozHidden'; + this.visibilitychange = 'mozvisibilitychange'; + } + else if ('webkitHidden' in document) { + this.hidden = 'webkitHidden'; + this.visibilitychange = 'webkitvisibilitychange'; + } + else if ('msHidden' in document) { + this.hidden = 'msHidden'; + this.visibilitychange = 'msvisibilitychange'; + } + } + + document.addEventListener(this.visibilitychange, this.onVisibilityChange, false); +}; + +AdminTools.init = function() { + var cnt, html; + + AdminTools.initVisibilityAPI(); + + cnt = document.createElement('div'); + cnt.className = 'extPanel reply'; + cnt.id = 'adminToolbox'; + cnt.setAttribute('data-trackpos', 'AT-position'); + + if( Config['AT-position'] ) { + cnt.style.cssText = Config['AT-position']; + } else { + cnt.style.right = '10px'; + cnt.style.top = '380px'; + } + + cnt.style.position = Config.fixedAdminToolbox ? 'fixed' : ''; + + html = '
    Janitor Tools' + + 'Refresh
    ' + + '

    Reports: ' + + '? (' + + '?)

    ' + + '

    Messages: ?

    '; + + cnt.innerHTML = html; + document.body.appendChild(cnt); + AdminTools.refreshReportCount(); + + Draggable.set($.id('atHeader')); +}; + +AdminTools.onVisibilityChange = function() { + var self; + + self = AdminTools; + + if (document[AdminTools.hidden]) { + clearInterval(self.autoRefreshTimeout); + self.autoRefreshTimeout = null; + } + else { + self.refreshReportCount(); + self.autoRefreshTimeout = setInterval(self.refreshReportCount, self.autoRefreshDelay); + } +}; + +AdminTools.refreshReportCount = function(force) { + var xhr, cache; + + if (force !== true && (cache = localStorage.getItem('4chan-cache-rc'))) { + cache = JSON.parse(cache); + + if (cache.ts > Date.now() - AdminTools.cacheTTL) { + $.id('at-total').textContent = cache.data[0]; + $.id('at-illegal').textContent = cache.data[1]; + + $.id('at-msg-cnt').style.display = cache.data[2] ? 'block' : ''; + $.id('at-msg').textContent = cache.data[2] || 0; + + return; + } + } + + xhr = new XMLHttpRequest(); + + xhr.open('GET', 'https://' + J.reportsSubDomain + '.4chan.org/H429f6uIsUqU.php', true); + + xhr.withCredentials = true; + + xhr.onload = function() { + var cache, resp, data, msg_count; + + if (this.status == 200) { + try { + resp = JSON.parse(this.responseText); + } + catch (e) { + console.log(e); + return; + } + + if (resp.status !== 'success') { + console.log(resp.message); // FIXME, use global message + return; + } + + data = resp.data; + + msg_count = data.msg || 0; + + $.id('at-msg-cnt').style.display = msg_count ? 'block' : ''; + $.id('at-msg').textContent = msg_count; + $.id('at-total').textContent = data.total; + $.id('at-illegal').textContent = data.illegal; + + cache = { + ts: Date.now(), + data: [ data.total, data.illegal, msg_count ] + }; + + cache = JSON.stringify(cache); + + localStorage.setItem('4chan-cache-rc', cache); + + document.dispatchEvent(new CustomEvent('4chanATUpdated')); + } + else { + this.onerror(); + } + }; + + xhr.onerror = function() { + console.log('Error while refreshing the report count (Status: ' + this.status + ').'); + }; + + xhr.onloadend = function() { + $.id('atRefresh').src = Main.icons.refresh; + }; + + $.id('atRefresh').src = Main.icons.rotate; + + xhr.send(null); +}; + +AdminTools.resetMsgCount = function() { + var cache; + + $.id('at-msg').textContent = 0; + + if (cache = localStorage.getItem('4chan-cache-rc')) { + cache = JSON.parse(cache); + cache.data[2] = 0; + cache = JSON.stringify(cache); + localStorage.setItem('4chan-cache-rc', cache); + } +}; + +var J = { + nextChunkIndex: 0, + nextChunk: null, + chunkSize: 100, + + reportsSubDomain: 'reports' +}; + +J.initIconsCatalog = function() { + var key, paths, url; + + Main.icons = { + up: 'arrow_up.png', + down: 'arrow_down.png', + right: 'arrow_right.png', + download: 'arrow_down2.png', + refresh: 'refresh.png', + cross: 'cross.png', + gis: 'gis.png', + iqdb: 'iqdb.png', + minus: 'post_expand_minus.png', + plus: 'post_expand_plus.png', + rotate: 'post_expand_rotate.gif', + quote: 'quote.png', + report: 'report.png', + notwatched: 'watch_thread_off.png', + watched: 'watch_thread_on.png', + help: 'question.png' + }; + + paths = { + yotsuba_new: 'futaba/', + futaba_new: 'futaba/', + yotsuba_b_new: 'burichan/', + burichan_new: 'burichan/', + tomorrow: 'tomorrow/', + photon: 'photon/' + }; + + url = '//s.4cdn.org/image/'; + + if (window.devicePixelRatio >= 2) { + for (key in Main.icons) { + Main.icons[key] = Main.icons[key].replace('.', '@2x.'); + } + } + + url += 'buttons/' + paths[Main.stylesheet]; + for (key in Main.icons) { + Main.icons[key] = url + Main.icons[key]; + } +}; + +J.apiUrlFilter = function(url) { + return url + '?' + Math.round(Date.now() / 1000 / 3); +}; + +J.openDeletePrompt = function(id) { + var html, cnt; + + id = id.getAttribute('data-id'); + html = '
    Delete Post No.' + id + + 'Close' + + '
    ' + + ' ' + + ''; + + cnt = document.createElement('div'); + cnt.className = 'UIPanel'; + cnt.id = 'delete-prompt'; + + cnt.innerHTML = html; + cnt.addEventListener('click', J.closeDeletePrompt, false); + document.body.appendChild(cnt); + + $.id('delete-prompt-inner').firstElementChild.focus(); +}; + +J.closeDeletePrompt = function(e) { + var prompt; + + if (!e || e.target.id == 'delete-prompt') { + if (prompt = $.id('delete-prompt')) { + prompt.removeEventListener('click', J.closeDeletePrompt, false); + document.body.removeChild(prompt); + } + } +}; + +J.deletePost = function(btn, imageOnly) { + var id, xhr, form, msg, el, url, mode, del, isOp; + + id = btn.getAttribute('data-id'); + + isOp = $.id('t' + id); + + form = new FormData(); + msg = 'Delete Post No.'; + url = 'https://sys.' + $L.d(Main.board) + '/' + Main.board + '/post'; + mode = window.thread_archived ? 'arcdel' : 'usrdel'; + + if(imageOnly) { + msg = 'Delete Image No.'; + form.append('onlyimgdel', 'on'); + } + + form.append(id, 'delete'); + form.append('mode', mode); + form.append('pwd', 'janitorise'); + + (del = $.id('delete-prompt-inner')).textContent = 'Deleting...'; + + xhr = new XMLHttpRequest(); + xhr.open('POST', url); + xhr.withCredentials = true; + xhr.onload = function() { + btn.src = Main.icons.cross; + if (this.status == 200) { + if (this.responseText.indexOf('Updating') != -1) { + if (!imageOnly) { + if (id == Main.tid) { + location.href = '//boards.' + $L.d(Main.board) + '/' + Main.board + '/'; + return; + } + else { + if (isOp) { + el = isOp.parentNode; + el.removeChild(isOp.nextElementSibling); + el.removeChild(isOp); + } + else { + el = $.id('pc' + id); + el.parentNode.removeChild(el); + } + } + } + else { + el = $.id('f' + id); + el.innerHTML = 'File deleted.'; + } + + J.closeDeletePrompt(); + } + else { + del.textContent = 'Error: Post might have already been deleted, or is a sticky.'; + } + } + else { + del.textContent = 'Error: Wrong status while deleting No.' + id + ' (Status: ' + this.status + ').'; + } + }; + xhr.onerror = function() { + del.textContent = 'Error: Error while deleting No.' + id + ' (Status: ' + this.status + ').'; + }; + + xhr.send(form); +}; + +J.openBanReqWindow = function(btn) +{ + var id; + + id = btn.getAttribute('data-id'); + window.open('https://sys.' + $L.d(Main.board) + '/' + Main.board + '/admin?mode=admin&admin=banreq&id=' + id, '_blank', 'scrollBars=yes,resizable=no,toolbar=no,menubar=no,location=no,directories=no,width=400,height=245'); +}; + +J.openBanReqFrame = function(btn) { + var id; + + if (this.banReqCnt) { + this.close(); + } + + id = btn.getAttribute('data-id'); + + this.banReqCnt = document.createElement('div'); + this.banReqCnt.id = 'banReq'; + this.banReqCnt.className = 'extPanel reply'; + this.banReqCnt.setAttribute('data-trackpos', 'banReq-position'); + + if (Config['banReq-position']) { + this.banReqCnt.style.cssText = Config['banReq-position']; + } + else { + this.banReqCnt.style.right = '0px'; + this.banReqCnt.style.top = '50px'; + } + + this.banReqCnt.innerHTML = + '
    Ban Request No.' + id + + 'X
    ' + + ''; + + document.body.appendChild(this.banReqCnt); + + window.addEventListener('message', J.onMessage, false); + document.addEventListener('keydown', J.onKeyDown, false); + + $.id('banReqClose').addEventListener('click', J.closeBanReqFrame, false); + Draggable.set($.id('banReqHeader')); +}; + +J.closeBanReqFrame = function() { + window.removeEventListener('message', J.onMessage, false); + document.removeEventListener('keydown', J.onKeyDown, false); + Draggable.unset($.id('banReqHeader')); + $.id('banReqClose').removeEventListener('click', J.closeBanReqFrame, false); + document.body.removeChild(J.banReqCnt); + J.banReqCnt = null; +}; + +J.processMessage = function(data) { + if (!data) { + return {}; + } + + data = data.split('-'); + + return { + cmd: data[0], + type: data[1], + id: data.slice(2).join('-') + }; +}; + +J.onKeyDown = function(e) { + if (e.keyCode == 27 && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { + J.closeBanReqFrame(); + } +}; + +J.onMessage = function(e) { + var msg; + + if (e.origin !== 'https://sys.' + $L.d(Main.board)) { + return; + } + + msg = J.processMessage(e.data); + + if (msg.type !== 'ban') { + return; + } + + if (msg.cmd === 'done' || msg.cmd === 'cancel') { + J.closeBanReqFrame(); + } +}; + +/** + * Click handler + */ +J.onClick = function(e) { + var t, cmd; + + if ((t = e.target) == document) { + return; + } + + if (cmd = t.getAttribute('data-cmd')) { + switch (cmd) { + case 'at-refresh': + AdminTools.refreshReportCount(true); + break; + case 'delete-post': + case 'delete-image': + J.deletePost(t, (cmd === 'delete-image')); + break; + case 'open-delete-prompt': + J.openDeletePrompt(t); + break; + case 'close-delete-prompt': + J.closeDeletePrompt(); + break; + case 'open-banreq-prompt': + if (Config.inlinePopups) { + J.openBanReqFrame(t); + } + else { + J.openBanReqWindow(t); + } + break; + case 'at-msg': + AdminTools.resetMsgCount(); + break; + case 'toggle-file-spoiler': + J.setFileSpoiler(t); + break; + + case 'prompt-spoiler': + if (confirm('Toggle spoiler?')) { + J.setFileSpoiler(t); + } + break; + + case 'boardlist-open': + J.openBoardList(); + break; + case 'boardlist-close': + J.closeBoardList(); + break; + case 'boardlist-save': + J.saveBoardList(); + J.closeBoardList(); + break; + } + } +}; + +J.onScroll = function() { + var end; + + while (J.nextChunk.offsetTop < (document.documentElement.clientHeight + window.scrollY)) { + end = J.nextChunkIndex + J.chunkSize; + if (end >= J.postCount) { + J.parseRange(J.nextChunkIndex, J.postCount); + window.removeEventListener('scroll', J.onScroll, false); + return false; + } + else { + J.parseRange(J.nextChunkIndex, end); + } + } + + return true; +}; + +J.parseRange = function(start, end) { + var i, j, posts; + + posts = document.getElementById('t' + Main.tid).getElementsByClassName('postInfo'); + + for (i = start; i < end; ++i) { + j = posts[i]; + + if (!j) { + break; + } + + J.parsePost(j); + } + + J.nextChunkIndex = i; + J.nextChunk = posts[i]; +}; + +J.onParsingDone = function(e) { + var i, tid, offset, limit, posts; + + if (Config.useIconButtons) { + if (e) { + tid = e.detail.threadId; + offset = e.detail.offset; + limit = e.detail.limit; + posts = document.getElementById('t' + tid).getElementsByClassName('postInfo'); + } + else { + offset = 0; + posts = document.getElementsByClassName('postInfo'); + limit = posts.length; + } + + for (i = offset; i < limit; ++i) { + J.parsePost(posts[i]); + } + } +}; + +J.onPostMenuReady = function(e) { + var el, pid, menu, flag; + + pid = e.detail.postId; + menu = e.detail.node; + + el = document.createElement('li'); + el.className = 'dd-admin'; + el.setAttribute('data-cmd', 'open-delete-prompt'); + el.setAttribute('data-id', pid); + el.textContent = 'Delete'; + menu.appendChild(el); + + if (window.spoilers && (el = $.id('fT' + pid))) { + flag = $.cls('imgspoiler', el.parentNode)[0] ? 0 : 1; + el = document.createElement('li'); + el.className = 'dd-admin'; + el.setAttribute('data-cmd', 'toggle-file-spoiler'); + el.setAttribute('data-id', pid); + el.setAttribute('data-flag', flag); + el.textContent = (flag ? 'Set' : 'Unset') + ' Spoiler'; + menu.appendChild(el); + } + + if (window.thread_archived) { + return; + } + + el = document.createElement('li'); + el.className = 'dd-admin'; + el.setAttribute('data-cmd', 'open-banreq-prompt'); + el.setAttribute('data-id', pid); + el.textContent = 'Ban request'; + menu.appendChild(el); +}; + +J.parsePost = function(postInfo) { + var pid, html, cnt, tail; + + pid = postInfo.id.slice(2); + + html = 'X'; + + if (window.spoilers && (el = $.id('fT' + pid))) { + html += 'S'; + } + + if (!window.thread_archived) { + html += 'B'; + } + + cnt = document.createElement('div'); + cnt.className = 'extControls'; + cnt.innerHTML = html; + + tail = postInfo.getElementsByClassName('postMenuBtn')[0]; + + postInfo.insertBefore(cnt, tail); +}; + +J.displayJCount = function(jLink, jLinkBot, no, delta) { + var msg; + + $.addClass(jLink, 'j-newposts'); + $.addClass(jLinkBot, 'j-newposts'); + jLink.setAttribute('data-no', no); + jLinkBot.setAttribute('data-no', no); + jLink.textContent = jLinkBot.textContent = 'j +' + delta; + + msg = delta + ' new post' + (delta > 1 ? 's' : ''); + + Main.addTooltip(jLink, msg, 'j-tooltip'); + Main.addTooltip(jLinkBot, msg, 'j-tooltip-bot'); +}; + +J.refreshJCount = function() { + var stored, jLink, jLinkBot, xhr; + + jLink = $.id('j-link'); + jLinkBot = $.id('j-link-bot'); + + if (!jLink || !jLinkBot) { + return; + } + + jLink = jLink.firstElementChild; + jLinkBot = jLinkBot.firstElementChild; + + if (stored = localStorage.getItem('4chan-j-count')) { + stored = JSON.parse(stored); + } + + if (!stored || (Date.now() - stored.time) >= 10000) { + xhr = new XMLHttpRequest(); + xhr.open('GET', 'https://sys.4chan.org/j/1mcQTXbjW5WO.php?&' + Date.now()); + xhr.withCredentials = true; + xhr.onloadend = function() { + var data, obj, delta; + if (this.status == 200 || this.status == 304) { + data = JSON.parse(this.responseText); + if (!stored || Main.board == 'j') { + obj = { time: Date.now(), no: data.no }; + } + else if (data.no > stored.no) { + delta = data.no - stored.no; + J.displayJCount(jLink, jLinkBot, data.no, delta); + obj = { time: Date.now(), no: stored.no, delta: delta }; + } + if (obj) { + localStorage.setItem('4chan-j-count', JSON.stringify(obj)); + } + } + else { + console.log('Error: Could not load /j/ post count (Status: ' + this.status + ').'); + } + }; + xhr.send(null); + } + else if (stored.delta) { + J.displayJCount(jLink, jLinkBot, stored.no, stored.delta); + } +}; + +J.clearJCount = function() { + var obj, no, tt, ttbot; + + tt = $.id('j-tooltip'); + ttbot = $.id('j-tooltip-bot'); + + if (!tt) { + return; + } + + no = this.getAttribute('data-no'); + obj = { time: Date.now(), no: no }; + localStorage.setItem('4chan-j-count', JSON.stringify(obj)); + + tt.parentNode.removeChild(tt); + ttbot.parentNode.removeChild(ttbot); + + setTimeout(function() { + var nodes = $.cls('j-newposts'); + if (nodes[0]) { + nodes[0].textContent = 'j'; + $.removeClass(nodes[0], 'j-newposts'); + nodes[0].textContent = 'j'; + $.removeClass(nodes[0], 'j-newposts'); + } + }, 10); +}; + +J.icons = { + ban: 'ban.png', + spoiler: 's.png' +}; + +J.initIcons = function() { + var key, paths, url; + + paths = { + yotsuba_new: 'futaba/', + futaba_new: 'futaba/', + yotsuba_b_new: 'burichan/', + burichan_new: 'burichan/', + tomorrow: 'tomorrow/', + photon: 'photon/' + }; + + url = '//s.4cdn.org/image/buttons/' + paths[Main.stylesheet]; + + if (window.devicePixelRatio >= 2) { + for (key in J.icons) { + J.icons[key] = J.icons[key].replace('.', '@2x.'); + } + } + + for (key in J.icons) { + J.icons[key] = url + J.icons[key]; + } +}; + +J.initNavLinks = function() { + var el, nav, navbot; + + nav = $.id('navtopright'); + navbot = $.id('navbotright'); + + // [j] link + el = document.createElement('span'); + el.id = 'j-link'; + el.innerHTML = '[j]'; + el.firstElementChild.addEventListener('mouseup', J.clearJCount, false); + nav.parentNode.insertBefore(el, nav); + + // [j] bottom link + el = el.cloneNode(true); + el.id = 'j-link-bot'; + el.firstElementChild.addEventListener('mouseup', J.clearJCount, false); + navbot.parentNode.insertBefore(el, navbot); + + J.refreshJCount(); +}; + +J.openBoardList = function() { + var cnt; + + if ($.id('boardList')) { + return; + } + + cnt = document.createElement('div'); + cnt.id = 'boardList'; + cnt.className = 'UIPanel'; + cnt.setAttribute('data-cmd', 'boardlist-close'); + cnt.innerHTML = '\ +
    Boards\ +Close
    \ +\ +
    \ +
    '; + + document.body.appendChild(cnt); + cnt.addEventListener('click', this.onClick, false); +}; + +J.saveBoardList = function() { + var input; + + if (input = $.id('boardListBox')) { + localStorage.setItem('4chan-boardlist', input.value); + } +}; + +J.closeBoardList = function() { + var cnt; + + if (cnt = $.id('boardList')) { + cnt.removeEventListener('click', this.onClick, false); + document.body.removeChild(cnt); + } +}; + +J.setFileSpoiler = function(t) { + var xhr, pid, flag, el; + + pid = t.getAttribute('data-id'); + flag = t.getAttribute('data-flag'); + + if (!pid) { + return; + } + + el = $.id('f' + pid); + + if (!flag) { + flag = $.cls('imgspoiler', el.parentNode)[0] ? 0 : 1; + } + + if (!el || el.hasAttribute('data-processing')) { + return; + } + + xhr = new XMLHttpRequest(); + xhr.open('GET', 'https://sys.' + $L.d(Main.board) + '/' + Main.board + + '/admin.php?admin=spoiler&pid=' + pid + '&flag=' + flag, true); + xhr.withCredentials = true; + xhr.onload = J.onFileSpoilerLoad; + xhr.onerror = J.onFileSpoilerError; + xhr._pid = +pid; + xhr._flag = +flag; + + Feedback.notify('Processing...', null); + + el.setAttribute('data-processing', '1'); + + xhr.send(null); +}; + +J.onFileSpoilerLoad = function() { + var el, el2; + + Feedback.hideMessage(); + + if (this.responseText !== '1') { + if (this.responseText === '-1') { + Feedback.error('You are not logged in'); + } + else { + Feedback.error("Couldn't set spoiler flag for post No." + this._pid); + } + + return; + } + + if (!(el = $.id('f' + this._pid))) { + return; + } + + el.removeAttribute('data-processing'); + + if (!(el = $.cls('fileThumb', el)[0])) { + return; + } + + if (this._flag) { + $.addClass(el, 'imgspoiler'); + + el2 = el.previousElementSibling; + el2.setAttribute('title', el2.firstElementChild.textContent); + + if (!Config.revealSpoilers) { + el = $.tag('img', el)[0]; + el.style.width = el.style.height = '100px'; + el.src = '//s.4cdn.org/image/spoiler-' + Main.board + '.png'; + } + } + else { + if (!Config.revealSpoilers) { + Parser.revealImageSpoiler(el); + } + $.removeClass(el, 'imgspoiler'); + } +}; + +J.onFileSpoilerError = function() { + var el; + + if (!(el = $.id('f' + this._pid))) { + return; + } + el.removeAttribute('data-processing'); + Feedback.error("Couldn't update the spoiler flag for post No." + this.pid); +}; + +J.initCatalog = function() { + var storage; + + window.Main = { + board: location.pathname.split(/\//)[1] + }; + + window.Main.addTooltip = function(link, message, id) { + var el, pos; + + el = document.createElement('div'); + el.className = 'click-me'; + if (id) { + el.id = id; + } + el.innerHTML = message || 'Change your settings'; + link.parentNode.appendChild(el); + + pos = (link.offsetWidth - el.offsetWidth + link.offsetLeft - el.offsetLeft) / 2; + el.style.marginLeft = pos + 'px'; + + return el; + }; + + if (J.stylesheet = J.getCookie(window.style_group)) { + J.stylesheet = J.stylesheet.toLowerCase().replace(/ /g, '_'); + } + else { + J.stylesheet = + style_group == 'nws_style' ? 'yotsuba_new' : 'yotsuba_b_new'; + } + + Main.stylesheet = J.stylesheet; + + J.initIconsCatalog(); + + J.addCss(); // fixme + + document.addEventListener('click', J.onClick, false); + + J.runCatalog(); +}; + +J.runCatalog = function () { + var threads; + //J.addCss(); // fixme + //document.removeEventListener('4chanMainInit', J.runCatalog, false); + + J.initNavLinks(); + + if (!FC.hasMobileLayout) { + AdminTools.init(); + } + + threads = $.id('threads'); + + $.on(threads, 'mouseover', J.onThreadMouseOver); + //$.on(threads, 'mouseout', J.onThreadMouseOut); +}; + +J.init = function() { + var ts; + + Config.boardList = true; + + SettingsMenu.options['Janitor'] = { + boardList: [ 'Janitor Boards [Select]', 'Select boards to enable janitor buttons on', true ], + useIconButtons: [ 'Use icon buttons', 'Display old-style buttons instead of using drop-down' ], + changeUpdateDelay: [ 'Reduce auto-update delay', 'Reduce the thread updater delay', true ], + fixedAdminToolbox: [ 'Pin Janitor Tools to the page', 'Janitor Tools will scroll with you' ], + inlinePopups: [ 'Inline ban request panel', 'Open ban request panel in browser window, instead of a popup' ], + disableMngExt: [ 'Disable janitor extension', 'Completely disable the janitor extension (overrides any checked boxes)', true ] + }; + + if (Config.disableMngExt) { + return; + } + + J.addCss(); + + if (Config.useIconButtons) { + J.initIcons(); + } + + QR.noCooldown = QR.noCaptcha = true; + + document.addEventListener('click', J.onClick, false); + document.addEventListener('DOMContentLoaded', J.run, false); +}; + +J.run = function() { + var boards, posts, nav, el; + + document.removeEventListener('DOMContentLoaded', J.run, false); + + J.initNavLinks(); + + if (!Main.hasMobileLayout) { + AdminTools.init(); + } + + if (Config.revealSpoilers) { + $.addClass(document.body, 'reveal-img-spoilers'); + } + + if (Config.threadUpdater && Main.tid) { + if (Config.changeUpdateDelay) { + ThreadUpdater.delayIdHidden = 3; + ThreadUpdater.delayRange = [ 5, 10, 15, 20, 30, 60 ]; + ThreadUpdater.apiUrlFilter = J.apiUrlFilter; + } + } + + boards = localStorage.getItem('4chan-boardlist') || ''; + + if (Main.board != 'j' && (boards == 'all' || boards.split(/[, ]+/).indexOf(Main.board) != -1)) { + if (Config.useIconButtons && !Main.hasMobileLayout) { + if (Main.tid) { + posts = document.getElementById('t' + Main.tid).getElementsByClassName('postInfo'); + J.postCount = posts.length; + if (J.postCount > J.chunkSize) { + J.nextChunk = posts[0]; + window.addEventListener('scroll', J.onScroll, false); + J.onScroll(); + } + else { + J.onParsingDone(); + } + } + else { + J.onParsingDone(); + } + + document.addEventListener('4chanParsingDone', J.onParsingDone, false); + } + + document.addEventListener('4chanPostMenuReady', J.onPostMenuReady, false); + } + + if (nav = $.id('boardSelectMobile')) { + el = document.createElement('option'); + el.value = 'j'; + el.textContent = '/j/ - Janitors & Moderators'; + nav.insertBefore(el, nav.firstElementChild); + } +}; + +J.getCookie = function(name) { + var i, c, ca, key; + + key = name + "="; + ca = document.cookie.split(';'); + + for (i = 0; c = ca[i]; ++i) { + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(key) === 0) { + return decodeURIComponent(c.substring(key.length, c.length)); + } + } + return null; +}; + +J.addCss = function() { + var style, css; + + css = '\ +#adminToolbox {\ + max-width: 256px;\ + display: block;\ + position: absolute;\ + padding: 3px;\ +}\ +#adminToolbox h4 {\ + font-size: 12px;\ + margin: 2px 0 0;\ + padding: 0;\ + font-weight: normal;\ +}\ +#adminToolbox li {\ + list-style: none;\ +}\ +#adminToolbox ul {\ + padding: 0;\ + margin: 2px 0 0 10px;\ +}\ +#atHeader {\ + height: 17px;\ + font-weight: bold;\ + padding-bottom: 2px;\ +}\ +#atRefresh {\ + margin: -1px 0 0 3px;\ +}\ +.post-ip {\ + margin-left: 5px;\ +}\ +#delete-prompt > div {\ + text-align: center;\ +}\ +#watchList li:first-child {\ + margin-top: 3px;\ + padding-top: 2px;\ + border-top: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.photon #atHeader {\ + border-bottom: 1px solid #ccc;\ +}\ +.yotsuba_new #atHeader {\ + border-bottom: 1px solid #d9bfb7;\ +}\ +.yotsuba_b_new #atHeader {\ + border-bottom: 1px solid #b7c5d9;\ +}\ +.tomorrow #atHeader {\ + border-bottom: 1px solid #111;\ +}\ +#at-illegal {\ + color: red;\ +}\ +#at-msg-cnt {\ + display: none;\ +}\ +.j-newposts {\ + font-weight: bold !important;\ +}\ +#j-link,\ +#j-link-bot {\ + margin-right: 3px;\ + display: inline-block;\ + margin-left: 3px;\ +}\ +#boardList input {\ + width: 385px;\ + margin: auto;\ + display: block;\ +}\ +#boardListSave {\ + margin-top: 5px;\ +}\ +#banReqClose {\ + float: right;\ +}\ +#banReq iframe {\ + overflow: hidden;\ +}\ +#banReq {\ + display: block;\ + position: fixed;\ + padding: 2px;\ + font-size: 10pt;\ + height: 250px;\ +}\ +#banReqHeader {\ + text-align: center;\ + margin-bottom: 1px;\ + padding: 0;\ + height: 18px;\ + line-height: 18px;\ +}\ +#captchaFormPart {\ + display: none;\ +}\ +.mobileExtControls {\ + float: right;\ + font-size: 11px;\ + margin-bottom: 3px;\ +}\ +.ws .mobileExtControls {\ + color: #34345C;\ +}\ +.nws .mobileExtControls {\ + color: #0000EE;\ +}\ +.reply .mobileExtControls {\ + margin-right: 5px;\ +}\ +.mobileExtControls span {\ + margin-left: 10px;\ +}\ +.mobileExtControls span:after {\ + content: "]";\ +}\ +.mobileExtControls span:before {\ + content: "[";\ +}\ +.nws .mobileExtControls span:after {\ + color: #800000;\ +}\ +.nws .mobileExtControls span:before {\ + color: #800000;\ +}\ +.ws .mobileExtControls span:after {\ + color: #000;\ +}\ +.ws .mobileExtControls span:before {\ + color: #000;\ +}\ +.m-dark .mobileExtControls,\ +.m-dark .mobileExtControls span:after,\ +.m-dark .mobileExtControls span:before {\ + color: #707070 !important;\ +}\ +.dd-admin {\ + text-indent: 5px;\ +}\ +.dd-admin:before {\ + color: #FF0000;\ + content: "•";\ + left: -3px;\ + position: absolute;\ +}\ +.extPanel {\ + border: 1px solid rgba(0, 0, 0, 0.2);\ +}\ +.extPanel img.pointer { width: 18px; height: 18px }\ +.drag {\ + -moz-user-select: none !important;\ + cursor: move !important;\ +}\ +.reveal-img-spoilers .imgspoiler::before {\ + content: " ";\ + width:0.75em;\ + height:0.75em;\ + border-radius: 0.5em;\ + position: absolute;\ + display: block;\ + background: red;\ + margin-top: 1px;\ + margin-left: 1px;\ + pointer-events: none;\ +}\ +.reveal-img-spoilers.is_catalog .imgspoiler::before { margin-top: 4px; margin-left: 12px;}\ +.reveal-img-spoilers .imgspoiler:hover::before { background: #fff; }'; + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = css; + document.head.appendChild(style); +}; + +if (/https?:\/\/boards\.(?:4chan|4channel)\.org\/[a-z0-9]+\/catalog($|#.*$)/.test(location.href)) { + J.initCatalog(); +} +else { + J.init(); + //J.run(); +} + +})(); diff --git a/js/janitor.js b/js/janitor.js new file mode 100644 index 0000000..5d93551 --- /dev/null +++ b/js/janitor.js @@ -0,0 +1 @@ +!function(){var e={cacheTTL:6e4,autoRefreshDelay:12e4,autoRefreshTimeout:null};e.initVisibilityAPI=function(){this.hidden="hidden",this.visibilitychange="visibilitychange","undefined"==typeof document.hidden&&("mozHidden"in document?(this.hidden="mozHidden",this.visibilitychange="mozvisibilitychange"):"webkitHidden"in document?(this.hidden="webkitHidden",this.visibilitychange="webkitvisibilitychange"):"msHidden"in document&&(this.hidden="msHidden",this.visibilitychange="msvisibilitychange")),document.addEventListener(this.visibilitychange,this.onVisibilityChange,!1)},e.init=function(){var n,o;e.initVisibilityAPI(),(n=document.createElement("div")).className="extPanel reply",n.id="adminToolbox",n.setAttribute("data-trackpos","AT-position"),Config["AT-position"]?n.style.cssText=Config["AT-position"]:(n.style.right="10px",n.style.top="380px"),n.style.position=Config.fixedAdminToolbox?"fixed":"",o='
    Janitor ToolsRefresh

    Reports: ? (?)

    Messages: ?

    ',n.innerHTML=o,document.body.appendChild(n),e.refreshReportCount(),Draggable.set($.id("atHeader"))},e.onVisibilityChange=function(){var t;t=e,document[e.hidden]?(clearInterval(t.autoRefreshTimeout),t.autoRefreshTimeout=null):(t.refreshReportCount(),t.autoRefreshTimeout=setInterval(t.refreshReportCount,t.autoRefreshDelay))},e.refreshReportCount=function(n){var o,i;if(!0!==n&&(i=localStorage.getItem("4chan-cache-rc"))&&(i=JSON.parse(i)).ts>Date.now()-e.cacheTTL)return $.id("at-total").textContent=i.data[0],$.id("at-illegal").textContent=i.data[1],$.id("at-msg-cnt").style.display=i.data[2]?"block":"",void($.id("at-msg").textContent=i.data[2]||0);(o=new XMLHttpRequest).open("GET","https://"+t.reportsSubDomain+".4chan.org/H429f6uIsUqU.php",!0),o.withCredentials=!0,o.onload=function(){var e,t,n,o;if(200==this.status){try{t=JSON.parse(this.responseText)}catch(i){return void console.log(i)}if("success"!==t.status)return void console.log(t.message);o=(n=t.data).msg||0,$.id("at-msg-cnt").style.display=o?"block":"",$.id("at-msg").textContent=o,$.id("at-total").textContent=n.total,$.id("at-illegal").textContent=n.illegal,e={ts:Date.now(),data:[n.total,n.illegal,o]},e=JSON.stringify(e),localStorage.setItem("4chan-cache-rc",e),document.dispatchEvent(new CustomEvent("4chanATUpdated"))}else this.onerror()},o.onerror=function(){console.log("Error while refreshing the report count (Status: "+this.status+").")},o.onloadend=function(){$.id("atRefresh").src=Main.icons.refresh},$.id("atRefresh").src=Main.icons.rotate,o.send(null)},e.resetMsgCount=function(){var e;$.id("at-msg").textContent=0,(e=localStorage.getItem("4chan-cache-rc"))&&((e=JSON.parse(e)).data[2]=0,e=JSON.stringify(e),localStorage.setItem("4chan-cache-rc",e))};var t={nextChunkIndex:0,nextChunk:null,chunkSize:100,reportsSubDomain:"reports"};t.initIconsCatalog=function(){var e,t,n;if(Main.icons={up:"arrow_up.png",down:"arrow_down.png",right:"arrow_right.png",download:"arrow_down2.png",refresh:"refresh.png",cross:"cross.png",gis:"gis.png",iqdb:"iqdb.png",minus:"post_expand_minus.png",plus:"post_expand_plus.png",rotate:"post_expand_rotate.gif",quote:"quote.png",report:"report.png",notwatched:"watch_thread_off.png",watched:"watch_thread_on.png",help:"question.png"},t={yotsuba_new:"futaba/",futaba_new:"futaba/",yotsuba_b_new:"burichan/",burichan_new:"burichan/",tomorrow:"tomorrow/",photon:"photon/"},n="//s.4cdn.org/image/",window.devicePixelRatio>=2)for(e in Main.icons)Main.icons[e]=Main.icons[e].replace(".","@2x.");n+="buttons/"+t[Main.stylesheet];for(e in Main.icons)Main.icons[e]=n+Main.icons[e]},t.apiUrlFilter=function(e){return e+"?"+Math.round(Date.now()/1e3/3)},t.openDeletePrompt=function(e){var n,o;n='
    Delete Post No.'+(e=e.getAttribute("data-id"))+'Close
    ',(o=document.createElement("div")).className="UIPanel",o.id="delete-prompt",o.innerHTML=n,o.addEventListener("click",t.closeDeletePrompt,!1),document.body.appendChild(o),$.id("delete-prompt-inner").firstElementChild.focus()},t.closeDeletePrompt=function(e){var n;e&&"delete-prompt"!=e.target.id||(n=$.id("delete-prompt"))&&(n.removeEventListener("click",t.closeDeletePrompt,!1),document.body.removeChild(n))},t.deletePost=function(e,n){var o,i,a,s,r,d,l,c;o=e.getAttribute("data-id"),c=$.id("t"+o),a=new FormData,r="https://sys."+$L.d(Main.board)+"/"+Main.board+"/post",d=window.thread_archived?"arcdel":"usrdel",n&&("Delete Image No.",a.append("onlyimgdel","on")),a.append(o,"delete"),a.append("mode",d),a.append("pwd","janitorise"),(l=$.id("delete-prompt-inner")).textContent="Deleting...",(i=new XMLHttpRequest).open("POST",r),i.withCredentials=!0,i.onload=function(){if(e.src=Main.icons.cross,200==this.status)if(-1!=this.responseText.indexOf("Updating")){if(n)(s=$.id("f"+o)).innerHTML='File deleted.';else{if(o==Main.tid)return void(location.href="//boards."+$L.d(Main.board)+"/"+Main.board+"/");c?((s=c.parentNode).removeChild(c.nextElementSibling),s.removeChild(c)):(s=$.id("pc"+o)).parentNode.removeChild(s)}t.closeDeletePrompt()}else l.textContent="Error: Post might have already been deleted, or is a sticky.";else l.textContent="Error: Wrong status while deleting No."+o+" (Status: "+this.status+")."},i.onerror=function(){l.textContent="Error: Error while deleting No."+o+" (Status: "+this.status+")."},i.send(a)},t.openBanReqWindow=function(e){var t;t=e.getAttribute("data-id"),window.open("https://sys."+$L.d(Main.board)+"/"+Main.board+"/admin?mode=admin&admin=banreq&id="+t,"_blank","scrollBars=yes,resizable=no,toolbar=no,menubar=no,location=no,directories=no,width=400,height=245")},t.openBanReqFrame=function(e){var n;this.banReqCnt&&this.close(),n=e.getAttribute("data-id"),this.banReqCnt=document.createElement("div"),this.banReqCnt.id="banReq",this.banReqCnt.className="extPanel reply",this.banReqCnt.setAttribute("data-trackpos","banReq-position"),Config["banReq-position"]?this.banReqCnt.style.cssText=Config["banReq-position"]:(this.banReqCnt.style.right="0px",this.banReqCnt.style.top="50px"),this.banReqCnt.innerHTML='
    Ban Request No.'+n+'X
    ',document.body.appendChild(this.banReqCnt),window.addEventListener("message",t.onMessage,!1),document.addEventListener("keydown",t.onKeyDown,!1),$.id("banReqClose").addEventListener("click",t.closeBanReqFrame,!1),Draggable.set($.id("banReqHeader"))},t.closeBanReqFrame=function(){window.removeEventListener("message",t.onMessage,!1),document.removeEventListener("keydown",t.onKeyDown,!1),Draggable.unset($.id("banReqHeader")),$.id("banReqClose").removeEventListener("click",t.closeBanReqFrame,!1),document.body.removeChild(t.banReqCnt),t.banReqCnt=null},t.processMessage=function(e){return e?{cmd:(e=e.split("-"))[0],type:e[1],id:e.slice(2).join("-")}:{}},t.onKeyDown=function(e){27!=e.keyCode||e.ctrlKey||e.altKey||e.shiftKey||e.metaKey||t.closeBanReqFrame()},t.onMessage=function(e){var n;e.origin==="https://sys."+$L.d(Main.board)&&"ban"===(n=t.processMessage(e.data)).type&&("done"!==n.cmd&&"cancel"!==n.cmd||t.closeBanReqFrame())},t.onClick=function(n){var o,i;if((o=n.target)!=document&&(i=o.getAttribute("data-cmd")))switch(i){case"at-refresh":e.refreshReportCount(!0);break;case"delete-post":case"delete-image":t.deletePost(o,"delete-image"===i);break;case"open-delete-prompt":t.openDeletePrompt(o);break;case"close-delete-prompt":t.closeDeletePrompt();break;case"open-banreq-prompt":Config.inlinePopups?t.openBanReqFrame(o):t.openBanReqWindow(o);break;case"at-msg":e.resetMsgCount();break;case"toggle-file-spoiler":t.setFileSpoiler(o);break;case"prompt-spoiler":confirm("Toggle spoiler?")&&t.setFileSpoiler(o);break;case"boardlist-open":t.openBoardList();break;case"boardlist-close":t.closeBoardList();break;case"boardlist-save":t.saveBoardList(),t.closeBoardList()}},t.onScroll=function(){for(var e;t.nextChunk.offsetTop=t.postCount)return t.parseRange(t.nextChunkIndex,t.postCount),window.removeEventListener("scroll",t.onScroll,!1),!1;t.parseRange(t.nextChunkIndex,e)}return!0},t.parseRange=function(e,n){var o,i,a;for(a=document.getElementById("t"+Main.tid).getElementsByClassName("postInfo"),o=e;o',window.spoilers&&(el=$.id("fT"+n))&&(o+='S'),window.thread_archived||(o+='B'),(i=document.createElement("div")).className="extControls",i.innerHTML=o,a=e.getElementsByClassName("postMenuBtn")[0],e.insertBefore(i,a)},t.displayJCount=function(e,t,n,o){var i;$.addClass(e,"j-newposts"),$.addClass(t,"j-newposts"),e.setAttribute("data-no",n),t.setAttribute("data-no",n),e.textContent=t.textContent="j +"+o,i=o+" new post"+(o>1?"s":""),Main.addTooltip(e,i,"j-tooltip"),Main.addTooltip(t,i,"j-tooltip-bot")},t.refreshJCount=function(){var e,n,o,i;n=$.id("j-link"),o=$.id("j-link-bot"),n&&o&&(n=n.firstElementChild,o=o.firstElementChild,(e=localStorage.getItem("4chan-j-count"))&&(e=JSON.parse(e)),!e||Date.now()-e.time>=1e4?((i=new XMLHttpRequest).open("GET","https://sys.4chan.org/j/1mcQTXbjW5WO.php?&"+Date.now()),i.withCredentials=!0,i.onloadend=function(){var i,a,s;200==this.status||304==this.status?(i=JSON.parse(this.responseText),e&&"j"!=Main.board?i.no>e.no&&(s=i.no-e.no,t.displayJCount(n,o,i.no,s),a={time:Date.now(),no:e.no,delta:s}):a={time:Date.now(),no:i.no},a&&localStorage.setItem("4chan-j-count",JSON.stringify(a))):console.log("Error: Could not load /j/ post count (Status: "+this.status+").")},i.send(null)):e.delta&&t.displayJCount(n,o,e.no,e.delta))},t.clearJCount=function(){var e,t,n,o;n=$.id("j-tooltip"),o=$.id("j-tooltip-bot"),n&&(t=this.getAttribute("data-no"),e={time:Date.now(),no:t},localStorage.setItem("4chan-j-count",JSON.stringify(e)),n.parentNode.removeChild(n),o.parentNode.removeChild(o),setTimeout(function(){var e=$.cls("j-newposts");e[0]&&(e[0].textContent="j",$.removeClass(e[0],"j-newposts"),e[0].textContent="j",$.removeClass(e[0],"j-newposts"))},10))},t.icons={ban:"ban.png",spoiler:"s.png"},t.initIcons=function(){var e,n;if(n="//s.4cdn.org/image/buttons/"+{yotsuba_new:"futaba/",futaba_new:"futaba/",yotsuba_b_new:"burichan/",burichan_new:"burichan/",tomorrow:"tomorrow/",photon:"photon/"}[Main.stylesheet],window.devicePixelRatio>=2)for(e in t.icons)t.icons[e]=t.icons[e].replace(".","@2x.");for(e in t.icons)t.icons[e]=n+t.icons[e]},t.initNavLinks=function(){var e,n,o;n=$.id("navtopright"),o=$.id("navbotright"),(e=document.createElement("span")).id="j-link",e.innerHTML='[j]',e.firstElementChild.addEventListener("mouseup",t.clearJCount,!1),n.parentNode.insertBefore(e,n),(e=e.cloneNode(!0)).id="j-link-bot",e.firstElementChild.addEventListener("mouseup",t.clearJCount,!1),o.parentNode.insertBefore(e,o),t.refreshJCount()},t.openBoardList=function(){var e;$.id("boardList")||((e=document.createElement("div")).id="boardList",e.className="UIPanel",e.setAttribute("data-cmd","boardlist-close"),e.innerHTML='
    BoardsClose
    ',document.body.appendChild(e),e.addEventListener("click",this.onClick,!1))},t.saveBoardList=function(){var e;(e=$.id("boardListBox"))&&localStorage.setItem("4chan-boardlist",e.value)},t.closeBoardList=function(){var e;(e=$.id("boardList"))&&(e.removeEventListener("click",this.onClick,!1),document.body.removeChild(e))},t.setFileSpoiler=function(e){var n,o,i,a;o=e.getAttribute("data-id"),i=e.getAttribute("data-flag"),o&&(a=$.id("f"+o),i||(i=$.cls("imgspoiler",a.parentNode)[0]?0:1),a&&!a.hasAttribute("data-processing")&&((n=new XMLHttpRequest).open("GET","https://sys."+$L.d(Main.board)+"/"+Main.board+"/admin.php?admin=spoiler&pid="+o+"&flag="+i,!0),n.withCredentials=!0,n.onload=t.onFileSpoilerLoad,n.onerror=t.onFileSpoilerError,n._pid=+o,n._flag=+i,Feedback.notify("Processing...",null),a.setAttribute("data-processing","1"),n.send(null)))},t.onFileSpoilerLoad=function(){var e,t;Feedback.hideMessage(),"1"===this.responseText?(e=$.id("f"+this._pid))&&(e.removeAttribute("data-processing"),(e=$.cls("fileThumb",e)[0])&&(this._flag?($.addClass(e,"imgspoiler"),(t=e.previousElementSibling).setAttribute("title",t.firstElementChild.textContent),Config.revealSpoilers||((e=$.tag("img",e)[0]).style.width=e.style.height="100px",e.src="//s.4cdn.org/image/spoiler-"+Main.board+".png")):(Config.revealSpoilers||Parser.revealImageSpoiler(e),$.removeClass(e,"imgspoiler")))):"-1"===this.responseText?Feedback.error("You are not logged in"):Feedback.error("Couldn't set spoiler flag for post No."+this._pid)},t.onFileSpoilerError=function(){var e;(e=$.id("f"+this._pid))&&(e.removeAttribute("data-processing"),Feedback.error("Couldn't update the spoiler flag for post No."+this.pid))},t.initCatalog=function(){window.Main={board:location.pathname.split(/\//)[1]},window.Main.addTooltip=function(e,t,n){var o,i;return(o=document.createElement("div")).className="click-me",n&&(o.id=n),o.innerHTML=t||"Change your settings",e.parentNode.appendChild(o),i=(e.offsetWidth-o.offsetWidth+e.offsetLeft-o.offsetLeft)/2,o.style.marginLeft=i+"px",o},(t.stylesheet=t.getCookie(window.style_group))?t.stylesheet=t.stylesheet.toLowerCase().replace(/ /g,"_"):t.stylesheet="nws_style"==style_group?"yotsuba_new":"yotsuba_b_new",Main.stylesheet=t.stylesheet,t.initIconsCatalog(),t.addCss(),document.addEventListener("click",t.onClick,!1),t.runCatalog()},t.runCatalog=function(){var n;t.initNavLinks(),FC.hasMobileLayout||e.init(),n=$.id("threads"),$.on(n,"mouseover",t.onThreadMouseOver)},t.init=function(){Config.boardList=!0,SettingsMenu.options.Janitor={boardList:['Janitor Boards [Select]',"Select boards to enable janitor buttons on",!0],useIconButtons:["Use icon buttons","Display old-style buttons instead of using drop-down"],changeUpdateDelay:["Reduce auto-update delay","Reduce the thread updater delay",!0],fixedAdminToolbox:["Pin Janitor Tools to the page","Janitor Tools will scroll with you"],inlinePopups:["Inline ban request panel","Open ban request panel in browser window, instead of a popup"],disableMngExt:["Disable janitor extension","Completely disable the janitor extension (overrides any checked boxes)",!0]},Config.disableMngExt||(t.addCss(),Config.useIconButtons&&t.initIcons(),QR.noCooldown=QR.noCaptcha=!0,document.addEventListener("click",t.onClick,!1),document.addEventListener("DOMContentLoaded",t.run,!1))},t.run=function(){var n,o,i,a;document.removeEventListener("DOMContentLoaded",t.run,!1),t.initNavLinks(),Main.hasMobileLayout||e.init(),Config.revealSpoilers&&$.addClass(document.body,"reveal-img-spoilers"),Config.threadUpdater&&Main.tid&&Config.changeUpdateDelay&&(ThreadUpdater.delayIdHidden=3,ThreadUpdater.delayRange=[5,10,15,20,30,60],ThreadUpdater.apiUrlFilter=t.apiUrlFilter),n=localStorage.getItem("4chan-boardlist")||"","j"==Main.board||"all"!=n&&-1==n.split(/[, ]+/).indexOf(Main.board)||(Config.useIconButtons&&!Main.hasMobileLayout&&(Main.tid?(o=document.getElementById("t"+Main.tid).getElementsByClassName("postInfo"),t.postCount=o.length,t.postCount>t.chunkSize?(t.nextChunk=o[0],window.addEventListener("scroll",t.onScroll,!1),t.onScroll()):t.onParsingDone()):t.onParsingDone(),document.addEventListener("4chanParsingDone",t.onParsingDone,!1)),document.addEventListener("4chanPostMenuReady",t.onPostMenuReady,!1)),(i=$.id("boardSelectMobile"))&&((a=document.createElement("option")).value="j",a.textContent="/j/ - Janitors & Moderators",i.insertBefore(a,i.firstElementChild))},t.getCookie=function(e){var t,n,o,i;for(i=e+"=",o=document.cookie.split(";"),t=0;n=o[t];++t){for(;" "==n.charAt(0);)n=n.substring(1,n.length);if(0===n.indexOf(i))return decodeURIComponent(n.substring(i.length,n.length))}return null},t.addCss=function(){var e,t;t='#adminToolbox { max-width: 256px; display: block; position: absolute; padding: 3px;}#adminToolbox h4 { font-size: 12px; margin: 2px 0 0; padding: 0; font-weight: normal;}#adminToolbox li { list-style: none;}#adminToolbox ul { padding: 0; margin: 2px 0 0 10px;}#atHeader { height: 17px; font-weight: bold; padding-bottom: 2px;}#atRefresh { margin: -1px 0 0 3px;}.post-ip { margin-left: 5px;}#delete-prompt > div { text-align: center;}#watchList li:first-child { margin-top: 3px; padding-top: 2px; border-top: 1px solid rgba(0, 0, 0, 0.20);}.photon #atHeader { border-bottom: 1px solid #ccc;}.yotsuba_new #atHeader { border-bottom: 1px solid #d9bfb7;}.yotsuba_b_new #atHeader { border-bottom: 1px solid #b7c5d9;}.tomorrow #atHeader { border-bottom: 1px solid #111;}#at-illegal { color: red;}#at-msg-cnt { display: none;}.j-newposts { font-weight: bold !important;}#j-link,#j-link-bot { margin-right: 3px; display: inline-block; margin-left: 3px;}#boardList input { width: 385px; margin: auto; display: block;}#boardListSave { margin-top: 5px;}#banReqClose { float: right;}#banReq iframe { overflow: hidden;}#banReq { display: block; position: fixed; padding: 2px; font-size: 10pt; height: 250px;}#banReqHeader { text-align: center; margin-bottom: 1px; padding: 0; height: 18px; line-height: 18px;}#captchaFormPart { display: none;}.mobileExtControls { float: right; font-size: 11px; margin-bottom: 3px;}.ws .mobileExtControls { color: #34345C;}.nws .mobileExtControls { color: #0000EE;}.reply .mobileExtControls { margin-right: 5px;}.mobileExtControls span { margin-left: 10px;}.mobileExtControls span:after { content: "]";}.mobileExtControls span:before { content: "[";}.nws .mobileExtControls span:after { color: #800000;}.nws .mobileExtControls span:before { color: #800000;}.ws .mobileExtControls span:after { color: #000;}.ws .mobileExtControls span:before { color: #000;}.m-dark .mobileExtControls,.m-dark .mobileExtControls span:after,.m-dark .mobileExtControls span:before { color: #707070 !important;}.dd-admin { text-indent: 5px;}.dd-admin:before { color: #FF0000; content: "\u2022"; left: -3px; position: absolute;}.extPanel { border: 1px solid rgba(0, 0, 0, 0.2);}.extPanel img.pointer { width: 18px; height: 18px }.drag { -moz-user-select: none !important; cursor: move !important;}.reveal-img-spoilers .imgspoiler::before { content: " "; width:0.75em; height:0.75em; border-radius: 0.5em; position: absolute; display: block; background: red; margin-top: 1px; margin-left: 1px; pointer-events: none;}.reveal-img-spoilers.is_catalog .imgspoiler::before { margin-top: 4px; margin-left: 12px;}.reveal-img-spoilers .imgspoiler:hover::before { background: #fff; }',(e=document.createElement("style")).setAttribute("type","text/css"),e.textContent=t,document.head.appendChild(e)},/https?:\/\/boards\.(?:4chan|4channel)\.org\/[a-z0-9]+\/catalog($|#.*$)/.test(location.href)?t.initCatalog():t.init()}(); \ No newline at end of file diff --git a/js/minify.rb b/js/minify.rb new file mode 100644 index 0000000..bdef038 --- /dev/null +++ b/js/minify.rb @@ -0,0 +1,74 @@ +#!/usr/bin/env ruby +# encoding: utf-8 + +require 'json' +require 'net/http' + +if !ARGV[0] + puts 'usage: minify.rb filename' + exit! +end + +def minify_js + api_url = 'http://closure-compiler.appspot.com/compile' + filename = ARGV[0] + + out_dir = File.expand_path(File.dirname(__FILE__)) + out_file = out_dir + '/' + filename.split('-')[0] + '.js' + + puts 'Compiling...' + + resp = Net::HTTP.post_form(URI(api_url), { + output_format: 'json', + output_info: [ 'compiled_code', 'warnings', 'errors', 'statistics' ], + compilation_level: 'SIMPLE_OPTIMIZATIONS', + language: 'ECMASCRIPT5', + js_code: File.open(filename, 'r:UTF-8') { |f| f.read } + }) + + if resp.kind_of?(Net::HTTPSuccess) + data = JSON.parse(resp.body, symbolize_names: true) + + if data[:serverErrors] + puts 'Server errors:' + data[:serverErrors].each do |err| + puts " #{err[:error]} (#{err[:code]})" + end + exit! + end + + if data[:errors] + puts 'Errors:' + data[:errors].each do |err| + puts " #{err[:error]} (#{err[:lineno]}, #{err[:charno]})" + end + exit! + end + + if data[:warnings] + puts 'Warnings:' + data[:warnings].each do |err| + puts " #{err[:warning]} (#{err[:lineno]}, #{err[:charno]})" + end + end + + if data[:statistics] + puts 'Statistics:' + data[:statistics].each do |k, v| + puts " #{k}: #{v}" + end + end + + puts 'Writing...' + File.open(out_file, 'wb') do |f| + f.write(data[:compiledCode]) + end + + else + puts "Bad status code: #{resp}" + end + + puts 'Done' +end + +minify_js diff --git a/js/mod-unminified.js b/js/mod-unminified.js new file mode 100644 index 0000000..bf7aaee --- /dev/null +++ b/js/mod-unminified.js @@ -0,0 +1,2208 @@ +/** + * Mod Extension + */ + +(function () { +var J = { + isCatalog: false, + colours: {}, + posterids: {}, + nextChunkIndex: 0, + nextChunk: null, + chunkSize: 100, + sameIDActive: false, + + parserEventBound: false, + + autoReloadCatInterval: null, + autoReloadCatDelay: 30000, + + samePostersMap: {}, + + xhrs: {}, + + reportsSubDomain: 'reports', + teamSubDomain: 'team', + + flags: [] +}; + +J.bin2hex = function(data) { + var i, l, hex, c; + + hex = ''; + l = data.length; + + for (i = 0; i < l; ++i) { + c = data.charCodeAt(i); + hex += (c >> 4).toString(16); + hex += (c & 0xF).toString(16); + } + + return hex; +}; + +J.getFileMD5FromPid = function(pid) { + var el, data; + + el = $.id('f' + pid); + + if (!el) { + return false; + } + + el = $.qs('img[data-md5]', el); + + if (!el) { + return false; + } + + data = window.atob(el.getAttribute('data-md5')); + + return J.bin2hex(data); +}; + +J.onGetMD5Click = function(el) { + var md5, pid = el.getAttribute('data-id'); + + md5 = J.getFileMD5FromPid(pid); + + if (md5 === false) { + alert('Post or file not found'); + } + else { + prompt('', md5); + } +}; + +J.apiUrlFilter = function(url) { + return url + '?' + Math.round(Date.now() / 1000 / 3); +}; + +J.openDeletePrompt = function(id) { + var html, cnt; + + id = id.getAttribute('data-id'); + + html = '
    Delete Post No.' + id + + 'Close' + + '
    ' + + ' ' + + ''; + + if ($.id((J.isCatalog ? 'thread-' : 't') + id) && !window.thread_archived) { + html += ' '; + } + + if (!window.thread_archived && !J.isCatalog) { + html += '
    []'; + } + + html += '
    '; + + cnt = document.createElement('div'); + cnt.className = 'UIPanel'; + cnt.id = 'delete-prompt'; + + cnt.innerHTML = html; + + document.addEventListener('keydown', J.onKeyDown, false); + cnt.addEventListener('click', J.closeDeletePrompt, false); + document.body.appendChild(cnt); + + $.id('delete-prompt-inner').firstElementChild.focus(); +}; + +J.addPosterIds = function(pid, hash, isMobile) { + var post, cnt, el, name, hand, p; + + post = !isMobile ? $.id('pi' + pid) : $.id('pim' + pid); + + if (!window.user_ids || !(el = $.cls('posteruid', post)[0])) { + el = $.el('span'); + + cnt = $.cls('nameBlock', post)[0]; + name = $.cls('name', cnt)[0]; + + if (name.classList.contains('capcode')) { + return; + } + + cnt.insertBefore(el, name.nextSibling); + + if (!isMobile) { + cnt.insertBefore(document.createTextNode(' '), name.nextSibling); + } + } + + el.innerHTML = '(ID: ' + hash + ')'; + el.className = 'posteruid id_' + hash; + + hand = el.firstElementChild; + + IDColor.apply(hand); + + el.addEventListener('click', window.idClick, false); + + if (window.currentHighlighted && el.className.indexOf('id_' + window.currentHighlighted) != -1) { + p = el.parentNode.parentNode.parentNode; + p.className = 'highlight ' + p.className; + } +} + +J.onSamePostersLoaded = function() { + var posts, hash, pid, tmp, isMobile; + + if (this.status != 200 && this.status != 304) { + return; + } + + posts = JSON.parse(this.responseText); + + if (!posts) { + return; + } + + isMobile = Main.hasMobileLayout; + + if (!IDColor.enabled) { + tmp = window.user_ids; + window.user_ids = true; + IDColor.init(); + window.user_ids = tmp; + } + + if (!J.sameIDActive) { + J.sameIDActive = true; + } + + for (pid in posts) { + if (J.samePostersMap[pid]) { + continue; + } + + hash = posts[pid]; + + J.samePostersMap[pid] = true; + + J.addPosterIds(pid, hash, isMobile); + } +} + +J.loadSamePosters = function(from) { + var url, theNode, xhr; + + if (!J.parserEventBound) { + document.addEventListener('4chanParsingDone', J.onParsingDone, false); + } + + url = 'https://sys.' + $L.d(Main.board) + '/' + Main.board + '/admin?admin=adminext&thread=' + Main.tid; + + if (from) { + url += '&from=' + from; + } + + xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.withCredentials = true; + xhr.onload = J.onSamePostersLoaded; + + xhr.send(null); +}; + +J.closeDeletePrompt = function (e) { + var prompt; + + if (!e || e.target.id == 'delete-prompt') { + if (prompt = $.id('delete-prompt')) { + document.removeEventListener('keydown', J.onKeyDown, false); + prompt.removeEventListener('click', J.closeDeletePrompt, false); + document.body.removeChild(prompt); + } + } +}; + +J.checkDeletedPosts = function () { + var url, xhr; + + if (!Main.tid) { + return; + } + + url = '//a.4cdn.org/' + Main.board + '/res/' + Main.tid + '.json'; + + xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onload = function () { + if (this.status == 200 || this.status == 304) { + ThreadUpdater.markDeletedReplies(Parser.parseThreadJSON(this.responseText)); + } + }; + + xhr.send(null); +}; + +J.get_random_light_color = function () { + var letters = 'ABCDE'.split(''); + var color = '#'; + for (var i = 0; i < 3; i++) { + color += letters[Math.floor(Math.random() * letters.length)]; + } + return color; +}; + +J.deletePost = function (btn, imageOnly) { + var id, xhr, form, msg, el, url, mode, delall, del, isOp, resp; + + id = btn.getAttribute('data-id'); + + isOp = !J.isCatalog && $.id('t' + id); + + form = new FormData(); + msg = 'Delete Post No.'; + url = 'https://sys.' + $L.d(Main.board) + '/' + Main.board; + + if (window.thread_archived) { + mode = 'arcdel'; + } + else { + mode = 'usrdel'; + delall = !J.isCatalog && $.id('delete-all-by-ip').checked; + } + + if (delall) { + mode = 'admin.php'; + form.append('admin', 'delall'); + form.append('id', id); + } + + if (delall) { + url += '/admin'; + } + else { + url += '/post'; + } + + if (imageOnly) { + msg = 'Delete Image No.'; + form.append('onlyimgdel', 'on'); + } + + form.append(id, 'delete'); + form.append('mode', mode); + form.append('pwd', 'janitorise'); + + (del = $.id('delete-prompt-inner')).textContent = 'Deleting...'; + + xhr = new XMLHttpRequest(); + xhr.open('POST', url); + xhr.withCredentials = true; + xhr.onload = function () { + var builtMsg; + btn.src = Main.icons.cross; + if (this.status == 200) { + if ((!delall && this.responseText.indexOf('Updating') != -1) || (delall && this.responseText.indexOf('deleted') != -1)) { + if (J.isCatalog) { + if (el = $.id('thread-' + id)) { + $.addClass(el, 'disabled'); + } + } + else if (!imageOnly) { + if (id == Main.tid) { + location.href = '//boards.' + $L.d(Main.board) + '/' + Main.board + '/'; + return; + } + else { + if (delall) { + builtMsg = document.createElement('span'); + builtMsg.innerHTML = '

    (YOU HAVE DELETED ALL POSTS BY THIS IP)'; + el = $.id('m' + id); + el.appendChild(builtMsg); + J.checkDeletedPosts(); + } + else { + if (isOp) { + el = isOp.parentNode; + el.removeChild(isOp.nextSibling); + el.removeChild(isOp); + } + else { + el = $.id('pc' + id); + el.parentNode.removeChild(el); + } + } + } + } + else { + el = $.id('f' + id); + el.innerHTML = 'File deleted.'; + + if (delall) { + builtMsg = document.createElement('span'); + builtMsg.innerHTML = '

    (YOU HAVE DELETED ALL IMAGES BY THIS IP)'; + el = $.id('m' + id); + el.appendChild(builtMsg); + } + } + + J.closeDeletePrompt(); + } + else { + if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) { + del.textContent = resp[1]; + } + else { + del.textContent = 'Error: Something went wrong.'; + } + } + } + else { + del.textContent = 'Error: Wrong status while deleting No.' + id + ' (Status: ' + this.status + ').'; + } + }; + xhr.onerror = function () { + del.textContent = 'Error: Error while deleting No.' + id + ' (Status: ' + this.status + ').'; + }; + + xhr.send(form); +}; + +J.forceArchive = function(btn) { + var id, xhr, form, msg, url, del, resp; + + id = btn.getAttribute('data-id'); + + form = new FormData(); + msg = 'Archive Thread No.'; + + url = 'https://sys.' + $L.d(Main.board) + '/' + Main.board + '/post'; + + form.append('id', id); + form.append('mode', 'forcearchive'); + + (del = $.id('delete-prompt-inner')).textContent = 'Archiving...'; + + xhr = new XMLHttpRequest(); + xhr.open('POST', url); + xhr.withCredentials = true; + xhr.onload = function () { + var el; + if (btn.src) { + btn.src = Main.icons.cross; + } + if (this.status == 200) { + if (this.responseText.indexOf('Updating') != -1) { + if (J.isCatalog) { + if (el = $.id('thread-' + id)) { + $.addClass(el, 'disabled'); + } + } + J.closeDeletePrompt(); + } + else { + if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) { + del.textContent = resp[1]; + } + else { + del.textContent = 'Error: Something went wrong.'; + } + } + } + else { + del.textContent = 'Error: Wrong status while archiving No.' + id + ' (Status: ' + this.status + ').'; + } + }; + xhr.onerror = function () { + del.textContent = 'Error: Error while archiving No.' + id + ' (Status: ' + this.status + ').'; + }; + + xhr.send(form); +}; + +J.openBanWindow = function (btn) { + var id; + + id = btn.getAttribute('data-id'); + window.open('https://sys.' + $L.d(Main.board) + '/' + Main.board + '/admin?mode=admin&admin=ban&id=' + id, '_blank', 'scrollBars=yes,resizable=no,toolbar=no,menubar=no,location=no,directories=no,width=400,height=470'); +}; + +J.openBanFrame = function(btn) { + var id; + + if (this.banReqCnt) { + this.closeBanFrame(); + } + + id = btn.getAttribute('data-id'); + + this.banReqCnt = document.createElement('div'); + this.banReqCnt.id = 'banReq'; + this.banReqCnt.className = 'extPanel reply'; + this.banReqCnt.setAttribute('data-trackpos', 'banReq-position'); + + if (Config['banReq-position']) { + this.banReqCnt.style.cssText = Config['banReq-position']; + } + else { + this.banReqCnt.style.right = '0px'; + this.banReqCnt.style.top = '50px'; + } + + this.banReqCnt.innerHTML = + '
    Ban No.' + id + + 'X
    ' + + ''; + + document.body.appendChild(this.banReqCnt); + + window.addEventListener('message', J.onMessage, false); + document.addEventListener('keydown', J.onKeyDown, false); + + $.id('banReqClose').addEventListener('click', J.closeBanFrame, false); + Draggable.set($.id('banReqHeader')); +}; + +J.closeBanFrame = function() { + window.removeEventListener('message', J.onMessage, false); + document.removeEventListener('keydown', J.onKeyDown, false); + Draggable.unset($.id('banReqHeader')); + $.id('banReqClose').removeEventListener('click', J.closeBanFrame, false); + document.body.removeChild(J.banReqCnt); + J.banReqCnt = null; +}; + +J.processMessage = function(data) { + if (!data) { + return {}; + } + + data = data.split('-'); + + return { + cmd: data[0], + type: data[1], + id: data.slice(2).join('-') + }; +}; + +J.onKeyDown = function(e) { + if (e.keyCode == 27 && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { + if (J.banReqCnt) { + J.closeBanFrame(); + } + if (J.threadOptsCnt) { + J.closeThreadOptionsFrame(); + } + if ($.id('delete-prompt')) { + J.closeDeletePrompt(); + } + } +}; + +J.onCatalogKeyDown = function(e) { + if (e.keyCode == 82 && e.shiftKey) { + J.initCatAutoReload(); + } +}; + +J.onMessage = function(e) { + var msg; + + if (e.origin !== 'https://sys.' + $L.d(Main.board)) { + return; + } + + msg = J.processMessage(e.data); + + if (msg.type !== 'ban') { + return; + } + + if (msg.cmd === 'done' || msg.cmd === 'cancel') { + J.closeBanFrame(); + } +}; + +J.initCatAutoReload = function(init) { + var flag; + + flag = sessionStorage.getItem('4chan-c-ar'); + + if (flag) { + if (init) { + window.scrollTo(0, +flag); + J.toggleCatAutoReload(true); + } + else { + J.toggleCatAutoReload(false); + } + } + else { + if (init) { + return; + } + J.toggleCatAutoReload(true); + } +}; + +J.toggleCatAutoReload = function(flag) { + if (flag) { + sessionStorage.setItem('4chan-c-ar', document.documentElement.scrollTop); + J.autoReloadCatInterval = setInterval(J.autoRefreshWindow, J.autoReloadCatDelay); + $.addClass($.id('refresh-btn'), 'active-btn'); + } + else { + sessionStorage.removeItem('4chan-c-ar'); + clearInterval(J.autoReloadCatInterval); + $.removeClass($.id('refresh-btn'), 'active-btn'); + } +}; + +J.autoRefreshWindow = function() { + var el = $.id('ctrl'); + + if (document.documentElement.scrollTop <= el.offsetTop + el.offsetHeight) { + sessionStorage.setItem('4chan-c-ar', document.documentElement.scrollTop); + location.href = location.href; + } +} + +J.openThreadOptions = function(btn) { + var id = btn.getAttribute('data-id'); + window.open('https://sys.' + $L.d(Main.board) + '/' + Main.board + '/admin?mode=admin&admin=opt&id=' + id, '_blank', 'scrollBars=yes,resizable=no,toolbar=no,menubar=no,location=no,directories=no,width=400,height=290'); +}; + +J.openThreadOptionsFrame = function(btn) { + var id; + + if (this.threadOptsCnt) { + this.closeThreadOptionsFrame(); + } + + id = btn.getAttribute('data-id'); + + this.threadOptsCnt = document.createElement('div'); + this.threadOptsCnt.id = 'threadOpts'; + this.threadOptsCnt.className = 'extPanel reply'; + this.threadOptsCnt.setAttribute('data-trackpos', 'threadOpts-position'); + + if (Config['threadOpts-position']) { + this.threadOptsCnt.style.cssText = Config['threadOpts-position']; + } + else { + this.threadOptsCnt.style.right = '0px'; + this.threadOptsCnt.style.top = '50px'; + } + + this.threadOptsCnt.innerHTML = + '
    Thread Options No.' + id + + 'X
    ' + + ''; + + document.body.appendChild(this.threadOptsCnt); + + window.addEventListener('message', J.onThreadOptsDone, false); + document.addEventListener('keydown', J.onKeyDown, false); + + $.id('threadOptsClose').addEventListener('click', J.closeThreadOptionsFrame, false); + Draggable.set($.id('threadOptsHeader')); +}; + +J.closeThreadOptionsFrame = function() { + window.removeEventListener('message', J.onThreadOptsDone, false); + document.removeEventListener('keydown', J.onKeyDown, false); + Draggable.unset($.id('threadOptsHeader')); + $.id('threadOptsClose').removeEventListener('click', J.closeThreadOptionsFrame, false); + document.body.removeChild(J.threadOptsCnt); + J.threadOptsCnt = null; +}; + +J.onThreadOptsDone = function(e) { + if (J.threadOptsCnt && e.origin === 'https://sys.' + $L.d(Main.board) && e.data === 'done-threadopt') { + J.closeThreadOptionsFrame(); + } +}; + +J.setFileSpoiler = function(t) { + var xhr, pid, flag, el; + + pid = t.getAttribute('data-id'); + flag = t.getAttribute('data-flag'); + + if (!pid) { + return; + } + + el = $.id('f' + pid); + + if (!flag) { + flag = $.cls('imgspoiler', el.parentNode)[0] ? 0 : 1; + } + + if (!el || el.hasAttribute('data-processing')) { + return; + } + + xhr = new XMLHttpRequest(); + xhr.open('GET', 'https://sys.' + $L.d(Main.board) + '/' + Main.board + + '/admin.php?admin=spoiler&pid=' + pid + '&flag=' + flag, true); + xhr.withCredentials = true; + xhr.onload = J.onFileSpoilerLoad; + xhr.onerror = J.onFileSpoilerError; + xhr._pid = +pid; + xhr._flag = +flag; + + Feedback.notify('Processing...', null); + + el.setAttribute('data-processing', '1'); + + xhr.send(null); +}; + +J.onFileSpoilerLoad = function() { + var el, el2; + + Feedback.hideMessage(); + + if (this.responseText !== '1') { + if (this.responseText === '-1') { + Feedback.error('You are not logged in'); + } + else { + Feedback.error("Couldn't set spoiler flag for post No." + this._pid); + } + + return; + } + + if (!(el = $.id('f' + this._pid))) { + return; + } + + el.removeAttribute('data-processing'); + + if (!(el = $.cls('fileThumb', el)[0])) { + return; + } + + if (this._flag) { + $.addClass(el, 'imgspoiler'); + + el2 = el.previousElementSibling; + el2.setAttribute('title', el2.firstElementChild.textContent); + + if (!Config.revealSpoilers) { + el = $.tag('img', el)[0]; + el.style.width = el.style.height = '100px'; + el.src = '//s.4cdn.org/image/spoiler-' + Main.board + '.png'; + } + } + else { + if (!Config.revealSpoilers) { + Parser.revealImageSpoiler(el); + } + $.removeClass(el, 'imgspoiler'); + } +}; + +J.onFileSpoilerError = function() { + var el; + + if (!(el = $.id('f' + this._pid))) { + return; + } + + el.removeAttribute('data-processing'); + Feedback.error("Couldn't update the spoiler flag for post No." + this.pid); +}; + +/** + * Multi + */ +var Multi = {}; + +Multi.exec = function(btn) { + var pid, sel; + + if (UA.isOpera && typeof (sel = document.getSelection()) == 'string') {} + else { + sel = window.getSelection().toString(); + } + + if (sel) { + window.open('https://' + J.teamSubDomain + + '.4chan.org/search#{"comment":"' + sel.replace(/[\r\n]+/g, ' ') + '"}'); + } + else { + pid = btn.getAttribute('data-id'); + + window.open('https://team.4chan.org/search?action=from_pid&board=' + Main.board + + '&pid=' + pid); + } +}; + +Multi.prompt = function (ip, pid) { + var cnt, btn, link; + + cnt = $.id('pi' + pid); + btn = $.cls('postMenuBtn', cnt)[0]; + + link = document.createElement('a'); + link.href = 'https://' + J.teamSubDomain + '.4chan.org/search#{"ip":"' + ip + '"}'; + link.setAttribute('target', '_blank'); + link.className = 'post-ip'; + link.textContent = ip; + + cnt.insertBefore(link, btn); +}; + +/** + * Admin tools + */ +var AdminTools = { + cacheTTL: 60000, + autoRefreshDelay: 120000, + autoRefreshTimeout: null +}; + +// FIXME, put it as a helper in extension.js +AdminTools.initVisibilityAPI = function() { + this.hidden = 'hidden'; + this.visibilitychange = 'visibilitychange'; + + if (typeof document.hidden === 'undefined') { + if ('mozHidden' in document) { + this.hidden = 'mozHidden'; + this.visibilitychange = 'mozvisibilitychange'; + } + else if ('webkitHidden' in document) { + this.hidden = 'webkitHidden'; + this.visibilitychange = 'webkitvisibilitychange'; + } + else if ('msHidden' in document) { + this.hidden = 'msHidden'; + this.visibilitychange = 'msvisibilitychange'; + } + } + + document.addEventListener(this.visibilitychange, this.onVisibilityChange, false); +}; + +J.initIconsCatalog = function() { + var key, paths, url; + + Main.icons = { + up: 'arrow_up.png', + down: 'arrow_down.png', + right: 'arrow_right.png', + download: 'arrow_down2.png', + refresh: 'refresh.png', + cross: 'cross.png', + gis: 'gis.png', + iqdb: 'iqdb.png', + minus: 'post_expand_minus.png', + plus: 'post_expand_plus.png', + rotate: 'post_expand_rotate.gif', + quote: 'quote.png', + report: 'report.png', + notwatched: 'watch_thread_off.png', + watched: 'watch_thread_on.png', + help: 'question.png' + }; + + paths = { + yotsuba_new: 'futaba/', + futaba_new: 'futaba/', + yotsuba_b_new: 'burichan/', + burichan_new: 'burichan/', + tomorrow: 'tomorrow/', + photon: 'photon/' + }; + + url = '//s.4cdn.org/image/'; + + if (window.devicePixelRatio >= 2) { + for (key in Main.icons) { + Main.icons[key] = Main.icons[key].replace('.', '@2x.'); + } + } + + url += 'buttons/' + paths[Main.stylesheet]; + for (key in Main.icons) { + Main.icons[key] = url + Main.icons[key]; + } +}; + +AdminTools.init = function () { + var cnt, html; + + AdminTools.initVisibilityAPI(); + + cnt = document.createElement('div'); + cnt.className = 'extPanel reply'; + cnt.id = 'adminToolbox'; + cnt.setAttribute('data-trackpos', 'AT-position'); + + if (Config['AT-position']) { + cnt.style.cssText = Config['AT-position']; + } else { + cnt.style.right = '10px'; + cnt.style.top = '380px'; + } + + cnt.style.position = Config.fixedAdminToolbox ? 'fixed' : ''; + + html = '
    Moderator Tools' + + 'Refresh
    ' + + '

    Reports: ' + + '? (' + + '?)

    ' + + '

    Ban Requests: ' + + '? (?)

    ' + + '

    Appeals: ' + + '? (?)

    ' + + '

    Messages: ?

    '; + + if (Main.tid) { + html += '

    Same Poster ID

    '; + } + + cnt.innerHTML = html; + document.body.appendChild(cnt); + AdminTools.refreshReportCount(); + + Draggable.set($.id('atHeader')); +}; + +AdminTools.onVisibilityChange = function() { + var self; + + self = AdminTools; + + if (document[AdminTools.hidden]) { + clearInterval(self.autoRefreshTimeout); + self.autoRefreshTimeout = null; + } + else { + self.refreshReportCount(); + self.autoRefreshTimeout = setInterval(self.refreshReportCount, self.autoRefreshDelay); + } +}; + +AdminTools.refreshReportCount = function(force) { + var xhr, cache, msg_count; + + if (force !== true && (cache = localStorage.getItem('4chan-cache-rc'))) { + cache = JSON.parse(cache); + + if (cache.ts > Date.now() - AdminTools.cacheTTL) { + $.id('at-total').textContent = cache.data[0]; + $.id('at-illegal').textContent = cache.data[1]; + $.id('at-banreqs').textContent = cache.data[2]; + $.id('at-appeals').textContent = cache.data[3]; + $.id('at-illegal-br').textContent = cache.data[4] || 0; + $.id('at-prio-appeals').textContent = cache.data[5] || 0; + + $.id('at-msg-cnt').style.display = cache.data[6] ? 'block' : ''; + $.id('at-msg').textContent = cache.data[6] || 0; + + return; + } + } + + xhr = new XMLHttpRequest(); + + xhr.open('GET', 'https://' + J.reportsSubDomain + '.4chan.org/H429f6uIsUqU.php', true); + + xhr.withCredentials = true; + + xhr.onload = function () { + var cache, resp, data; + + if (this.status == 200) { + try { + resp = JSON.parse(this.responseText); + } + catch (e) { + console.log(e); + return; + } + + if (resp.status !== 'success') { + console.log(resp.message); // FIXME, use global message + return; + } + + data = resp.data; + + msg_count = data.msg || 0; + + $.id('at-msg-cnt').style.display = msg_count ? 'block' : ''; + $.id('at-msg').textContent = msg_count; + $.id('at-total').textContent = data.total; + $.id('at-illegal').textContent = data.illegal; + $.id('at-banreqs').textContent = data.banreqs; + $.id('at-illegal-br').textContent = data.illegal_banreqs; + $.id('at-appeals').textContent = data.appeals; + $.id('at-prio-appeals').textContent = data.prio_appeals; + + cache = { + ts: Date.now(), + data: [ + data.total, + data.illegal, + data.banreqs, + data.appeals, + data.illegal_banreqs, + data.prio_appeals, + data.msg + ] + }; + + cache = JSON.stringify(cache); + + localStorage.setItem('4chan-cache-rc', cache); + + document.dispatchEvent(new CustomEvent('4chanATUpdated')); + } + else { + this.onerror(); + } + }; + + xhr.onerror = function () { + console.log('Error while refreshing the report count (Status: ' + this.status + ').'); + }; + + xhr.onloadend = function () { + $.id('atRefresh').src = Main.icons.refresh; + }; + + $.id('atRefresh').src = Main.icons.rotate; + + xhr.send(null); +}; + +AdminTools.resetMsgCount = function() { + var cache; + + $.id('at-msg').textContent = 0; + + if (cache = localStorage.getItem('4chan-cache-rc')) { + cache = JSON.parse(cache); + cache.data[6] = 0; + cache = JSON.stringify(cache); + localStorage.setItem('4chan-cache-rc', cache); + } +}; + +/** + * Click handler + */ +J.onClick = function (e) { + var t, cmd; + + if ((t = e.target) == document) { + return; + } + + if (cmd = t.getAttribute('data-cmd')) { + switch (cmd) { + case 'at-refresh': + AdminTools.refreshReportCount(true); + break; + case 'delete-post': + case 'delete-image': + J.deletePost(t, (cmd === 'delete-image')); + break; + case 'force-archive': + J.forceArchive(t); + break; + + case 'open-delete-prompt': + J.openDeletePrompt(t); + break; + + case 'close-delete-prompt': + J.closeDeletePrompt(); + break; + + case 'at-msg': + AdminTools.resetMsgCount(); + break; + + case 'toggle-file-spoiler': + J.setFileSpoiler(t); + break; + + case 'prompt-spoiler': + if (confirm('Toggle spoiler?')) { + J.setFileSpoiler(t); + } + break; + + case 'thread-options': + if (Config.inlinePopups) { + J.openThreadOptionsFrame(t); + } + else { + J.openThreadOptions(t); + } + break; + + case 'multi': + e.preventDefault(); + Multi.exec(t); + break; + + case 'get-md5': + J.onGetMD5Click(t); + break; + + case 'html-toggle': + J.onHTMLToggle(t); + break; + + case 'preview-html': + e.preventDefault(); + J.onPreviewHTMLClick(t); + break; + + case 'close-html-preview': + J.closeHTMLPreview(); + break; + + case 'poster-id': + J.loadSamePosters(); + break; + + case 'ban': + if (Config.inlinePopups) { + J.openBanFrame(t); + } + else { + J.openBanWindow(t); + } + break; + } + } +}; + +J.onScroll = function () { + var end; + + while (J.nextChunk.offsetTop < (document.documentElement.clientHeight + window.scrollY)) { + end = J.nextChunkIndex + J.chunkSize; + if (end >= J.postCount) { + J.parseRange(J.nextChunkIndex, J.postCount); + window.removeEventListener('scroll', J.onScroll, false); + return false; + } + else { + J.parseRange(J.nextChunkIndex, end); + } + } + + return true; +}; + +J.parseRange = function (start, end) { + var i, j, posts; + + posts = document.getElementById('t' + Main.tid).getElementsByClassName('postInfo'); + + for (i = start; i < end; ++i) { + j = posts[i]; + + if (!j) { + break; + } + + J.parsePost(j); + } + + J.nextChunkIndex = i; + J.nextChunk = posts[i]; +}; + +J.onParsingDone = function(e) { + var i, tid, offset, limit, posts; + + if (e) { + tid = e.detail.threadId; + offset = e.detail.offset; + limit = e.detail.limit; + posts = document.getElementById('t' + tid).getElementsByClassName('postInfo'); + + if (J.sameIDActive) { + J.loadSamePosters(posts[offset].id.slice(2)); + } + } + else { + offset = 0; + posts = document.getElementsByClassName('postInfo'); + limit = posts.length; + } + + if (Config.useIconButtons) { + for (i = offset; i < limit; ++i) { + J.parsePost(posts[i]); + } + } +}; + +J.onPostMenuReady = function(e) { + var elw, el, pid, menu, flag; + + pid = e.detail.postId; + menu = e.detail.node; + + if (window.thread_archived && $.id('f' + pid)) { + elw = document.createElement('li'); + elw.className = 'dd-admin'; + el = document.createElement('a'); + el.href = '#'; + el.setAttribute('data-cmd', 'get-md5'); + el.setAttribute('data-id', pid); + el.textContent = 'File MD5'; + elw.appendChild(el); + menu.appendChild(elw); + } + + if (window.spoilers && (el = $.id('fT' + pid))) { + flag = $.cls('imgspoiler', el.parentNode)[0] ? 0 : 1; + elw = document.createElement('li'); + elw.className = 'dd-admin'; + el = document.createElement('a'); + el.setAttribute('data-cmd', 'toggle-file-spoiler'); + el.setAttribute('data-id', pid); + el.setAttribute('data-flag', flag); + el.textContent = (flag ? 'Set' : 'Unset') + ' Spoiler'; + elw.appendChild(el); + menu.appendChild(elw); + } + + if (Config.useIconButtons && !Main.hasMobileLayout) { + return; + } + + elw = document.createElement('li'); + elw.className = 'dd-admin'; + el = document.createElement('a'); + el.setAttribute('data-cmd', 'open-delete-prompt'); + el.setAttribute('data-id', pid); + el.textContent = 'Delete'; + elw.appendChild(el); + menu.appendChild(elw); + + if (window.thread_archived) { + return; + } + + elw = document.createElement('li'); + elw.className = 'dd-admin'; + el = document.createElement('a'); + el.setAttribute('data-cmd', 'ban'); + el.setAttribute('data-id', pid); + el.textContent = 'Ban'; + elw.appendChild(el); + menu.appendChild(elw); + + elw = document.createElement('li'); + elw.className = 'dd-admin'; + el = document.createElement('a'); + el.setAttribute('data-cmd', 'multi'); + el.setAttribute('data-id', pid); + el.textContent = 'Search'; + elw.appendChild(el); + menu.appendChild(elw); + + if (e.detail.isOP) { + elw = document.createElement('li'); + elw.className = 'dd-admin'; + el = document.createElement('a'); + el.setAttribute('data-cmd', 'thread-options'); + el.setAttribute('data-id', pid); + el.textContent = 'Thread options'; + elw.appendChild(el); + menu.appendChild(elw); + } +}; + +J.parsePost = function(postInfo) { + var pid, html, cnt, tail; + + pid = postInfo.id.slice(2); + + html = 'X'; + + if (window.spoilers && (el = $.id('fT' + pid))) { + html += 'S'; + } + + if (!window.thread_archived) { + html += 'M' + + 'B'; + + if ($.id('t' + pid)) { + html += '>'; + } + } + + cnt = document.createElement('div'); + cnt.className = 'extControls'; + cnt.innerHTML = html; + + tail = postInfo.getElementsByClassName('postMenuBtn')[0]; + + postInfo.insertBefore(cnt, tail); +}; + +J.displayJCount = function(jLink, jLinkBot, no, delta) { + var msg; + + $.addClass(jLink, 'j-newposts'); + $.addClass(jLinkBot, 'j-newposts'); + jLink.setAttribute('data-no', no); + jLinkBot.setAttribute('data-no', no); + jLink.textContent = jLinkBot.textContent = 'j +' + delta; + + msg = delta + ' new post' + (delta > 1 ? 's' : ''); + + Main.addTooltip(jLink, msg, 'j-tooltip'); + Main.addTooltip(jLinkBot, msg, 'j-tooltip-bot'); +}; + +J.refreshJCount = function() { + var stored, jLink, jLinkBot, xhr; + + jLink = $.id('j-link'); + jLinkBot = $.id('j-link-bot'); + + if (!jLink || !jLinkBot) { + return; + } + + jLink = jLink.firstElementChild; + jLinkBot = jLinkBot.firstElementChild; + + if (stored = localStorage.getItem('4chan-j-count')) { + stored = JSON.parse(stored); + } + + if (!stored || (Date.now() - stored.time) >= 10000) { + xhr = new XMLHttpRequest(); + xhr.open('GET', 'https://sys.4chan.org/j/1mcQTXbjW5WO.php?&' + Date.now()); + xhr.withCredentials = true; + xhr.onloadend = function() { + var data, obj, delta; + if (this.status == 200 || this.status == 304) { + data = JSON.parse(this.responseText); + if (!stored || Main.board == 'j') { + obj = { time: Date.now(), no: data.no }; + } + else if (data.no > stored.no) { + delta = data.no - stored.no; + J.displayJCount(jLink, jLinkBot, data.no, delta); + obj = { time: Date.now(), no: stored.no, delta: delta }; + } + if (obj) { + localStorage.setItem('4chan-j-count', JSON.stringify(obj)); + } + } + else { + console.log('Error: Could not load /j/ post count (Status: ' + this.status + ').'); + } + }; + xhr.send(null); + } + else if (stored.delta) { + J.displayJCount(jLink, jLinkBot, stored.no, stored.delta); + } +}; + +J.clearJCount = function() { + var obj, no, tt, ttbot; + + tt = $.id('j-tooltip'); + ttbot = $.id('j-tooltip-bot'); + + if (!tt) { + return; + } + + no = this.getAttribute('data-no'); + obj = { time: Date.now(), no: no }; + localStorage.setItem('4chan-j-count', JSON.stringify(obj)); + + tt.parentNode.removeChild(tt); + ttbot.parentNode.removeChild(ttbot); + + setTimeout(function() { + var nodes = $.cls('j-newposts'); + if (nodes[0]) { + nodes[0].textContent = 'j'; + $.removeClass(nodes[0], 'j-newposts'); + nodes[0].textContent = 'j'; + $.removeClass(nodes[0], 'j-newposts'); + } + }, 10); +}; + +J.hasFlag = function(flag) { + return this.flags.indexOf(flag) != -1; +}; + +J.icons = { + multi: 'multi.png', + ban: 'ban.png', + arrow_right: 'arrow_right.png', + spoiler: 's.png' +}; + +J.initIcons = function () { + var key, paths, url; + + paths = { + yotsuba_new:'futaba/', + futaba_new:'futaba/', + yotsuba_b_new:'burichan/', + burichan_new:'burichan/', + tomorrow:'tomorrow/', + photon:'photon/' + }; + + url = '//s.4cdn.org/image/buttons/' + paths[Main.stylesheet]; + + if (window.devicePixelRatio >= 2) { + for (key in J.icons) { + J.icons[key] = J.icons[key].replace('.', '@2x.'); + } + } + + for (key in J.icons) { + J.icons[key] = url + J.icons[key]; + } +}; + +J.initNavLinks = function () { + var el, txt, frag, fragBot, nav, navbot; + + nav = $.id('navtopright'); + navbot = $.id('navbotright'); + + // [j] link + el = document.createElement('span'); + el.id = 'j-link'; + el.innerHTML = '[j]'; + el.firstElementChild.addEventListener('mouseup', J.clearJCount, false); + nav.parentNode.insertBefore(el, nav); + + // [j] bottom link + el = el.cloneNode(true); + el.id = 'j-link-bot'; + el.firstElementChild.addEventListener('mouseup', J.clearJCount, false); + navbot.parentNode.insertBefore(el, navbot); + + J.refreshJCount(); + + // Team link + txt = nav.lastElementChild.previousSibling; + frag = document.createDocumentFragment(); + frag.appendChild(document.createTextNode('] [')); + el = document.createElement('a'); + el.textContent = 'Team'; + el.href = 'https://' + J.teamSubDomain + '.4chan.org/'; + el.setAttribute('target', '_blank'); + frag.appendChild(el); + frag.appendChild(document.createTextNode('] [')); + fragBot = frag.cloneNode(true); + nav.replaceChild(frag, txt); + + // Team bottom link + txt = navbot.lastElementChild.previousSibling; + navbot.replaceChild(fragBot, txt); + + // Poster IDs + if (Main.tid && Main.hasMobileLayout) { + el = document.createElement('span'); + el.className = 'mobileib button redButton'; + el.innerHTML = ''; + if (nav = $.cls('navLinks')[0]) { + nav.appendChild(el); + } + } +}; + +J.initPostForm = function () { + var form, el, field, cnt, cookie; + + form = $.id('postForm'); + + if (!form && Main.tid && (cnt = $.cls('closed')[0])) { + el = document.createElement('form'); + el.name = 'post'; + el.method = 'POST'; + el.enctype = 'multipart/form-data'; + el.action = 'https://sys.' + $L.d(Main.board) + '/' + Main.board + '/post'; + el.innerHTML = J.postFormHTML + + '' + + ''; + + cnt.parentNode.insertBefore(el, cnt); + + form = el.firstElementChild; + + QR.enabled = true; + } + + if (form) { + el = document.forms.post.name; + + if (el.type == 'hidden' && J.hasFlag('forcedanonname')) { + field = document.createElement('tr'); + field.setAttribute('data-type', 'Name'); + field.innerHTML = 'Name'; + if (cookie = Main.getCookie('4chan_name')) { + field.lastChild.firstChild.value = cookie; + } + cnt = $.id('postForm').firstElementChild; + cnt.insertBefore(field, cnt.firstElementChild); + el.parentNode.removeChild(el); + } + + if (J.hasFlag('html')) { + el = document.createElement('tr'); + el.innerHTML = 'Extra[] [ ] Preview'; + form = form.firstElementChild; + form.insertBefore(el, form.lastElementChild); + } + } +}; + +J.onHTMLToggle = function(t) { + var el = $.cls('html-otp', t.parentNode.parentNode)[0]; + + if (!el) { + return; + } + + el.style.display = t.checked ? 'inline' : ''; +}; + +J.onPreviewHTMLClick = function(t) { + var data, form, xhr; + + if (J.xhrs['html']) { + return; + } + + if (document.forms.qrPost) { + form = document.forms.qrPost; + } + else { + form = document.forms.post; + } + + if (form.com.value === '') { + return; + } + + data = new FormData(); + data.append('com', form.com.value); + + xhr = new XMLHttpRequest(); + xhr.open('post', form.action + '?mode=preview_html', true); + xhr.withCredentials = true; + xhr.onload = J.onHTMLPReviewLoaded; + xhr.onerror = J.onHTMLPReviewError; + + J.xhrs['html'] = xhr; + + $.addClass(t, 'disabled'); + + xhr.send(data); + + Feedback.notify('Processing...', null); +}; + +J.resetHTMLPreviewBtn = function() { + var nodes = $.cls('preview-html-btn'); + $.removeClass(nodes[0], 'disabled'); + nodes[1] && $.removeClass(nodes[1], 'disabled'); +}; + +J.onHTMLPReviewLoaded = function() { + var resp; + + J.xhrs['html'] = null; + + Feedback.hideMessage(); + + J.resetHTMLPreviewBtn(); + + try { + resp = JSON.parse(this.responseText); + + if (resp.status == 'error') { + Feedback.error(resp.message); + } + } + catch (err) { + Feedback.error('Something went wrong.'); + } + + J.buildHTMLPreview(resp.data); +}; + +J.onHTMLPReviewError = function() { + J.xhrs['html'] = null; + + Feedback.hideMessage(); + + J.resetHTMLPreviewBtn(); + + console.log(this); +}; + +J.closeHTMLPreview = function() { + var el; + + if (el = $.id('html-preview-cnt')) { + el.parentNode.removeChild(el); + } + + J.resetHTMLPreviewBtn(); +}; + +J.buildHTMLPreview = function(html) { + var el; + + J.closeHTMLPreview(); + + el = document.createElement('div'); + el.id = 'html-preview-cnt'; + el.setAttribute('data-cmd', 'close-html-preview'); + el.innerHTML = '\ +
    Preview HTML Post\ +Close
    ' + html + '
    '; + + document.body.appendChild(el); +}; + +J.onThreadMouseOver = function(e) { + var t = e.target; + + if ($.hasClass(t, 'thumb')) { + if (J.hasCatalogControls) { + J.hideCatalogControls(); + } + if (!$.hasClass(t.parentNode.parentNode, 'disabled')) { + J.showCatalogControls(t); + } + } +}; +/* +J.onThreadMouseOut = function(e) { + var t = e.target; + + if (J.hasCatalogControls && $.hasClass(t, 'thumb')) { + J.hideCatalogControls(); + } +}; +*/ +J.showCatalogControls = function(t) { + var el, id, cnt; + + id = t.getAttribute('data-id'); + + el = document.createElement('div'); + el.id = 'cat-ctrl'; + el.className = J.stylesheet; + el.innerHTML = ''; + + if (cnt = $.cls('threadIcons', t.parentNode.parentNode)[0]) { + cnt.insertBefore(el, cnt.firstElementChild); + } + else { + cnt = document.createElement('div'); + cnt.className = 'threadIcons'; + cnt.appendChild(el); + t.parentNode.parentNode.insertBefore(cnt, t.parentNode.nextElementSibling); + } + + J.hasCatalogControls = true; +}; + +J.hideCatalogControls = function() { + var el = $.id('cat-ctrl'); + + if (el) { + el.parentNode.removeChild(el); + } + + J.hasCatalogControls = false; +}; + +J.initCatalog = function() { + var storage; + + window.Main = { + board: location.pathname.split(/\//)[1] + }; + + Main.addTooltip = function(link, message, id) { + var el, pos; + + el = document.createElement('div'); + el.className = 'click-me'; + if (id) { + el.id = id; + } + el.innerHTML = message || 'Change your settings'; + link.parentNode.appendChild(el); + + pos = (link.offsetWidth - el.offsetWidth + link.offsetLeft - el.offsetLeft) / 2; + el.style.marginLeft = pos + 'px'; + + return el; + }; + + if (J.stylesheet = J.getCookie(window.style_group)) { + J.stylesheet = J.stylesheet.toLowerCase().replace(/ /g, '_'); + } + else { + J.stylesheet = + style_group == 'nws_style' ? 'yotsuba_new' : 'yotsuba_b_new'; + } + + Main.stylesheet = J.stylesheet; + + J.initIconsCatalog(); + + J.addCss(); // fixme + + document.addEventListener('click', J.onClick, false); + + J.runCatalog(); +}; + +J.runCatalog = function () { + var threads; + //J.addCss(); // fixme + //document.removeEventListener('4chanMainInit', J.runCatalog, false); + + J.initNavLinks(); + + if (!FC.hasMobileLayout) { + AdminTools.init(); + } + + threads = $.id('threads'); + + J.initCatAutoReload(true); + + document.addEventListener('keydown', J.onCatalogKeyDown, false); + + $.on(threads, 'mouseover', J.onThreadMouseOver); + + if (window.text_only) { + document.addEventListener('4chanPostMenuReady', J.onPostMenuReady, false); + } + //$.on(threads, 'mouseout', J.onThreadMouseOut); +}; + +J.init = function () { + var flags, ts; + + SettingsMenu.options['Moderator'] = { + useIconButtons: [ 'Use icon buttons', 'Display old-style buttons instead of using drop-down' ], + changeUpdateDelay:[ 'Reduce auto-update delay', 'Reduce the thread updater delay', true ], + fixedAdminToolbox:[ 'Pin Moderator Tools to the page', 'Moderator Tools will scroll with you' ], + inlinePopups: [ 'Inline admin panels', 'Open admin panels in browser window, instead of a popup' ], + disableMngExt:[ 'Disable moderator extension', 'Completely disable the moderator extension (overrides any checked boxes)', true ] + }; + + if (Config.disableMngExt) { + return; + } + + if (flags = Main.getCookie('4chan_aflags')) { + J.flags = flags.split(','); + } + + J.addCss(); + + if (Config.useIconButtons) { + J.initIcons(); + } + + QR.noCooldown = QR.noCaptcha = true; + //QR.comLength = window.comlen = 10000; + + document.addEventListener('click', J.onClick, false); + document.addEventListener('DOMContentLoaded', J.run, false); +}; + +J.run = function() { + var posts, el, nav; + + document.removeEventListener('DOMContentLoaded', J.run, false); + + J.initNavLinks(); + J.initPostForm(); + + if (!Main.hasMobileLayout) { + AdminTools.init(); + } + + if (Config.revealSpoilers) { + $.addClass(document.body, 'reveal-img-spoilers'); + } + + if (Config.threadUpdater && Main.tid) { + if (Config.changeUpdateDelay) { + ThreadUpdater.delayIdHidden = 3; + ThreadUpdater.delayRange = [ 5, 10, 15, 20, 30, 60 ]; + ThreadUpdater.apiUrlFilter = J.apiUrlFilter; + } + } + + if (Config.useIconButtons && !Main.hasMobileLayout) { + if (Main.tid) { + posts = document.getElementById('t' + Main.tid).getElementsByClassName('postInfo'); + J.postCount = posts.length; + if (J.postCount > J.chunkSize) { + J.nextChunk = posts[0]; + window.addEventListener('scroll', J.onScroll, false); + J.onScroll(); + } + else { + J.onParsingDone(); + } + + nav = $.cls('navLinksBot')[0]; + el = document.createElement('span'); + el.id = 'threadOptsButtom'; + el.innerHTML = '[Thread Options]'; + nav.appendChild(el); + } + else { + J.onParsingDone(); + } + + document.addEventListener('4chanParsingDone', J.onParsingDone, false); + + J.parserEventBound = true; + } + + document.addEventListener('4chanPostMenuReady', J.onPostMenuReady, false); + + if (nav = $.id('boardSelectMobile')) { + el = document.createElement('option'); + el.value = 'j'; + el.textContent = '/j/ - Janitors & Moderators'; + nav.insertBefore(el, nav.firstElementChild); + } +}; + +J.getCookie = function(name) { + var i, c, ca, key; + + key = name + "="; + ca = document.cookie.split(';'); + + for (i = 0; c = ca[i]; ++i) { + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(key) === 0) { + return decodeURIComponent(c.substring(key.length, c.length)); + } + } + return null; +}; + +J.postFormHTML = '\ +\ +\ +
    Name\ +
    Options
    Comment
    File\ +\ +[]
    '; + +J.addCss = function () { + var style, css; + + css = '\ +#adminToolbox {\ + max-width: 256px;\ + display: block;\ + position: absolute;\ + padding: 3px;\ +}\ +#adminToolbox h4 {\ + font-size: 12px;\ + margin: 2px 0 0;\ + padding: 0;\ + font-weight: normal;\ +}\ +#adminToolbox li {\ + list-style: none;\ +}\ +#adminToolbox ul {\ + padding: 0;\ + margin: 2px 0 0 10px;\ +}\ +#atHeader {\ + height: 17px;\ + font-weight: bold;\ + padding-bottom: 2px;\ +}\ +#atRefresh {\ + margin: -1px 0 0 3px;\ +}\ +.post-ip {\ + margin-left: 5px;\ +}\ +#delete-prompt > div {\ + text-align: center;\ +}\ +#watchList li:first-child {\ + margin-top: 3px;\ + padding-top: 2px;\ + border-top: 1px solid rgba(0, 0, 0, 0.20);\ +}\ +.photon #atHeader {\ + border-bottom: 1px solid #ccc;\ +}\ +.yotsuba_new #atHeader {\ + border-bottom: 1px solid #d9bfb7;\ +}\ +.yotsuba_b_new #atHeader {\ + border-bottom: 1px solid #b7c5d9;\ +}\ +.tomorrow #atHeader {\ + border-bottom: 1px solid #111;\ +}\ +#captchaFormPart {\ + display: none;\ +}\ +#at-prio-appeals {\ + color: blue;\ +}\ +#at-illegal-br,\ +#at-illegal {\ + color: red;\ +}\ +#at-msg-cnt {\ + display: none;\ +}\ +.j-newposts {\ + font-weight: bold !important;\ +}\ +#j-link,\ +#j-link-bot {\ + margin-right: 3px;\ + display: inline-block;\ + margin-left: 3px;\ +}\ +#adminToolbox hr {\ + margin: 2px 0;\ +}\ +#threadOptsClose,\ +#banReqClose {\ + float: right;\ +}\ +#threadOpts iframe,\ +#banReq iframe {\ + overflow: hidden;\ +}\ +#threadOpts,\ +#banReq {\ + display: block;\ + position: fixed;\ + padding: 2px;\ + font-size: 10pt;\ +}\ +#banReq {\ + height: 490px;\ +}\ +#threadOpts {\ + height: 194px;\ +}\ +#threadOptsHeader,\ +#banReqHeader {\ + text-align: center;\ + margin-bottom: 1px;\ + padding: 0;\ + height: 18px;\ + line-height: 18px;\ +}\ +#threadOptsButtom {\ + float: right;\ + margin-right: 4px;\ +}\ +.mobileExtControls {\ + float: right;\ + font-size: 11px;\ + margin-bottom: 3px;\ +}\ +.ws .mobileExtControls {\ + color: #34345C;\ +}\ +.nws .mobileExtControls {\ + color: #0000EE;\ +}\ +.reply .mobileExtControls {\ + margin-right: 5px;\ +}\ +.mobileExtControls span {\ + margin-left: 10px;\ +}\ +.mobileExtControls span:after {\ + content: "]";\ +}\ +.mobileExtControls span:before {\ + content: "[";\ +}\ +.nws .mobileExtControls span:after {\ + color: #800000;\ +}\ +.nws .mobileExtControls span:before {\ + color: #800000;\ +}\ +.ws .mobileExtControls span:after {\ + color: #000;\ +}\ +.ws .mobileExtControls span:before {\ + color: #000;\ +}\ +.m-dark .mobileExtControls {\ + color: #81a2be !important;\ +}\ +.m-dark .mobileExtControls span:after,\ +.m-dark .mobileExtControls span:before {\ + color: #1d1f21 !important;\ +}\ +#cat-ctrl {\ + display: inline-block;\ + margin-right: 2px;\ + margin-top: -1px;\ +}\ +#cat-ctrl .threadIcon {\ + cursor: pointer;\ +}\ +.disabled {\ + opacity: 0.5;\ +}\ +.burichan_new .deleteIcon,\ +.yotsuba_b_new .deleteIcon {\ + background-image: url("//s.4cdn.org/image/buttons/burichan/cross.png");\ +}\ +.burichan_new .banIcon,\ +.yotsuba_b_new .banIcon {\ + background-image: url("//s.4cdn.org/image/buttons/burichan/ban.png");\ +}\ +.burichan_new .fileIcon,\ +.yotsuba_b_new .fileIcon {\ + background-image: url("//s.4cdn.org/image/buttons/burichan/f.png");\ +}\ +.burichan_new .multiIcon,\ +.yotsuba_b_new .multiIcon {\ + background-image: url("//s.4cdn.org/image/buttons/burichan/multi.png");\ +}\ +.futaba_new .deleteIcon,\ +.yotsuba_new .deleteIcon {\ + background-image: url("//s.4cdn.org/image/buttons/futaba/cross.png");\ +}\ +.futaba_new .banIcon,\ +.yotsuba_new .banIcon {\ + background-image: url("//s.4cdn.org/image/buttons/futaba/ban.png");\ +}\ +.futaba_new .fileIcon,\ +.yotsuba_new .fileIcon {\ + background-image: url("//s.4cdn.org/image/buttons/futaba/f.png");\ +}\ +.futaba_new .multiIcon,\ +.yotsuba_new .multiIcon {\ + background-image: url("//s.4cdn.org/image/buttons/futaba/multi.png");\ +}\ +.photon .deleteIcon {\ + background-image: url("//s.4cdn.org/image/buttons/photon/cross.png");\ +}\ +.photon .banIcon {\ + background-image: url("//s.4cdn.org/image/buttons/photon/ban.png");\ +}\ +.photon .fileIcon {\ + background-image: url("//s.4cdn.org/image/buttons/photon/f.png");\ +}\ +.photon .multiIcon {\ + background-image: url("//s.4cdn.org/image/buttons/photon/multi.png");\ +}\ +.tomorrow .deleteIcon {\ + background-image: url("//s.4cdn.org/image/buttons/tomorrow/cross.png");\ +}\ +.tomorrow .banIcon {\ + background-image: url("//s.4cdn.org/image/buttons/tomorrow/ban.png");\ +}\ +.tomorrow .fileIcon {\ + background-image: url("//s.4cdn.org/image/buttons/tomorrow/f.png");\ +}\ +.tomorrow .multiIcon {\ + background-image: url("//s.4cdn.org/image/buttons/tomorrow/multi.png");\ +}\ +.dd-admin {\ + text-indent: 5px;\ +}\ +.dd-admin:before {\ + color: #FF0000;\ + content: "•";\ + left: -3px;\ + position: absolute;\ +}\ +.extPanel {\ + border: 1px solid rgba(0, 0, 0, 0.2);\ +}\ +.extPanel img.pointer { width: 18px; height: 18px }\ +.preview-html-btn { font-size: 11px; }\ +#html-preview-cnt .extPanel { width: 800px; margin-left: -400px; }\ +#html-preview-cnt {\ + position: fixed;\ + width: 100%;\ + height: 100%;\ + z-index: 9002;\ + top: 0;\ + left: 0;\ +}\ +#html-preview-cnt {\ + line-height: 14px;\ + font-size: 14px;\ + background-color: rgba(0, 0, 0, 0.25);\ +}\ +#html-preview-cnt:after {\ + display: inline-block;\ + height: 100%;\ + vertical-align: middle;\ + content: "";\ +}\ +#html-preview-cnt > div {\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + display: inline-block;\ + height: auto;\ + max-height: 100%;\ + position: relative;\ + width: 400px;\ + left: 50%;\ + margin-left: -200px;\ + overflow: auto;\ + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\ + vertical-align: middle;\ +}\ +.reveal-img-spoilers .imgspoiler::before {\ + content: " ";\ + width:0.75em;\ + height:0.75em;\ + border-radius: 0.5em;\ + position: absolute;\ + display: block;\ + background: red;\ + margin-top: 1px;\ + margin-left: 1px;\ + pointer-events: none;\ +}\ +.reveal-img-spoilers.is_catalog .imgspoiler::before { margin-top: 4px; margin-left: 12px;}\ +.reveal-img-spoilers .imgspoiler:hover::before { background: #fff; }\ +.html-otp { display: none; width: 50px; }\ +.html-otp input { width: 50px !important; height: 12px; }\ +.drag {\ + -moz-user-select: none !important;\ + cursor: move !important;\ +}' + (J.isCatalog ? '\ +.panelHeader .panelCtrl {\ + position: absolute;\ + right: 5px;\ + top: 5px;\ +}\ +.active-btn { border-bottom: 3px double; }\ +.UIPanel {\ + position: fixed;\ + width: 100%;\ + height: 100%;\ + top: 0;\ + left: 0;\ + z-index: 9000 !important;\ +}\ +.UIPanel {\ + line-height: 14px;\ + font-size: 14px;\ + background-color: rgba(0, 0, 0, 0.25);\ +}\ +.UIPanel:after {\ + display: inline-block;\ + height: 100%;\ + vertical-align: middle;\ + content: "";\ +}\ +.UIPanel > div {\ + -moz-box-sizing: border-box;\ + box-sizing: border-box;\ + display: inline-block;\ + height: auto;\ + max-height: 100%;\ + position: relative;\ + width: 400px;\ + left: 50%;\ + margin-left: -200px;\ + overflow: auto;\ + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\ + vertical-align: middle;\ +}\ +.UIPanel .extPanel {\ + padding: 2px;\ +}' : ''); + + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.textContent = css; + document.head.appendChild(style); +}; + +if (/https?:\/\/boards\.(?:4chan|4channel)\.org\/[a-z0-9]+\/catalog($|#.*$)/.test(location.href)) { + J.isCatalog = true; + J.initCatalog(); +} +else { + J.init(); + //J.run(); +} + +})(); diff --git a/js/mod.js b/js/mod.js new file mode 100644 index 0000000..01a7742 --- /dev/null +++ b/js/mod.js @@ -0,0 +1,2 @@ +!function(){var e={isCatalog:!1,colours:{},posterids:{},nextChunkIndex:0,nextChunk:null,chunkSize:100,sameIDActive:!1,parserEventBound:!1,autoReloadCatInterval:null,autoReloadCatDelay:3e4,samePostersMap:{},xhrs:{},reportsSubDomain:"reports",teamSubDomain:"team",flags:[]};e.bin2hex=function(e){var t,n,a,o;for(a="",n=e.length,t=0;t>4).toString(16),a+=(15&o).toString(16);return a},e.getFileMD5FromPid=function(t){var n,a;return!!(n=$.id("f"+t))&&(!!(n=$.qs("img[data-md5]",n))&&(a=window.atob(n.getAttribute("data-md5")),e.bin2hex(a)))},e.onGetMD5Click=function(t){var n,a=t.getAttribute("data-id");!1===(n=e.getFileMD5FromPid(a))?alert("Post or file not found"):prompt("",n)},e.apiUrlFilter=function(e){return e+"?"+Math.round(Date.now()/1e3/3)},e.openDeletePrompt=function(t){var n,a;n='
    Delete Post No.'+(t=t.getAttribute("data-id"))+'Close
    ',$.id((e.isCatalog?"thread-":"t")+t)&&!window.thread_archived&&(n+=' '),window.thread_archived||e.isCatalog||(n+='
    []'),n+="
    ",(a=document.createElement("div")).className="UIPanel",a.id="delete-prompt",a.innerHTML=n,document.addEventListener("keydown",e.onKeyDown,!1),a.addEventListener("click",e.closeDeletePrompt,!1),document.body.appendChild(a),$.id("delete-prompt-inner").firstElementChild.focus()},e.addPosterIds=function(e,t,n){var a,o,i,r,s,d;if(a=n?$.id("pim"+e):$.id("pi"+e),!window.user_ids||!(i=$.cls("posteruid",a)[0])){if(i=$.el("span"),o=$.cls("nameBlock",a)[0],(r=$.cls("name",o)[0]).classList.contains("capcode"))return;o.insertBefore(i,r.nextSibling),n||o.insertBefore(document.createTextNode(" "),r.nextSibling)}i.innerHTML='(ID: '+t+")",i.className="posteruid id_"+t,s=i.firstElementChild,IDColor.apply(s),i.addEventListener("click",window.idClick,!1),window.currentHighlighted&&-1!=i.className.indexOf("id_"+window.currentHighlighted)&&((d=i.parentNode.parentNode.parentNode).className="highlight "+d.className)},e.onSamePostersLoaded=function(){var t,n,a,o,i;if((200==this.status||304==this.status)&&(t=JSON.parse(this.responseText))){i=Main.hasMobileLayout,IDColor.enabled||(o=window.user_ids,window.user_ids=!0,IDColor.init(),window.user_ids=o),e.sameIDActive||(e.sameIDActive=!0);for(a in t)e.samePostersMap[a]||(n=t[a],e.samePostersMap[a]=!0,e.addPosterIds(a,n,i))}},e.loadSamePosters=function(t){var n,a;e.parserEventBound||document.addEventListener("4chanParsingDone",e.onParsingDone,!1),n="https://sys."+$L.d(Main.board)+"/"+Main.board+"/admin?admin=adminext&thread="+Main.tid,t&&(n+="&from="+t),(a=new XMLHttpRequest).open("GET",n),a.withCredentials=!0,a.onload=e.onSamePostersLoaded,a.send(null)},e.closeDeletePrompt=function(t){var n;t&&"delete-prompt"!=t.target.id||(n=$.id("delete-prompt"))&&(document.removeEventListener("keydown",e.onKeyDown,!1),n.removeEventListener("click",e.closeDeletePrompt,!1),document.body.removeChild(n))},e.checkDeletedPosts=function(){var e,t;Main.tid&&(e="//a.4cdn.org/"+Main.board+"/res/"+Main.tid+".json",(t=new XMLHttpRequest).open("GET",e),t.onload=function(){200!=this.status&&304!=this.status||ThreadUpdater.markDeletedReplies(Parser.parseThreadJSON(this.responseText))},t.send(null))},e.get_random_light_color=function(){for(var e="ABCDE".split(""),t="#",n=0;n<3;n++)t+=e[Math.floor(Math.random()*e.length)];return t},e.deletePost=function(t,n){var a,o,i,r,s,d,l,p,c,m;a=t.getAttribute("data-id"),c=!e.isCatalog&&$.id("t"+a),i=new FormData,s="https://sys."+$L.d(Main.board)+"/"+Main.board,window.thread_archived?d="arcdel":(d="usrdel",l=!e.isCatalog&&$.id("delete-all-by-ip").checked),l&&(d="admin.php",i.append("admin","delall"),i.append("id",a)),s+=l?"/admin":"/post",n&&("Delete Image No.",i.append("onlyimgdel","on")),i.append(a,"delete"),i.append("mode",d),i.append("pwd","janitorise"),(p=$.id("delete-prompt-inner")).textContent="Deleting...",(o=new XMLHttpRequest).open("POST",s),o.withCredentials=!0,o.onload=function(){var o;if(t.src=Main.icons.cross,200==this.status)if(!l&&-1!=this.responseText.indexOf("Updating")||l&&-1!=this.responseText.indexOf("deleted")){if(e.isCatalog)(r=$.id("thread-"+a))&&$.addClass(r,"disabled");else if(n)(r=$.id("f"+a)).innerHTML='File deleted.',l&&((o=document.createElement("span")).innerHTML='

    (YOU HAVE DELETED ALL IMAGES BY THIS IP)',(r=$.id("m"+a)).appendChild(o));else{if(a==Main.tid)return void(location.href="//boards."+$L.d(Main.board)+"/"+Main.board+"/");l?((o=document.createElement("span")).innerHTML='

    (YOU HAVE DELETED ALL POSTS BY THIS IP)',(r=$.id("m"+a)).appendChild(o),e.checkDeletedPosts()):c?((r=c.parentNode).removeChild(c.nextSibling),r.removeChild(c)):(r=$.id("pc"+a)).parentNode.removeChild(r)}e.closeDeletePrompt()}else(m=this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/))?p.textContent=m[1]:p.textContent="Error: Something went wrong.";else p.textContent="Error: Wrong status while deleting No."+a+" (Status: "+this.status+")."},o.onerror=function(){p.textContent="Error: Error while deleting No."+a+" (Status: "+this.status+")."},o.send(i)},e.forceArchive=function(t){var n,a,o,i,r,s;n=t.getAttribute("data-id"),o=new FormData,i="https://sys."+$L.d(Main.board)+"/"+Main.board+"/post",o.append("id",n),o.append("mode","forcearchive"),(r=$.id("delete-prompt-inner")).textContent="Archiving...",(a=new XMLHttpRequest).open("POST",i),a.withCredentials=!0,a.onload=function(){var a;t.src&&(t.src=Main.icons.cross),200==this.status?-1!=this.responseText.indexOf("Updating")?(e.isCatalog&&(a=$.id("thread-"+n))&&$.addClass(a,"disabled"),e.closeDeletePrompt()):(s=this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/))?r.textContent=s[1]:r.textContent="Error: Something went wrong.":r.textContent="Error: Wrong status while archiving No."+n+" (Status: "+this.status+")."},a.onerror=function(){r.textContent="Error: Error while archiving No."+n+" (Status: "+this.status+")."},a.send(o)},e.openBanWindow=function(e){var t;t=e.getAttribute("data-id"),window.open("https://sys."+$L.d(Main.board)+"/"+Main.board+"/admin?mode=admin&admin=ban&id="+t,"_blank","scrollBars=yes,resizable=no,toolbar=no,menubar=no,location=no,directories=no,width=400,height=470")},e.openBanFrame=function(t){var n;this.banReqCnt&&this.closeBanFrame(),n=t.getAttribute("data-id"),this.banReqCnt=document.createElement("div"),this.banReqCnt.id="banReq",this.banReqCnt.className="extPanel reply",this.banReqCnt.setAttribute("data-trackpos","banReq-position"),Config["banReq-position"]?this.banReqCnt.style.cssText=Config["banReq-position"]:(this.banReqCnt.style.right="0px",this.banReqCnt.style.top="50px"),this.banReqCnt.innerHTML='
    Ban No.'+n+'X
    ',document.body.appendChild(this.banReqCnt),window.addEventListener("message",e.onMessage,!1),document.addEventListener("keydown",e.onKeyDown,!1),$.id("banReqClose").addEventListener("click",e.closeBanFrame,!1),Draggable.set($.id("banReqHeader"))},e.closeBanFrame=function(){window.removeEventListener("message",e.onMessage,!1),document.removeEventListener("keydown",e.onKeyDown,!1),Draggable.unset($.id("banReqHeader")),$.id("banReqClose").removeEventListener("click",e.closeBanFrame,!1),document.body.removeChild(e.banReqCnt),e.banReqCnt=null},e.processMessage=function(e){return e?{cmd:(e=e.split("-"))[0],type:e[1],id:e.slice(2).join("-")}:{}},e.onKeyDown=function(t){27!=t.keyCode||t.ctrlKey||t.altKey||t.shiftKey||t.metaKey||(e.banReqCnt&&e.closeBanFrame(),e.threadOptsCnt&&e.closeThreadOptionsFrame(),$.id("delete-prompt")&&e.closeDeletePrompt())},e.onCatalogKeyDown=function(t){82==t.keyCode&&t.shiftKey&&e.initCatAutoReload()},e.onMessage=function(t){var n;t.origin==="https://sys."+$L.d(Main.board)&&"ban"===(n=e.processMessage(t.data)).type&&("done"!==n.cmd&&"cancel"!==n.cmd||e.closeBanFrame())},e.initCatAutoReload=function(t){var n;if(n=sessionStorage.getItem("4chan-c-ar"))t?(window.scrollTo(0,+n),e.toggleCatAutoReload(!0)):e.toggleCatAutoReload(!1);else{if(t)return;e.toggleCatAutoReload(!0)}},e.toggleCatAutoReload=function(t){t?(sessionStorage.setItem("4chan-c-ar",document.documentElement.scrollTop),e.autoReloadCatInterval=setInterval(e.autoRefreshWindow,e.autoReloadCatDelay),$.addClass($.id("refresh-btn"),"active-btn")):(sessionStorage.removeItem("4chan-c-ar"),clearInterval(e.autoReloadCatInterval),$.removeClass($.id("refresh-btn"),"active-btn"))},e.autoRefreshWindow=function(){var e=$.id("ctrl");document.documentElement.scrollTop<=e.offsetTop+e.offsetHeight&&(sessionStorage.setItem("4chan-c-ar",document.documentElement.scrollTop),location.href=location.href)},e.openThreadOptions=function(e){var t=e.getAttribute("data-id");window.open("https://sys."+$L.d(Main.board)+"/"+Main.board+"/admin?mode=admin&admin=opt&id="+t,"_blank","scrollBars=yes,resizable=no,toolbar=no,menubar=no,location=no,directories=no,width=400,height=290")},e.openThreadOptionsFrame=function(t){var n;this.threadOptsCnt&&this.closeThreadOptionsFrame(),n=t.getAttribute("data-id"),this.threadOptsCnt=document.createElement("div"),this.threadOptsCnt.id="threadOpts",this.threadOptsCnt.className="extPanel reply",this.threadOptsCnt.setAttribute("data-trackpos","threadOpts-position"),Config["threadOpts-position"]?this.threadOptsCnt.style.cssText=Config["threadOpts-position"]:(this.threadOptsCnt.style.right="0px",this.threadOptsCnt.style.top="50px"),this.threadOptsCnt.innerHTML='
    Thread Options No.'+n+'X
    ',document.body.appendChild(this.threadOptsCnt),window.addEventListener("message",e.onThreadOptsDone,!1),document.addEventListener("keydown",e.onKeyDown,!1),$.id("threadOptsClose").addEventListener("click",e.closeThreadOptionsFrame,!1),Draggable.set($.id("threadOptsHeader"))},e.closeThreadOptionsFrame=function(){window.removeEventListener("message",e.onThreadOptsDone,!1),document.removeEventListener("keydown",e.onKeyDown,!1),Draggable.unset($.id("threadOptsHeader")),$.id("threadOptsClose").removeEventListener("click",e.closeThreadOptionsFrame,!1),document.body.removeChild(e.threadOptsCnt),e.threadOptsCnt=null},e.onThreadOptsDone=function(t){e.threadOptsCnt&&t.origin==="https://sys."+$L.d(Main.board)&&"done-threadopt"===t.data&&e.closeThreadOptionsFrame()},e.setFileSpoiler=function(t){var n,a,o,i;a=t.getAttribute("data-id"),o=t.getAttribute("data-flag"),a&&(i=$.id("f"+a),o||(o=$.cls("imgspoiler",i.parentNode)[0]?0:1),i&&!i.hasAttribute("data-processing")&&((n=new XMLHttpRequest).open("GET","https://sys."+$L.d(Main.board)+"/"+Main.board+"/admin.php?admin=spoiler&pid="+a+"&flag="+o,!0),n.withCredentials=!0,n.onload=e.onFileSpoilerLoad,n.onerror=e.onFileSpoilerError,n._pid=+a,n._flag=+o,Feedback.notify("Processing...",null),i.setAttribute("data-processing","1"),n.send(null)))},e.onFileSpoilerLoad=function(){var e,t;Feedback.hideMessage(),"1"===this.responseText?(e=$.id("f"+this._pid))&&(e.removeAttribute("data-processing"),(e=$.cls("fileThumb",e)[0])&&(this._flag?($.addClass(e,"imgspoiler"),(t=e.previousElementSibling).setAttribute("title",t.firstElementChild.textContent),Config.revealSpoilers||((e=$.tag("img",e)[0]).style.width=e.style.height="100px",e.src="//s.4cdn.org/image/spoiler-"+Main.board+".png")):(Config.revealSpoilers||Parser.revealImageSpoiler(e),$.removeClass(e,"imgspoiler")))):"-1"===this.responseText?Feedback.error("You are not logged in"):Feedback.error("Couldn't set spoiler flag for post No."+this._pid)},e.onFileSpoilerError=function(){var e;(e=$.id("f"+this._pid))&&(e.removeAttribute("data-processing"),Feedback.error("Couldn't update the spoiler flag for post No."+this.pid))};var t={};t.exec=function(t){var n,a;UA.isOpera&&"string"==typeof(a=document.getSelection())||(a=window.getSelection().toString()),a?window.open("https://"+e.teamSubDomain+'.4chan.org/search#{"comment":"'+a.replace(/[\r\n]+/g," ")+'"}'):(n=t.getAttribute("data-id"),window.open("https://team.4chan.org/search?action=from_pid&board="+Main.board+"&pid="+n))},t.prompt=function(t,n){var a,o,i;a=$.id("pi"+n),o=$.cls("postMenuBtn",a)[0],(i=document.createElement("a")).href="https://"+e.teamSubDomain+'.4chan.org/search#{"ip":"'+t+'"}',i.setAttribute("target","_blank"),i.className="post-ip",i.textContent=t,a.insertBefore(i,o)};var n={cacheTTL:6e4,autoRefreshDelay:12e4,autoRefreshTimeout:null};n.initVisibilityAPI=function(){this.hidden="hidden",this.visibilitychange="visibilitychange","undefined"==typeof document.hidden&&("mozHidden"in document?(this.hidden="mozHidden",this.visibilitychange="mozvisibilitychange"):"webkitHidden"in document?(this.hidden="webkitHidden",this.visibilitychange="webkitvisibilitychange"):"msHidden"in document&&(this.hidden="msHidden",this.visibilitychange="msvisibilitychange")),document.addEventListener(this.visibilitychange,this.onVisibilityChange,!1)},e.initIconsCatalog=function(){var e,t,n;if(Main.icons={up:"arrow_up.png",down:"arrow_down.png",right:"arrow_right.png",download:"arrow_down2.png",refresh:"refresh.png",cross:"cross.png",gis:"gis.png",iqdb:"iqdb.png",minus:"post_expand_minus.png",plus:"post_expand_plus.png",rotate:"post_expand_rotate.gif",quote:"quote.png",report:"report.png",notwatched:"watch_thread_off.png",watched:"watch_thread_on.png",help:"question.png"},t={yotsuba_new:"futaba/",futaba_new:"futaba/",yotsuba_b_new:"burichan/",burichan_new:"burichan/",tomorrow:"tomorrow/",photon:"photon/"},n="//s.4cdn.org/image/",window.devicePixelRatio>=2)for(e in Main.icons)Main.icons[e]=Main.icons[e].replace(".","@2x.");n+="buttons/"+t[Main.stylesheet];for(e in Main.icons)Main.icons[e]=n+Main.icons[e]},n.init=function(){var t,a;n.initVisibilityAPI(),(t=document.createElement("div")).className="extPanel reply",t.id="adminToolbox",t.setAttribute("data-trackpos","AT-position"),Config["AT-position"]?t.style.cssText=Config["AT-position"]:(t.style.right="10px",t.style.top="380px"),t.style.position=Config.fixedAdminToolbox?"fixed":"",a='
    Moderator ToolsRefresh

    Reports: ? (?)

    Ban Requests: ? (?)

    Appeals: ? (?)

    Messages: ?

    ',Main.tid&&(a+='

    Same Poster ID

    '),t.innerHTML=a,document.body.appendChild(t),n.refreshReportCount(),Draggable.set($.id("atHeader"))},n.onVisibilityChange=function(){var e;e=n,document[n.hidden]?(clearInterval(e.autoRefreshTimeout),e.autoRefreshTimeout=null):(e.refreshReportCount(),e.autoRefreshTimeout=setInterval(e.refreshReportCount,e.autoRefreshDelay))},n.refreshReportCount=function(t){var a,o,i;if(!0!==t&&(o=localStorage.getItem("4chan-cache-rc"))&&(o=JSON.parse(o)).ts>Date.now()-n.cacheTTL)return $.id("at-total").textContent=o.data[0],$.id("at-illegal").textContent=o.data[1],$.id("at-banreqs").textContent=o.data[2],$.id("at-appeals").textContent=o.data[3],$.id("at-illegal-br").textContent=o.data[4]||0,$.id("at-prio-appeals").textContent=o.data[5]||0,$.id("at-msg-cnt").style.display=o.data[6]?"block":"",void($.id("at-msg").textContent=o.data[6]||0);(a=new XMLHttpRequest).open("GET","https://"+e.reportsSubDomain+".4chan.org/H429f6uIsUqU.php",!0),a.withCredentials=!0,a.onload=function(){var e,t,n;if(200==this.status){try{t=JSON.parse(this.responseText)}catch(a){return void console.log(a)}if("success"!==t.status)return void console.log(t.message);n=t.data,i=n.msg||0,$.id("at-msg-cnt").style.display=i?"block":"",$.id("at-msg").textContent=i,$.id("at-total").textContent=n.total,$.id("at-illegal").textContent=n.illegal,$.id("at-banreqs").textContent=n.banreqs,$.id("at-illegal-br").textContent=n.illegal_banreqs,$.id("at-appeals").textContent=n.appeals,$.id("at-prio-appeals").textContent=n.prio_appeals,e={ts:Date.now(),data:[n.total,n.illegal,n.banreqs,n.appeals,n.illegal_banreqs,n.prio_appeals,n.msg]},e=JSON.stringify(e),localStorage.setItem("4chan-cache-rc",e),document.dispatchEvent(new CustomEvent("4chanATUpdated"))}else this.onerror()},a.onerror=function(){console.log("Error while refreshing the report count (Status: "+this.status+").")},a.onloadend=function(){$.id("atRefresh").src=Main.icons.refresh},$.id("atRefresh").src=Main.icons.rotate,a.send(null)},n.resetMsgCount=function(){var e;$.id("at-msg").textContent=0,(e=localStorage.getItem("4chan-cache-rc"))&&((e=JSON.parse(e)).data[6]=0,e=JSON.stringify(e),localStorage.setItem("4chan-cache-rc",e))},e.onClick=function(a){var o,i;if((o=a.target)!=document&&(i=o.getAttribute("data-cmd")))switch(i){case"at-refresh":n.refreshReportCount(!0);break;case"delete-post":case"delete-image":e.deletePost(o,"delete-image"===i);break;case"force-archive":e.forceArchive(o);break;case"open-delete-prompt":e.openDeletePrompt(o);break;case"close-delete-prompt":e.closeDeletePrompt();break;case"at-msg":n.resetMsgCount();break;case"toggle-file-spoiler":e.setFileSpoiler(o);break;case"prompt-spoiler":confirm("Toggle spoiler?")&&e.setFileSpoiler(o);break;case"thread-options":Config.inlinePopups?e.openThreadOptionsFrame(o):e.openThreadOptions(o);break;case"multi":a.preventDefault(),t.exec(o);break;case"get-md5":e.onGetMD5Click(o);break;case"html-toggle":e.onHTMLToggle(o);break;case"preview-html":a.preventDefault(),e.onPreviewHTMLClick(o);break;case"close-html-preview":e.closeHTMLPreview();break;case"poster-id":e.loadSamePosters();break;case"ban":Config.inlinePopups?e.openBanFrame(o):e.openBanWindow(o)}},e.onScroll=function(){for(var t;e.nextChunk.offsetTop=e.postCount)return e.parseRange(e.nextChunkIndex,e.postCount),window.removeEventListener("scroll",e.onScroll,!1),!1;e.parseRange(e.nextChunkIndex,t)}return!0},e.parseRange=function(t,n){var a,o,i;for(i=document.getElementById("t"+Main.tid).getElementsByClassName("postInfo"),a=t;a',window.spoilers&&(el=$.id("fT"+n))&&(a+='S'),window.thread_archived||(a+='MB',$.id("t"+n)&&(a+='>')),(o=document.createElement("div")).className="extControls",o.innerHTML=a,i=t.getElementsByClassName("postMenuBtn")[0],t.insertBefore(o,i)},e.displayJCount=function(e,t,n,a){var o;$.addClass(e,"j-newposts"),$.addClass(t,"j-newposts"),e.setAttribute("data-no",n),t.setAttribute("data-no",n),e.textContent=t.textContent="j +"+a,o=a+" new post"+(a>1?"s":""),Main.addTooltip(e,o,"j-tooltip"),Main.addTooltip(t,o,"j-tooltip-bot")},e.refreshJCount=function(){var t,n,a,o;n=$.id("j-link"),a=$.id("j-link-bot"),n&&a&&(n=n.firstElementChild,a=a.firstElementChild,(t=localStorage.getItem("4chan-j-count"))&&(t=JSON.parse(t)),!t||Date.now()-t.time>=1e4?((o=new XMLHttpRequest).open("GET","https://sys.4chan.org/j/1mcQTXbjW5WO.php?&"+Date.now()),o.withCredentials=!0,o.onloadend=function(){var o,i,r;200==this.status||304==this.status?(o=JSON.parse(this.responseText),t&&"j"!=Main.board?o.no>t.no&&(r=o.no-t.no,e.displayJCount(n,a,o.no,r),i={time:Date.now(),no:t.no,delta:r}):i={time:Date.now(),no:o.no},i&&localStorage.setItem("4chan-j-count",JSON.stringify(i))):console.log("Error: Could not load /j/ post count (Status: "+this.status+").")},o.send(null)):t.delta&&e.displayJCount(n,a,t.no,t.delta))},e.clearJCount=function(){var e,t,n,a;n=$.id("j-tooltip"),a=$.id("j-tooltip-bot"),n&&(t=this.getAttribute("data-no"),e={time:Date.now(),no:t},localStorage.setItem("4chan-j-count",JSON.stringify(e)),n.parentNode.removeChild(n),a.parentNode.removeChild(a),setTimeout(function(){var e=$.cls("j-newposts");e[0]&&(e[0].textContent="j",$.removeClass(e[0],"j-newposts"),e[0].textContent="j",$.removeClass(e[0],"j-newposts"))},10))},e.hasFlag=function(e){return-1!=this.flags.indexOf(e)},e.icons={multi:"multi.png",ban:"ban.png",arrow_right:"arrow_right.png",spoiler:"s.png"},e.initIcons=function(){var t,n;if(n="//s.4cdn.org/image/buttons/"+{yotsuba_new:"futaba/",futaba_new:"futaba/",yotsuba_b_new:"burichan/",burichan_new:"burichan/",tomorrow:"tomorrow/",photon:"photon/"}[Main.stylesheet],window.devicePixelRatio>=2)for(t in e.icons)e.icons[t]=e.icons[t].replace(".","@2x.");for(t in e.icons)e.icons[t]=n+e.icons[t]},e.initNavLinks=function(){var t,n,a,o,i,r;i=$.id("navtopright"),r=$.id("navbotright"),(t=document.createElement("span")).id="j-link",t.innerHTML='[j]',t.firstElementChild.addEventListener("mouseup",e.clearJCount,!1),i.parentNode.insertBefore(t,i),(t=t.cloneNode(!0)).id="j-link-bot",t.firstElementChild.addEventListener("mouseup",e.clearJCount,!1),r.parentNode.insertBefore(t,r),e.refreshJCount(),n=i.lastElementChild.previousSibling,(a=document.createDocumentFragment()).appendChild(document.createTextNode("] [")),(t=document.createElement("a")).textContent="Team",t.href="https://"+e.teamSubDomain+".4chan.org/",t.setAttribute("target","_blank"),a.appendChild(t),a.appendChild(document.createTextNode("] [")),o=a.cloneNode(!0),i.replaceChild(a,n),n=r.lastElementChild.previousSibling,r.replaceChild(o,n),Main.tid&&Main.hasMobileLayout&&((t=document.createElement("span")).className="mobileib button redButton",t.innerHTML='',(i=$.cls("navLinks")[0])&&i.appendChild(t))},e.initPostForm=function(){var t,n,a,o,i;!(t=$.id("postForm"))&&Main.tid&&(o=$.cls("closed")[0])&&((n=document.createElement("form")).name="post",n.method="POST",n.enctype="multipart/form-data",n.action="https://sys."+$L.d(Main.board)+"/"+Main.board+"/post",n.innerHTML=e.postFormHTML+'',o.parentNode.insertBefore(n,o),t=n.firstElementChild,QR.enabled=!0),t&&("hidden"==(n=document.forms.post.name).type&&e.hasFlag("forcedanonname")&&((a=document.createElement("tr")).setAttribute("data-type","Name"),a.innerHTML='Name',(i=Main.getCookie("4chan_name"))&&(a.lastChild.firstChild.value=i),(o=$.id("postForm").firstElementChild).insertBefore(a,o.firstElementChild),n.parentNode.removeChild(n)),e.hasFlag("html")&&((n=document.createElement("tr")).innerHTML='Extra[] [ ] Preview',(t=t.firstElementChild).insertBefore(n,t.lastElementChild)))},e.onHTMLToggle=function(e){var t=$.cls("html-otp",e.parentNode.parentNode)[0];t&&(t.style.display=e.checked?"inline":"")},e.onPreviewHTMLClick=function(t){var n,a,o;e.xhrs.html||""!==(a=document.forms.qrPost?document.forms.qrPost:document.forms.post).com.value&&((n=new FormData).append("com",a.com.value),(o=new XMLHttpRequest).open("post",a.action+"?mode=preview_html",!0),o.withCredentials=!0,o.onload=e.onHTMLPReviewLoaded,o.onerror=e.onHTMLPReviewError,e.xhrs.html=o,$.addClass(t,"disabled"),o.send(n),Feedback.notify("Processing...",null))},e.resetHTMLPreviewBtn=function(){var e=$.cls("preview-html-btn");$.removeClass(e[0],"disabled"),e[1]&&$.removeClass(e[1],"disabled")},e.onHTMLPReviewLoaded=function(){var t;e.xhrs.html=null,Feedback.hideMessage(),e.resetHTMLPreviewBtn();try{"error"==(t=JSON.parse(this.responseText)).status&&Feedback.error(t.message)}catch(n){Feedback.error("Something went wrong.")}e.buildHTMLPreview(t.data)},e.onHTMLPReviewError=function(){e.xhrs.html=null,Feedback.hideMessage(),e.resetHTMLPreviewBtn(),console.log(this)},e.closeHTMLPreview=function(){var t;(t=$.id("html-preview-cnt"))&&t.parentNode.removeChild(t),e.resetHTMLPreviewBtn()},e.buildHTMLPreview=function(t){var n;e.closeHTMLPreview(),(n=document.createElement("div")).id="html-preview-cnt",n.setAttribute("data-cmd","close-html-preview"),n.innerHTML='
    Preview HTML PostClose
    '+t+"
    ",document.body.appendChild(n)},e.onThreadMouseOver=function(t){var n=t.target;$.hasClass(n,"thumb")&&(e.hasCatalogControls&&e.hideCatalogControls(),$.hasClass(n.parentNode.parentNode,"disabled")||e.showCatalogControls(n))},e.showCatalogControls=function(t){var n,a,o;a=t.getAttribute("data-id"),(n=document.createElement("div")).id="cat-ctrl",n.className=e.stylesheet,n.innerHTML='',(o=$.cls("threadIcons",t.parentNode.parentNode)[0])?o.insertBefore(n,o.firstElementChild):((o=document.createElement("div")).className="threadIcons",o.appendChild(n),t.parentNode.parentNode.insertBefore(o,t.parentNode.nextElementSibling)),e.hasCatalogControls=!0},e.hideCatalogControls=function(){var t=$.id("cat-ctrl");t&&t.parentNode.removeChild(t),e.hasCatalogControls=!1},e.initCatalog=function(){window.Main={board:location.pathname.split(/\//)[1]},Main.addTooltip=function(e,t,n){var a,o;return(a=document.createElement("div")).className="click-me",n&&(a.id=n),a.innerHTML=t||"Change your settings",e.parentNode.appendChild(a),o=(e.offsetWidth-a.offsetWidth+e.offsetLeft-a.offsetLeft)/2,a.style.marginLeft=o+"px",a},(e.stylesheet=e.getCookie(window.style_group))?e.stylesheet=e.stylesheet.toLowerCase().replace(/ /g,"_"):e.stylesheet="nws_style"==style_group?"yotsuba_new":"yotsuba_b_new",Main.stylesheet=e.stylesheet,e.initIconsCatalog(),e.addCss(),document.addEventListener("click",e.onClick,!1),e.runCatalog()},e.runCatalog=function(){var t;e.initNavLinks(),FC.hasMobileLayout||n.init(),t=$.id("threads"),e.initCatAutoReload(!0),document.addEventListener("keydown",e.onCatalogKeyDown,!1),$.on(t,"mouseover",e.onThreadMouseOver),window.text_only&&document.addEventListener("4chanPostMenuReady",e.onPostMenuReady,!1)},e.init=function(){var t;SettingsMenu.options.Moderator={useIconButtons:["Use icon buttons","Display old-style buttons instead of using drop-down"],changeUpdateDelay:["Reduce auto-update delay","Reduce the thread updater delay",!0],fixedAdminToolbox:["Pin Moderator Tools to the page","Moderator Tools will scroll with you"],inlinePopups:["Inline admin panels","Open admin panels in browser window, instead of a popup"],disableMngExt:["Disable moderator extension","Completely disable the moderator extension (overrides any checked boxes)",!0]},Config.disableMngExt||((t=Main.getCookie("4chan_aflags"))&&(e.flags=t.split(",")),e.addCss(),Config.useIconButtons&&e.initIcons(),QR.noCooldown=QR.noCaptcha=!0,document.addEventListener("click",e.onClick,!1),document.addEventListener("DOMContentLoaded",e.run,!1))},e.run=function(){var t,a,o;document.removeEventListener("DOMContentLoaded",e.run,!1),e.initNavLinks(),e.initPostForm(),Main.hasMobileLayout||n.init(),Config.revealSpoilers&&$.addClass(document.body,"reveal-img-spoilers"),Config.threadUpdater&&Main.tid&&Config.changeUpdateDelay&&(ThreadUpdater.delayIdHidden=3,ThreadUpdater.delayRange=[5,10,15,20,30,60],ThreadUpdater.apiUrlFilter=e.apiUrlFilter),Config.useIconButtons&&!Main.hasMobileLayout&&(Main.tid?(t=document.getElementById("t"+Main.tid).getElementsByClassName("postInfo"),e.postCount=t.length,e.postCount>e.chunkSize?(e.nextChunk=t[0],window.addEventListener("scroll",e.onScroll,!1),e.onScroll()):e.onParsingDone(),o=$.cls("navLinksBot")[0],(a=document.createElement("span")).id="threadOptsButtom",a.innerHTML='[Thread Options]',o.appendChild(a)):e.onParsingDone(),document.addEventListener("4chanParsingDone",e.onParsingDone,!1),e.parserEventBound=!0),document.addEventListener("4chanPostMenuReady",e.onPostMenuReady,!1),(o=$.id("boardSelectMobile"))&&((a=document.createElement("option")).value="j",a.textContent="/j/ - Janitors & Moderators",o.insertBefore(a,o.firstElementChild))},e.getCookie=function(e){var t,n,a,o;for(o=e+"=",a=document.cookie.split(";"),t=0;n=a[t];++t){for(;" "==n.charAt(0);)n=n.substring(1,n.length) +;if(0===n.indexOf(o))return decodeURIComponent(n.substring(o.length,n.length))}return null},e.postFormHTML='
    Name
    Options
    Comment
    File[]
    ',e.addCss=function(){var t,n;n='#adminToolbox { max-width: 256px; display: block; position: absolute; padding: 3px;}#adminToolbox h4 { font-size: 12px; margin: 2px 0 0; padding: 0; font-weight: normal;}#adminToolbox li { list-style: none;}#adminToolbox ul { padding: 0; margin: 2px 0 0 10px;}#atHeader { height: 17px; font-weight: bold; padding-bottom: 2px;}#atRefresh { margin: -1px 0 0 3px;}.post-ip { margin-left: 5px;}#delete-prompt > div { text-align: center;}#watchList li:first-child { margin-top: 3px; padding-top: 2px; border-top: 1px solid rgba(0, 0, 0, 0.20);}.photon #atHeader { border-bottom: 1px solid #ccc;}.yotsuba_new #atHeader { border-bottom: 1px solid #d9bfb7;}.yotsuba_b_new #atHeader { border-bottom: 1px solid #b7c5d9;}.tomorrow #atHeader { border-bottom: 1px solid #111;}#captchaFormPart { display: none;}#at-prio-appeals { color: blue;}#at-illegal-br,#at-illegal { color: red;}#at-msg-cnt { display: none;}.j-newposts { font-weight: bold !important;}#j-link,#j-link-bot { margin-right: 3px; display: inline-block; margin-left: 3px;}#adminToolbox hr { margin: 2px 0;}#threadOptsClose,#banReqClose { float: right;}#threadOpts iframe,#banReq iframe { overflow: hidden;}#threadOpts,#banReq { display: block; position: fixed; padding: 2px; font-size: 10pt;}#banReq { height: 490px;}#threadOpts { height: 194px;}#threadOptsHeader,#banReqHeader { text-align: center; margin-bottom: 1px; padding: 0; height: 18px; line-height: 18px;}#threadOptsButtom { float: right; margin-right: 4px;}.mobileExtControls { float: right; font-size: 11px; margin-bottom: 3px;}.ws .mobileExtControls { color: #34345C;}.nws .mobileExtControls { color: #0000EE;}.reply .mobileExtControls { margin-right: 5px;}.mobileExtControls span { margin-left: 10px;}.mobileExtControls span:after { content: "]";}.mobileExtControls span:before { content: "[";}.nws .mobileExtControls span:after { color: #800000;}.nws .mobileExtControls span:before { color: #800000;}.ws .mobileExtControls span:after { color: #000;}.ws .mobileExtControls span:before { color: #000;}.m-dark .mobileExtControls { color: #81a2be !important;}.m-dark .mobileExtControls span:after,.m-dark .mobileExtControls span:before { color: #1d1f21 !important;}#cat-ctrl { display: inline-block; margin-right: 2px; margin-top: -1px;}#cat-ctrl .threadIcon { cursor: pointer;}.disabled { opacity: 0.5;}.burichan_new .deleteIcon,.yotsuba_b_new .deleteIcon { background-image: url("//s.4cdn.org/image/buttons/burichan/cross.png");}.burichan_new .banIcon,.yotsuba_b_new .banIcon { background-image: url("//s.4cdn.org/image/buttons/burichan/ban.png");}.burichan_new .fileIcon,.yotsuba_b_new .fileIcon { background-image: url("//s.4cdn.org/image/buttons/burichan/f.png");}.burichan_new .multiIcon,.yotsuba_b_new .multiIcon { background-image: url("//s.4cdn.org/image/buttons/burichan/multi.png");}.futaba_new .deleteIcon,.yotsuba_new .deleteIcon { background-image: url("//s.4cdn.org/image/buttons/futaba/cross.png");}.futaba_new .banIcon,.yotsuba_new .banIcon { background-image: url("//s.4cdn.org/image/buttons/futaba/ban.png");}.futaba_new .fileIcon,.yotsuba_new .fileIcon { background-image: url("//s.4cdn.org/image/buttons/futaba/f.png");}.futaba_new .multiIcon,.yotsuba_new .multiIcon { background-image: url("//s.4cdn.org/image/buttons/futaba/multi.png");}.photon .deleteIcon { background-image: url("//s.4cdn.org/image/buttons/photon/cross.png");}.photon .banIcon { background-image: url("//s.4cdn.org/image/buttons/photon/ban.png");}.photon .fileIcon { background-image: url("//s.4cdn.org/image/buttons/photon/f.png");}.photon .multiIcon { background-image: url("//s.4cdn.org/image/buttons/photon/multi.png");}.tomorrow .deleteIcon { background-image: url("//s.4cdn.org/image/buttons/tomorrow/cross.png");}.tomorrow .banIcon { background-image: url("//s.4cdn.org/image/buttons/tomorrow/ban.png");}.tomorrow .fileIcon { background-image: url("//s.4cdn.org/image/buttons/tomorrow/f.png");}.tomorrow .multiIcon { background-image: url("//s.4cdn.org/image/buttons/tomorrow/multi.png");}.dd-admin { text-indent: 5px;}.dd-admin:before { color: #FF0000; content: "\u2022"; left: -3px; position: absolute;}.extPanel { border: 1px solid rgba(0, 0, 0, 0.2);}.extPanel img.pointer { width: 18px; height: 18px }.preview-html-btn { font-size: 11px; }#html-preview-cnt .extPanel { width: 800px; margin-left: -400px; }#html-preview-cnt { position: fixed; width: 100%; height: 100%; z-index: 9002; top: 0; left: 0;}#html-preview-cnt { line-height: 14px; font-size: 14px; background-color: rgba(0, 0, 0, 0.25);}#html-preview-cnt:after { display: inline-block; height: 100%; vertical-align: middle; content: "";}#html-preview-cnt > div { -moz-box-sizing: border-box; box-sizing: border-box; display: inline-block; height: auto; max-height: 100%; position: relative; width: 400px; left: 50%; margin-left: -200px; overflow: auto; box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); vertical-align: middle;}.reveal-img-spoilers .imgspoiler::before { content: " "; width:0.75em; height:0.75em; border-radius: 0.5em; position: absolute; display: block; background: red; margin-top: 1px; margin-left: 1px; pointer-events: none;}.reveal-img-spoilers.is_catalog .imgspoiler::before { margin-top: 4px; margin-left: 12px;}.reveal-img-spoilers .imgspoiler:hover::before { background: #fff; }.html-otp { display: none; width: 50px; }.html-otp input { width: 50px !important; height: 12px; }.drag { -moz-user-select: none !important; cursor: move !important;}'+(e.isCatalog?'.panelHeader .panelCtrl { position: absolute; right: 5px; top: 5px;}.active-btn { border-bottom: 3px double; }.UIPanel { position: fixed; width: 100%; height: 100%; top: 0; left: 0; z-index: 9000 !important;}.UIPanel { line-height: 14px; font-size: 14px; background-color: rgba(0, 0, 0, 0.25);}.UIPanel:after { display: inline-block; height: 100%; vertical-align: middle; content: "";}.UIPanel > div { -moz-box-sizing: border-box; box-sizing: border-box; display: inline-block; height: auto; max-height: 100%; position: relative; width: 400px; left: 50%; margin-left: -200px; overflow: auto; box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); vertical-align: middle;}.UIPanel .extPanel { padding: 2px;}':""),(t=document.createElement("style")).setAttribute("type","text/css"),t.textContent=n,document.head.appendChild(t)},/https?:\/\/boards\.(?:4chan|4channel)\.org\/[a-z0-9]+\/catalog($|#.*$)/.test(location.href)?(e.isCatalog=!0,e.initCatalog()):e.init()}(); \ No newline at end of file diff --git a/js/tcaptcha.js b/js/tcaptcha.js new file mode 100644 index 0000000..cccfb14 --- /dev/null +++ b/js/tcaptcha.js @@ -0,0 +1,684 @@ +var TCaptcha = { + node: null, + + frameNode: null, + imgCntNode: null, + bgNode: null, + fgNode: null, + msgNode: null, + sliderNode: null, + respNode: null, + reloadNode: null, + helpNode: null, + challengeNode: null, + + ticketCaptchaNode: null, + + challenge: null, + + reloadTs: null, + reloadTimeout: null, + expireTimeout: null, + frameTimeout: null, + + pcdBypassable: false, + + errorCb: null, + + path: '/captcha', + + ticketKey: '4chan-tc-ticket', + + domain: '4chan.org', + + failCd: 60, + + tabindex: null, + + hCaptchaSiteKey: '49d294fa-f15c-41fc-80ba-c2544c52ec2a', + + init: function(el, board, thread_id, tabindex) { + if (this.node) { + this.destroy(); + } + + if (tabindex) { + this.tabindex = tabindex; + } + + this.node = el; + + el.style.position = 'relative'; + el.style.width = '300px'; + + this.frameNode = null; + this.imgCntNode = this.buildImgCntNode(); + this.bgNode = this.buildImgNode('bg'); + this.fgNode = this.buildImgNode('fg'); + this.sliderNode = this.buildSliderNode(); + + this.respNode = this.buildRespField(); + this.reloadNode = this.buildReloadNode(board, thread_id); + this.helpNode = this.buildHelpNode(); + this.msgNode = this.buildMsgNode(); + this.challengeNode = this.buildChallengeNode(); + + el.appendChild(this.reloadNode); + el.appendChild(this.respNode); + el.appendChild(this.helpNode); + + this.imgCntNode.appendChild(this.bgNode); + this.imgCntNode.appendChild(this.fgNode); + el.appendChild(this.imgCntNode); + + el.appendChild(this.sliderNode); + el.appendChild(this.msgNode); + el.appendChild(this.challengeNode); + + window.addEventListener('message', this.onFrameMessage); + }, + + destroy: function() { + let self = TCaptcha; + + if (!self.node) { + return; + } + + window.removeEventListener('message', self.onFrameMessage); + + clearTimeout(self.frameTimeout); + clearTimeout(self.reloadTimeout); + clearTimeout(self.expireTimeout); + + self.node.textContent = ''; + + self.node = null; + self.frameNode = null; + self.imgCntNode = null; + self.bgNode = null; + self.fgNode = null; + self.msgNode = null; + self.sliderNode = null; + self.respNode = null; + self.reloadNode = null; + self.helpNode = null; + self.challengeNode = null; + + self.ticketCaptchaNode = null; + + self.challenge = null; + + self.pcdBypassable = false; + + self.errorCb = null; + + self.reloadTs = null; + + self.onReloadCdDone = null; + }, + + setErrorCb: function(func) { + TCaptcha.errorCb = func; + }, + + toggleImgCntNode: function(flag) { + TCaptcha.imgCntNode.style.display = flag ? 'block' : 'none'; + }, + + getTicket: function() { + return localStorage.getItem(TCaptcha.ticketKey); + }, + + setTicket: function(val) { + if (val) { + localStorage.setItem(TCaptcha.ticketKey, val); + } + else if (val === false) { + localStorage.removeItem(TCaptcha.ticketKey); + } + }, + + buildFrameNode: function() { + let el = document.createElement('iframe'); + el.id = 't-frame'; + el.style.border = '0'; + el.style.width = '100%'; + el.style.height = '80px'; + el.style.marginTop = '2px'; + el.style.position = 'relative'; + el.style.display = 'block'; + el.onerror = TCaptcha.onFrameError; + return el; + }, + + buildImgCntNode: function() { + let el = document.createElement('div'); + el.id = 't-cnt'; + el.style.height = '80px'; + el.style.marginTop = '2px'; + el.style.position = 'relative'; + return el; + }, + + buildImgNode: function(id) { + let el = document.createElement('div'); + el.id = 't-' + id; + el.style.width = '100%'; + el.style.height = '100%'; + el.style.position = 'absolute'; + el.style.backgroundRepeat = 'no-repeat'; + el.style.backgroundPosition = 'top left'; + el.style.pointerEvents = 'none'; + return el; + }, + + buildMsgNode: function() { + let el = document.createElement('div'); + el.id = 't-msg'; + el.style.width = '100%'; + el.style.height = 'calc(100% - 20px)'; + el.style.position = 'absolute'; + el.style.top = '20px'; + el.style.textAlign = 'center'; + el.style.fontSize = '12px'; + el.style.filter = 'inherit'; + el.style.display = 'none'; + el.style.alignContent = 'center'; + return el; + }, + + buildRespField: function() { + let el = document.createElement('input'); + el.id = 't-resp'; + el.name = 't-response'; + el.placeholder = 'Type the CAPTCHA here'; + el.setAttribute('autocomplete', 'off'); + el.type = 'text'; + el.style.width = '160px'; + el.style.boxSizing = 'border-box'; + el.style.textTransform = 'uppercase'; + el.style.fontSize = '11px'; + el.style.height = '18px'; + el.style.margin = '0'; + el.style.padding = '0 2px'; + el.style.fontFamily = 'monospace'; + el.style.verticalAlign = 'middle'; + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex + 2); + } + return el; + }, + + buildSliderNode: function() { + let el = document.createElement('input'); + el.id = 't-slider'; + el.setAttribute('autocomplete', 'off'); + el.type = 'range'; + el.style.width = '100%'; + el.style.boxSizing = 'border-box'; + el.style.visibility = 'hidden'; + el.style.margin = '0'; + el.style.transition = 'box-shadow 15s linear'; + el.style.boxShadow = '0 0 6px 4px #1d8dc4'; + el.style.position = 'relative'; + el.value = 0; + el.min = 0; + el.max = 100; + el.addEventListener('input', this.onSliderInput, false); + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex + 1); + } + return el; + }, + + buildChallengeNode: function() { + let el = document.createElement('input'); + el.name = 't-challenge'; + el.type = 'hidden'; + return el; + }, + + buildReloadNode: function(board, thread_id) { + let el = document.createElement('button'); + el.id = 't-load'; + el.type = 'button'; + el.style.fontSize = '11px'; + el.style.padding = '0'; + el.style.width = '90px'; + el.style.boxSizing = 'border-box'; + el.style.margin = '0 6px 0 0'; + el.style.verticalAlign = 'middle'; + el.style.height = '18px'; + el.textContent = 'Get Captcha'; + el.setAttribute('data-board', board); + el.setAttribute('data-tid', thread_id); + el.addEventListener('click', this.onReloadClick, false); + if (this.tabindex) { + el.setAttribute('tabindex', this.tabindex); + } + return el; + }, + + buildHelpNode: function() { + let el = document.createElement('button'); + el.id = 't-help'; + el.type = 'button'; + el.style.fontSize = '11px'; + el.style.padding = '0'; + el.style.width = '20px'; + el.style.boxSizing = 'border-box'; + el.style.margin = '0 0 0 6px'; + el.style.verticalAlign = 'middle'; + el.style.height = '18px'; + el.textContent = '?'; + el.setAttribute('data-tip', 'Help'); + el.tabIndex = -1; + el.addEventListener('click', this.onHelpClick, false); + return el; + }, + + onHelpClick: function() { + let str = `- Only type letters and numbers displayed in the image. +- If needed, use the slider to align the image to make it readable. +- Make sure to not block any cookies set by 4chan.`; + alert(str); + }, + + onTicketCaptchaError: function() { + TCaptcha.toggleMsgOverlay(true, "Couldn't load the captcha.

    Please check your browser's content blocker."); + }, + + onTicketCaptchaDone: function(resp) { + TCaptcha.reloadNode.setAttribute('data-ticket-resp', resp); + TCaptcha.destroyTicketCaptcha(); + TCaptcha.onReloadClick(); + }, + + loadTicketCaptcha: function() { + window.pcd_c_loaded = TCaptcha.buildTicketCaptcha; + window.pcd_c_done = TCaptcha.onTicketCaptchaDone; + TCaptcha.toggleMsgOverlay(true, 'Loading…'); + let s = document.createElement('script'); + s.src = 'https://js.hcaptcha.com/1/api.js?onload=pcd_c_loaded&render=explicit&recaptchacompat=off'; + s.onerror = TCaptcha.onTicketCaptchaError; + document.head.appendChild(s); + }, + + buildTicketCaptcha: function() { + let self = TCaptcha; + + self.toggleMsgOverlay(false); + + if (!window.hcaptcha) { + self.loadTicketCaptcha(); + return; + } + + let el = document.createElement('div'); + el.id = 't-tc-cnt'; + self.imgCntNode.appendChild(el); + + let wid = window.hcaptcha.render('t-tc-cnt', { + sitekey: self.hCaptchaSiteKey, + callback: 'pcd_c_done' + }); + + el.setAttribute('data-wid', wid); + + self.ticketCaptchaNode = el; + }, + + destroyTicketCaptcha: function() { + let self = TCaptcha; + + if (!window.hcaptcha || !self.ticketCaptchaNode) { + return; + } + + let wid = self.ticketCaptchaNode.getAttribute('data-wid'); + window.hcaptcha.reset(wid); + self.imgCntNode.removeChild(self.ticketCaptchaNode); + self.ticketCaptchaNode = null; + }, + + onReloadClick: function() { + let btn = TCaptcha.reloadNode; + let board = btn.getAttribute('data-board'); + let thread_id = btn.getAttribute('data-tid'); + let ticket_resp = btn.getAttribute('data-ticket-resp'); + btn.removeAttribute('data-ticket-resp'); + TCaptcha.toggleReloadBtn(false, 'Loading'); + TCaptcha.load(board, thread_id, ticket_resp); + }, + + onFrameMessage: function(e) { + if (e.origin !== `https://sys.${TCaptcha.domain}`) { + return; + } + + if (e.data && e.data.twister) { + TCaptcha.destroyFrame(); + TCaptcha.buildFromJson(e.data.twister); + } + }, + + onFrameError: function(e) { + TCaptcha.unlockReloadBtn(); + + console.log(e); + + if (TCaptcha.errorCb) { + TCaptcha.errorCb.call(null, + "Couldn't load the captcha frame. Check your content blocker settings." + ); + } + }, + + load: function(board, thread_id, ticket_resp) { + let self = TCaptcha; + + clearTimeout(self.frameTimeout); + clearTimeout(self.reloadTimeout); + clearTimeout(self.expireTimeout); + + let params = ['framed=1']; + + if (board) { + params.push('board=' + board); + } + + if (thread_id > 0) { + params.push('thread_id=' + thread_id); + } + + if (ticket_resp) { + params.push('ticket_resp=' + encodeURIComponent(ticket_resp)); + } + + let ticket = self.getTicket(); + + if (ticket) { + params.push('ticket=' + ticket); + } + + if (params.length > 0) { + params = '?' + params.join('&'); + } + + let src = 'https://sys.' + self.domain + self.path + params; + + self.frameNode = self.buildFrameNode(); + self.toggleImgCntNode(false); + self.node.insertBefore(self.frameNode, self.imgCntNode); + self.frameTimeout = setTimeout(self.onFrameTimeout, 60000, src); + self.frameNode.src = src; + }, + + onFrameTimeout: function(src) { + let self = TCaptcha; + + self.destroyFrame(); + + console.log('Captcha frame timeout'); + + if (self.errorCb) { + self.errorCb.call(null, `Couldn't get the captcha. +Make sure your browser doesn't block content on 4chan then click +here.`); + } + }, + + destroyFrame: function() { + let self = TCaptcha; + + clearTimeout(self.frameTimeout); + self.frameTimeout = null; + if (self.frameNode) { + self.frameNode.remove(); + self.frameNode = null; + } + self.toggleImgCntNode(true); + self.unlockReloadBtn(); + }, + + unlockReloadBtn: function() { + TCaptcha.reloadTs = null; + TCaptcha.toggleReloadBtn(true, 'Get Captcha'); + }, + + toggleReloadBtn: function(flag, label) { + let self = TCaptcha; + + if (self.reloadNode) { + self.reloadNode.disabled = !flag; + + if (label !== undefined) { + self.reloadNode.textContent = label; + } + } + }, + + onCaptchaFailed: function() { + let self = TCaptcha; + + let cd = self.failCd * 1000; + + if (self.reloadTs && self.reloadTs < cd) { + self.setReloadCd(cd, true); + } + }, + + setReloadCd: function(cd, visible, onDone) { + let self = TCaptcha; + + if (!self.node) { + return; + } + + clearTimeout(self.reloadTimeout); + + self.onReloadCdDone = onDone; + + self.pcdBypassable = visible === -1; + + if (cd) { + self.toggleReloadBtn(false); + if (visible) { + self.reloadTs = Date.now() + cd; + self.onReloadCdTick(); + } + else { + self.reloadTimeout = setTimeout(self.stopReloadCd, cd); + } + } + else { + self.stopReloadCd(); + } + }, + + stopReloadCd: function() { + let self = TCaptcha; + self.unlockReloadBtn(); + if (self.onReloadCdDone) { + self.onReloadCdDone.call(self); + } + }, + + onReloadCdTick: function() { + let self = TCaptcha; + + if (!self.reloadNode || !self.reloadTs) { + return; + } + + let cd = self.reloadTs - Date.now(); + + if (self.pcdBypassable) { + if (document.cookie.indexOf('_ev1=') !== -1) { + cd = 0; + } + } + + if (cd > 0) { + self.reloadNode.textContent = Math.ceil(cd / 1000); + self.reloadTimeout = setTimeout(self.onReloadCdTick, Math.min(cd, 1000)); + } + else { + self.stopReloadCd(); + } + }, + + clearChallenge: function() { + let self = TCaptcha; + + if (self.node) { + self.challengeNode.value = ''; + self.respNode.value = ''; + self.fgNode.style.backgroundImage = ''; + self.bgNode.style.backgroundImage = ''; + self.toggleSlider(false); + self.toggleMsgOverlay(false); + } + }, + + toggleSlider: function(flag) { + TCaptcha.sliderNode.style.visibility = flag ? '' : 'hidden'; + TCaptcha.sliderNode.style.boxShadow = flag ? '' : '0 0 4px 2px #1d8dc4'; + }, + + toggleMsgOverlay: function(flag, txt) { + if (txt !== undefined) { + TCaptcha.msgNode.innerHTML = `
    ${txt}
    `; + } + TCaptcha.msgNode.style.display = flag ? 'grid' : 'none'; + }, + + onSliderInput: function() { + var m = -Math.floor((+this.value) / 100 * this.twisterDelta); + TCaptcha.bgNode.style.backgroundPositionX = m + 'px'; + }, + + onTicketPcdTick: function() { + let self = TCaptcha; + + let el = document.getElementById('t-pcd'); + + if (!el) { + return; + } + + let pcd = +el.getAttribute('data-pcd'); + + pcd = pcd - (0 | (Date.now() / 1000)); + + if (pcd <= 0) { + self.onTicketPcdEnd(); + return; + } + + el.textContent = pcd; + + setTimeout(self.updateTicketPcd, 1000); + }, + + clearTicketOverlay: function() { + TCaptcha.toggleMsgOverlay(false); + }, + + buildFromJson: function(data) { + let self = TCaptcha; + + if (!self.node) { + return; + } + + self.unlockReloadBtn(); + self.toggleSlider(false); + self.toggleMsgOverlay(false); + + self.setTicket(data.ticket); + + if (TCaptcha.errorCb) { + TCaptcha.errorCb.call(null, ''); + } + + if (data.cd) { + self.setReloadCd(data.cd * 1000, !data.challenge); + } + + if (data.mpcd) { + self.clearChallenge(); + self.destroyTicketCaptcha(); + self.buildTicketCaptcha(); + return; + } + + if (data.pcd) { + self.buildTicket(data); + return; + } + + if (data.error) { + console.log(data.error); + + if (TCaptcha.errorCb) { + TCaptcha.errorCb.call(null, data.error); + } + + return; + } + + self.imgCntNode.style.width = data.img_width + 'px'; + self.imgCntNode.style.height = data.img_height + 'px'; + + self.challengeNode.value = data.challenge; + + self.expireTimeout = setTimeout(self.clearChallenge, data.ttl * 1000 - 3000); + + if (data.bg_width) { + self.buildTwister(data); + } + else if (data.img) { + self.buildStatic(data); + } + else { + self.buildNoop(data); + } + }, + + buildTwister: function(data) { + let self = TCaptcha; + + self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')'; + self.bgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.bg + ')'; + + self.bgNode.style.backgroundPositionX = '0px'; + + self.toggleSlider(true); + self.sliderNode.value = 0; + self.sliderNode.twisterDelta = data.bg_width - data.img_width; + self.sliderNode.focus(); + }, + + buildStatic: function(data) { + let self = TCaptcha; + self.fgNode.style.backgroundImage = 'url(data:image/png;base64,' + data.img + ')'; + self.bgNode.style.backgroundImage = ''; + }, + + buildTicket: function(data) { + let self = TCaptcha; + self.toggleMsgOverlay(true, data.pcd_msg || 'Please wait a while.'); + self.fgNode.style.backgroundImage = ''; + self.bgNode.style.backgroundImage = ''; + self.setReloadCd(data.pcd * 1000, data.bpcd ? -1 : true, self.clearTicketOverlay); + }, + + buildNoop: function(data) { + let self = TCaptcha; + self.toggleMsgOverlay(true, 'Verification not required.'); + self.fgNode.style.backgroundImage = ''; + self.bgNode.style.backgroundImage = ''; + } +}; diff --git a/js/tcaptcha.min.js b/js/tcaptcha.min.js new file mode 100644 index 0000000..fae49be --- /dev/null +++ b/js/tcaptcha.min.js @@ -0,0 +1 @@ +var TCaptcha={node:null,frameNode:null,imgCntNode:null,bgNode:null,fgNode:null,msgNode:null,sliderNode:null,respNode:null,reloadNode:null,helpNode:null,challengeNode:null,ticketCaptchaNode:null,challenge:null,reloadTs:null,reloadTimeout:null,expireTimeout:null,frameTimeout:null,pcdBypassable:!1,errorCb:null,path:"/captcha",ticketKey:"4chan-tc-ticket",domain:"4chan.org",failCd:60,tabindex:null,hCaptchaSiteKey:"49d294fa-f15c-41fc-80ba-c2544c52ec2a",init:function(e,t,o,a){this.node&&this.destroy(),a&&(this.tabindex=a),this.node=e,e.style.position="relative",e.style.width="300px",this.frameNode=null,this.imgCntNode=this.buildImgCntNode(),this.bgNode=this.buildImgNode("bg"),this.fgNode=this.buildImgNode("fg"),this.sliderNode=this.buildSliderNode(),this.respNode=this.buildRespField(),this.reloadNode=this.buildReloadNode(t,o),this.helpNode=this.buildHelpNode(),this.msgNode=this.buildMsgNode(),this.challengeNode=this.buildChallengeNode(),e.appendChild(this.reloadNode),e.appendChild(this.respNode),e.appendChild(this.helpNode),this.imgCntNode.appendChild(this.bgNode),this.imgCntNode.appendChild(this.fgNode),e.appendChild(this.imgCntNode),e.appendChild(this.sliderNode),e.appendChild(this.msgNode),e.appendChild(this.challengeNode),window.addEventListener("message",this.onFrameMessage)},destroy:function(){let e=TCaptcha;e.node&&(window.removeEventListener("message",e.onFrameMessage),clearTimeout(e.frameTimeout),clearTimeout(e.reloadTimeout),clearTimeout(e.expireTimeout),e.node.textContent="",e.node=null,e.frameNode=null,e.imgCntNode=null,e.bgNode=null,e.fgNode=null,e.msgNode=null,e.sliderNode=null,e.respNode=null,e.reloadNode=null,e.helpNode=null,e.challengeNode=null,e.ticketCaptchaNode=null,e.challenge=null,e.pcdBypassable=!1,e.errorCb=null,e.reloadTs=null,e.onReloadCdDone=null)},setErrorCb:function(e){TCaptcha.errorCb=e},toggleImgCntNode:function(e){TCaptcha.imgCntNode.style.display=e?"block":"none"},getTicket:function(){return localStorage.getItem(TCaptcha.ticketKey)},setTicket:function(e){e?localStorage.setItem(TCaptcha.ticketKey,e):!1===e&&localStorage.removeItem(TCaptcha.ticketKey)},buildFrameNode:function(){let e=document.createElement("iframe");return e.id="t-frame",e.style.border="0",e.style.width="100%",e.style.height="80px",e.style.marginTop="2px",e.style.position="relative",e.style.display="block",e.onerror=TCaptcha.onFrameError,e},buildImgCntNode:function(){let e=document.createElement("div");return e.id="t-cnt",e.style.height="80px",e.style.marginTop="2px",e.style.position="relative",e},buildImgNode:function(e){let t=document.createElement("div");return t.id="t-"+e,t.style.width="100%",t.style.height="100%",t.style.position="absolute",t.style.backgroundRepeat="no-repeat",t.style.backgroundPosition="top left",t.style.pointerEvents="none",t},buildMsgNode:function(){let e=document.createElement("div");return e.id="t-msg",e.style.width="100%",e.style.height="calc(100% - 20px)",e.style.position="absolute",e.style.top="20px",e.style.textAlign="center",e.style.fontSize="12px",e.style.filter="inherit",e.style.display="none",e.style.alignContent="center",e},buildRespField:function(){let e=document.createElement("input");return e.id="t-resp",e.name="t-response",e.placeholder="Type the CAPTCHA here",e.setAttribute("autocomplete","off"),e.type="text",e.style.width="160px",e.style.boxSizing="border-box",e.style.textTransform="uppercase",e.style.fontSize="11px",e.style.height="18px",e.style.margin="0",e.style.padding="0 2px",e.style.fontFamily="monospace",e.style.verticalAlign="middle",this.tabindex&&e.setAttribute("tabindex",this.tabindex+2),e},buildSliderNode:function(){let e=document.createElement("input");return e.id="t-slider",e.setAttribute("autocomplete","off"),e.type="range",e.style.width="100%",e.style.boxSizing="border-box",e.style.visibility="hidden",e.style.margin="0",e.style.transition="box-shadow 15s linear",e.style.boxShadow="0 0 6px 4px #1d8dc4",e.style.position="relative",e.value=0,e.min=0,e.max=100,e.addEventListener("input",this.onSliderInput,!1),this.tabindex&&e.setAttribute("tabindex",this.tabindex+1),e},buildChallengeNode:function(){let e=document.createElement("input");return e.name="t-challenge",e.type="hidden",e},buildReloadNode:function(e,t){let o=document.createElement("button");return o.id="t-load",o.type="button",o.style.fontSize="11px",o.style.padding="0",o.style.width="90px",o.style.boxSizing="border-box",o.style.margin="0 6px 0 0",o.style.verticalAlign="middle",o.style.height="18px",o.textContent="Get Captcha",o.setAttribute("data-board",e),o.setAttribute("data-tid",t),o.addEventListener("click",this.onReloadClick,!1),this.tabindex&&o.setAttribute("tabindex",this.tabindex),o},buildHelpNode:function(){let e=document.createElement("button");return e.id="t-help",e.type="button",e.style.fontSize="11px",e.style.padding="0",e.style.width="20px",e.style.boxSizing="border-box",e.style.margin="0 0 0 6px",e.style.verticalAlign="middle",e.style.height="18px",e.textContent="?",e.setAttribute("data-tip","Help"),e.tabIndex=-1,e.addEventListener("click",this.onHelpClick,!1),e},onHelpClick:function(){alert("- Only type letters and numbers displayed in the image.\n- If needed, use the slider to align the image to make it readable.\n- Make sure to not block any cookies set by 4chan.")},onTicketCaptchaError:function(){TCaptcha.toggleMsgOverlay(!0,"Couldn't load the captcha.

    Please check your browser's content blocker.")},onTicketCaptchaDone:function(e){TCaptcha.reloadNode.setAttribute("data-ticket-resp",e),TCaptcha.destroyTicketCaptcha(),TCaptcha.onReloadClick()},loadTicketCaptcha:function(){window.pcd_c_loaded=TCaptcha.buildTicketCaptcha,window.pcd_c_done=TCaptcha.onTicketCaptchaDone,TCaptcha.toggleMsgOverlay(!0,"Loading\u2026");let e=document.createElement("script");e.src="https://js.hcaptcha.com/1/api.js?onload=pcd_c_loaded&render=explicit&recaptchacompat=off",e.onerror=TCaptcha.onTicketCaptchaError,document.head.appendChild(e)},buildTicketCaptcha:function(){let e=TCaptcha;if(e.toggleMsgOverlay(!1),!window.hcaptcha)return void e.loadTicketCaptcha();let t=document.createElement("div");t.id="t-tc-cnt",e.imgCntNode.appendChild(t);let o=window.hcaptcha.render("t-tc-cnt",{sitekey:e.hCaptchaSiteKey,callback:"pcd_c_done"});t.setAttribute("data-wid",o),e.ticketCaptchaNode=t},destroyTicketCaptcha:function(){let e=TCaptcha;if(!window.hcaptcha||!e.ticketCaptchaNode)return;let t=e.ticketCaptchaNode.getAttribute("data-wid");window.hcaptcha.reset(t),e.imgCntNode.removeChild(e.ticketCaptchaNode),e.ticketCaptchaNode=null},onReloadClick:function(){let e=TCaptcha.reloadNode,t=e.getAttribute("data-board"),o=e.getAttribute("data-tid"),a=e.getAttribute("data-ticket-resp");e.removeAttribute("data-ticket-resp"),TCaptcha.toggleReloadBtn(!1,"Loading"),TCaptcha.load(t,o,a)},onFrameMessage:function(e){e.origin===`https://sys.${TCaptcha.domain}`&&e.data&&e.data.twister&&(TCaptcha.destroyFrame(),TCaptcha.buildFromJson(e.data.twister))},onFrameError:function(e){TCaptcha.unlockReloadBtn(),console.log(e),TCaptcha.errorCb&&TCaptcha.errorCb.call(null,"Couldn't load the captcha frame. Check your content blocker settings.")},load:function(e,t,o){let a=TCaptcha;clearTimeout(a.frameTimeout),clearTimeout(a.reloadTimeout),clearTimeout(a.expireTimeout);let l=["framed=1"];e&&l.push("board="+e),t>0&&l.push("thread_id="+t),o&&l.push("ticket_resp="+encodeURIComponent(o));let i=a.getTicket();i&&l.push("ticket="+i),l.length>0&&(l="?"+l.join("&"));let d="https://sys."+a.domain+a.path+l;a.frameNode=a.buildFrameNode(),a.toggleImgCntNode(!1),a.node.insertBefore(a.frameNode,a.imgCntNode),a.frameTimeout=setTimeout(a.onFrameTimeout,6e4,d),a.frameNode.src=d},onFrameTimeout:function(e){let t=TCaptcha;t.destroyFrame(),console.log("Captcha frame timeout"),t.errorCb&&t.errorCb.call(null,`Couldn't get the captcha.\nMake sure your browser doesn't block content on 4chan then click\nhere.`)},destroyFrame:function(){let e=TCaptcha;clearTimeout(e.frameTimeout),e.frameTimeout=null,e.frameNode&&(e.frameNode.remove(),e.frameNode=null),e.toggleImgCntNode(!0),e.unlockReloadBtn()},unlockReloadBtn:function(){TCaptcha.reloadTs=null,TCaptcha.toggleReloadBtn(!0,"Get Captcha")},toggleReloadBtn:function(e,t){let o=TCaptcha;o.reloadNode&&(o.reloadNode.disabled=!e,t!==undefined&&(o.reloadNode.textContent=t))},onCaptchaFailed:function(){let e=TCaptcha,t=1e3*e.failCd;e.reloadTs&&e.reloadTs0?(e.reloadNode.textContent=Math.ceil(t/1e3),e.reloadTimeout=setTimeout(e.onReloadCdTick,Math.min(t,1e3))):e.stopReloadCd()},clearChallenge:function(){let e=TCaptcha;e.node&&(e.challengeNode.value="",e.respNode.value="",e.fgNode.style.backgroundImage="",e.bgNode.style.backgroundImage="",e.toggleSlider(!1),e.toggleMsgOverlay(!1))},toggleSlider:function(e){TCaptcha.sliderNode.style.visibility=e?"":"hidden",TCaptcha.sliderNode.style.boxShadow=e?"":"0 0 4px 2px #1d8dc4"},toggleMsgOverlay:function(e,t){t!==undefined&&(TCaptcha.msgNode.innerHTML=`
    ${t}
    `),TCaptcha.msgNode.style.display=e?"grid":"none"},onSliderInput:function(){var e=-Math.floor(+this.value/100*this.twisterDelta);TCaptcha.bgNode.style.backgroundPositionX=e+"px"},onTicketPcdTick:function(){let e=TCaptcha,t=document.getElementById("t-pcd");if(!t)return;let o=+t.getAttribute("data-pcd");(o-=0|Date.now()/1e3)<=0?e.onTicketPcdEnd():(t.textContent=o,setTimeout(e.updateTicketPcd,1e3))},clearTicketOverlay:function(){TCaptcha.toggleMsgOverlay(!1)},buildFromJson:function(e){let t=TCaptcha;if(t.node){if(t.unlockReloadBtn(),t.toggleSlider(!1),t.toggleMsgOverlay(!1),t.setTicket(e.ticket),TCaptcha.errorCb&&TCaptcha.errorCb.call(null,""),e.cd&&t.setReloadCd(1e3*e.cd,!e.challenge),e.mpcd)return t.clearChallenge(),t.destroyTicketCaptcha(),void t.buildTicketCaptcha();if(e.pcd)t.buildTicket(e);else{if(e.error)return console.log(e.error),void(TCaptcha.errorCb&&TCaptcha.errorCb.call(null,e.error));t.imgCntNode.style.width=e.img_width+"px",t.imgCntNode.style.height=e.img_height+"px",t.challengeNode.value=e.challenge,t.expireTimeout=setTimeout(t.clearChallenge,1e3*e.ttl-3e3),e.bg_width?t.buildTwister(e):e.img?t.buildStatic(e):t.buildNoop(e)}}},buildTwister:function(e){let t=TCaptcha;t.fgNode.style.backgroundImage="url(data:image/png;base64,"+e.img+")",t.bgNode.style.backgroundImage="url(data:image/png;base64,"+e.bg+")",t.bgNode.style.backgroundPositionX="0px",t.toggleSlider(!0),t.sliderNode.value=0,t.sliderNode.twisterDelta=e.bg_width-e.img_width,t.sliderNode.focus()},buildStatic:function(e){let t=TCaptcha;t.fgNode.style.backgroundImage="url(data:image/png;base64,"+e.img+")",t.bgNode.style.backgroundImage=""},buildTicket:function(e){let t=TCaptcha;t.toggleMsgOverlay(!0,e.pcd_msg||"Please wait a while."),t.fgNode.style.backgroundImage="",t.bgNode.style.backgroundImage="",t.setReloadCd(1e3*e.pcd,!e.bpcd||-1,t.clearTicketOverlay)},buildNoop:function(){let e=TCaptcha;e.toggleMsgOverlay(!0,"Verification not required."),e.fgNode.style.backgroundImage="",e.bgNode.style.backgroundImage=""}}; \ No newline at end of file diff --git a/js/tegaki-test.js b/js/tegaki-test.js new file mode 100644 index 0000000..0e6e8f3 --- /dev/null +++ b/js/tegaki-test.js @@ -0,0 +1,7665 @@ +/*! tegaki.js, MIT License */'use strict';var TegakiStrings = { + // Messages + badDimensions: 'Invalid dimensions.', + promptWidth: 'Canvas width in pixels', + promptHeight: 'Canvas height in pixels', + confirmDelLayers: 'Delete selected layers?', + confirmMergeLayers: 'Merge selected layers?', + tooManyLayers: 'Layer limit reached.', + errorLoadImage: 'Could not load the image.', + noActiveLayer: 'No active layer.', + hiddenActiveLayer: 'The active layer is not visible.', + confirmCancel: 'Are you sure? Your work will be lost.', + confirmChangeCanvas: 'Are you sure? Changing the canvas will clear all layers and history and disable replay recording.', + + // Controls + color: 'Color', + size: 'Size', + alpha: 'Opacity', + flow: 'Flow', + zoom: 'Zoom', + layers: 'Layers', + switchPalette: 'Switch color palette', + paletteSlotReplace: 'Right click to replace with the current color', + + // Layers + layer: 'Layer', + addLayer: 'Add layer', + delLayers: 'Delete layers', + mergeLayers: 'Merge layers', + moveLayerUp: 'Move up', + moveLayerDown: 'Move down', + toggleVisibility: 'Toggle visibility', + + // Menu bar + newCanvas: 'New', + open: 'Open', + save: 'Save', + saveAs: 'Save As', + export: 'Export', + undo: 'Undo', + redo: 'Redo', + close: 'Close', + finish: 'Finish', + + // Tool modes + tip: 'Tip', + pressure: 'Pressure', + preserveAlpha: 'Preserve Alpha', + + // Tools + pen: 'Pen', + pencil: 'Pencil', + airbrush: 'Airbrush', + pipette: 'Pipette', + blur: 'Blur', + eraser: 'Eraser', + bucket: 'Bucket', + tone: 'Tone', + + // Replay + gapless: 'Gapless', + play: 'Play', + pause: 'Pause', + rewind: 'Rewind', + slower: 'Slower', + faster: 'Faster', + recordingEnabled: 'Recording replay', + errorLoadReplay: 'Could not load the replay: ', + loadingReplay: 'Loading replay…', +}; +class TegakiTool { + constructor() { + this.id = 0; + + this.name = null; + + this.keybind = null; + + this.useFlow = false; + + this.useSizeDynamics = false; + this.useAlphaDynamics = false; + this.useFlowDynamics = false; + + this.usePreserveAlpha = false; + + this.step = 0.0; + + this.size = 1; + this.alpha = 1.0; + this.flow = 1.0; + + this.useSize = true; + this.useAlpha = true; + this.useFlow = true; + + this.noCursor = false; + + this.color = '#000000'; + this.rgb = [0, 0, 0]; + + this.brushSize = 0; + this.brushAlpha = 0.0; + this.brushFlow = 0.0; + this.stepSize = 0.0; + this.center = 0.0; + + this.sizeDynamicsEnabled = false; + this.alphaDynamicsEnabled = false; + this.flowDynamicsEnabled = false; + this.preserveAlphaEnabled = false; + + this.tip = -1; + this.tipList = null; + + this.stepAcc = 0; + + this.shapeCache = null; + + this.kernel = null; + } + + brushFn(x, y, offsetX, offsetY) {} + + start(posX, posY) {} + + commit() {} + + draw(posX, posY) {} + + usesDynamics() { + return this.useSizeDynamics || this.useAlphaDynamics || this.useFlowDynamics; + } + + enabledDynamics() { + return this.sizeDynamicsEnabled || this.alphaDynamicsEnabled || this.flowDynamicsEnabled; + } + + setSize(size) { + this.size = size; + } + + setAlpha(alpha) { + this.alpha = alpha; + this.brushAlpha = alpha; + } + + setFlow(flow) { + this.flow = flow; + this.brushFlow = this.easeFlow(flow); + } + + easeFlow(flow) { + return flow; + } + + setColor(hex) { + this.rgb = $T.hexToRgb(hex); + } + + setSizeDynamics(flag) { + if (!this.useSizeDynamics) { + return; + } + + if (!flag) { + this.setSize(this.size); + } + + this.sizeDynamicsEnabled = flag; + } + + setAlphaDynamics(flag) { + if (!this.useAlphaDynamics) { + return; + } + + if (!flag) { + this.setAlpha(this.alpha); + } + + this.alphaDynamicsEnabled = flag; + } + + setFlowDynamics(flag) { + if (!this.useFlowDynamics) { + return; + } + + if (!flag) { + this.setFlow(this.flow); + } + + this.flowDynamicsEnabled = flag; + } + + setPreserveAlpha(flag) { + this.preserveAlphaEnabled = flag; + } + + set() { + this.setAlpha(this.alpha); + this.setFlow(this.flow); + this.setSize(this.size); + this.setColor(Tegaki.toolColor); + + Tegaki.onToolChanged(this); + } +} +class TegakiBrush extends TegakiTool { + constructor() { + super(); + } + + generateShape(size) {} + + brushFn(x, y, offsetX, offsetY) { + var aData, gData, bData, aWidth, canvasWidth, canvasHeight, + kernel, xx, yy, ix, iy, + pa, ka, a, sa, + kr, kg, kb, + r, g, b, + pr, pg, pb, + px, ba, + brushSize, brushAlpha, brushFlow, preserveAlpha; + + preserveAlpha = this.preserveAlphaEnabled; + + kernel = this.kernel; + + brushAlpha = this.brushAlpha; + brushFlow = this.brushFlow; + brushSize = this.brushSize; + + aData = Tegaki.activeLayer.imageData.data; + gData = Tegaki.ghostBuffer.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + canvasHeight = Tegaki.baseHeight; + + aWidth = canvasWidth; + + kr = this.rgb[0]; + kg = this.rgb[1]; + kb = this.rgb[2]; + + for (yy = 0; yy < brushSize; ++yy) { + iy = y + yy + offsetY; + + if (iy < 0 || iy >= canvasHeight) { + continue; + } + + for (xx = 0; xx < brushSize; ++xx) { + ix = x + xx + offsetX; + + if (ix < 0 || ix >= canvasWidth) { + continue; + } + + ka = kernel[(yy * brushSize + xx) * 4 + 3] / 255; + + if (ka <= 0.0) { + continue; + } + + px = (iy * canvasWidth + ix) * 4; + + sa = bData[px + 3] / 255; + sa = sa + ka * brushFlow * (brushAlpha - sa); + + ba = Math.ceil(sa * 255); + + if (ba > bData[px + 3]) { + if (bData[px] === 0) { + gData[px] = aData[px]; + gData[px + 1] = aData[px + 1]; + gData[px + 2] = aData[px + 2]; + gData[px + 3] = aData[px + 3]; + } + + bData[px] = 1; + bData[px + 3] = ba; + + pr = gData[px]; + pg = gData[px + 1]; + pb = gData[px + 2]; + pa = gData[px + 3] / 255; + + a = pa + sa - pa * sa; + + r = ((kr * sa) + (pr * pa) * (1 - sa)) / a; + g = ((kg * sa) + (pg * pa) * (1 - sa)) / a; + b = ((kb * sa) + (pb * pa) * (1 - sa)) / a; + + aData[px] = (kr > pr) ? Math.ceil(r) : Math.floor(r); + aData[px + 1] = (kg > pg) ? Math.ceil(g) : Math.floor(g); + aData[px + 2] = (kb > pb) ? Math.ceil(b) : Math.floor(b); + + if (!preserveAlpha) { + aData[px + 3] = Math.ceil(a * 255); + } + } + } + } + } + + generateShapeCache(force) { + var i, shape; + + if (!this.shapeCache) { + this.shapeCache = new Array(Tegaki.maxSize); + } + + if (this.shapeCache[0] && !force) { + return; + } + + for (i = 0; i < Tegaki.maxSize; ++i) { + shape = this.generateShape(i + 1); + this.shapeCache[i] = shape; + this.setShape(shape); + } + } + + updateDynamics(t) { + var pressure, shape, val; + + pressure = TegakiPressure.lerp(t); + + if (this.sizeDynamicsEnabled) { + val = Math.ceil(pressure * this.size); + + if (val === 0) { + return false; + } + + shape = this.shapeCache[val - 1]; + + this.setShape(shape); + } + + if (this.alphaDynamicsEnabled) { + val = this.alpha * pressure; + + if (val <= 0) { + return false; + } + + this.brushAlpha = val; + } + + if (this.flowDynamicsEnabled) { + val = this.flow * pressure; + + if (val <= 0) { + return false; + } + + this.brushFlow = this.easeFlow(val); + } + + return true; + } + + start(posX, posY) { + var sampleX, sampleY; + + this.stepAcc = 0; + this.posX = posX; + this.posY = posY; + + if (this.enabledDynamics()) { + if (!this.updateDynamics(1.0)) { + return; + } + } + + sampleX = posX - this.center; + sampleY = posY - this.center; + + this.readImageData(sampleX, sampleY, this.brushSize, this.brushSize); + + this.brushFn(0, 0, sampleX, sampleY); + + this.writeImageData(sampleX, sampleY, this.brushSize, this.brushSize); + } + + commit() { + Tegaki.clearBuffers(); + } + + draw(posX, posY) { + var mx, my, fromX, fromY, sampleX, sampleY, dx, dy, err, derr, stepAcc, + lastX, lastY, distBase, shape, center, brushSize, t, tainted, w, h; + + stepAcc = this.stepAcc; + + fromX = this.posX; + fromY = this.posY; + + if (fromX < posX) { dx = posX - fromX; sampleX = fromX; mx = 1; } + else { dx = fromX - posX; sampleX = posX; mx = -1; } + + if (fromY < posY) { dy = posY - fromY; sampleY = fromY; my = 1; } + else { dy = fromY - posY; sampleY = posY; my = -1; } + + if (this.enabledDynamics()) { + distBase = Math.sqrt((posX - fromX) * (posX - fromX) + (posY - fromY) * (posY - fromY)); + } + + if (this.sizeDynamicsEnabled) { + shape = this.shapeCache[this.size - 1]; + center = shape.center; + brushSize = shape.brushSize; + } + else { + center = this.center; + brushSize = this.brushSize; + } + + sampleX -= center; + sampleY -= center; + + w = dx + brushSize; + h = dy + brushSize; + + this.readImageData(sampleX, sampleY, w, h); + + err = (dx > dy ? dx : (dy !== 0 ? -dy : 0)) / 2; + + if (dx !== 0) { + dx = -dx; + } + + tainted = false; + + lastX = fromX; + lastY = fromY; + + while (true) { + stepAcc += Math.max(Math.abs(lastX - fromX), Math.abs(lastY - fromY)); + + lastX = fromX; + lastY = fromY; + + if (stepAcc >= this.stepSize) { + if (this.enabledDynamics()) { + if (distBase > 0) { + t = 1.0 - (Math.sqrt((posX - fromX) * (posX - fromX) + (posY - fromY) * (posY - fromY)) / distBase); + } + else { + t = 0.0; + } + + if (this.updateDynamics(t)) { + this.brushFn(fromX - this.center - sampleX, fromY - this.center - sampleY, sampleX, sampleY); + tainted = true; + } + } + else { + this.brushFn(fromX - this.center - sampleX, fromY - this.center - sampleY, sampleX, sampleY); + tainted = true; + } + + stepAcc = 0; + } + + if (fromX === posX && fromY === posY) { + break; + } + + derr = err; + + if (derr > dx) { err -= dy; fromX += mx; } + if (derr < dy) { err -= dx; fromY += my; } + } + + this.stepAcc = stepAcc; + this.posX = posX; + this.posY = posY; + + if (tainted) { + this.writeImageData(sampleX, sampleY, w, h); + } + } + + writeImageData(x, y, w, h) { + Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData, 0, 0, x, y, w, h); + } + + readImageData(x, y, w, h) {} + + setShape(shape) { + this.center = shape.center; + this.stepSize = shape.stepSize; + this.brushSize = shape.brushSize; + this.kernel = shape.kernel; + } + + setSize(size) { + this.size = size; + + if (this.sizeDynamicsEnabled) { + this.generateShapeCache(); + } + else { + this.setShape(this.generateShape(size)); + } + } + + setSizeDynamics(flag) { + if (!this.useSizeDynamics) { + return; + } + + if (this.sizeDynamicsEnabled === flag) { + return; + } + + if (flag) { + this.generateShapeCache(); + } + else { + this.setShape(this.generateShape(this.size)); + } + + this.sizeDynamicsEnabled = flag; + } + + setTip(tipId) { + this.tipId = tipId; + + if (this.sizeDynamicsEnabled) { + this.generateShapeCache(true); + } + else { + this.setShape(this.generateShape(this.size)); + } + } +} +class TegakiPencil extends TegakiBrush { + constructor() { + super(); + + this.id = 1; + + this.name = 'pencil'; + + this.keybind = 'b'; + + this.step = 0.01; + + this.useFlow = false; + + this.size = 1; + this.alpha = 1.0; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.usePreserveAlpha = true; + } + + generateShape(size) { + var e, x, y, imageData, data, c, color, r, rr; + + r = 0 | ((size) / 2); + + rr = 0 | ((size + 1) % 2); + + imageData = new ImageData(size, size); + + data = new Uint32Array(imageData.data.buffer); + + color = 0xFF000000; + + x = r; + y = 0; + e = 1 - r; + c = r; + + while (x >= y) { + data[c + x - rr + (c + y - rr) * size] = color; + data[c + y - rr + (c + x - rr) * size] = color; + + data[c - y + (c + x - rr) * size] = color; + data[c - x + (c + y - rr) * size] = color; + + data[c - y + (c - x) * size] = color; + data[c - x + (c - y) * size] = color; + + data[c + y - rr + (c - x) * size] = color; + data[c + x - rr + (c - y) * size] = color; + + ++y; + + if (e <= 0) { + e += 2 * y + 1; + } + else { + x--; + e += 2 * (y - x) + 1; + } + } + + if (r > 0) { + Tegaki.tools.bucket.fill(imageData, r, r, this.rgb, 1.0); + } + + return { + center: r, + stepSize: Math.ceil(size * this.step), + brushSize: size, + kernel: imageData.data, + }; + } +} +class TegakiAirbrush extends TegakiBrush { + constructor() { + super(); + + this.id = 3; + + this.name = 'airbrush'; + + this.keybind = 'a'; + + this.step = 0.1; + + this.size = 32; + this.alpha = 1.0; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.useFlowDynamics = true; + + this.usePreserveAlpha = true; + } + + easeFlow(flow) { + return 1 - Math.sqrt(1 - flow); + } + + generateShape(size) { + var i, r, data, len, sqd, sqlen, hs, col, row, + ecol, erow, a; + + r = size; + size = size * 2; + + data = new ImageData(size, size).data; + + len = size * size * 4; + sqlen = Math.sqrt(r * r); + hs = Math.round(r); + col = row = -hs; + + i = 0; + while (i < len) { + if (col >= hs) { + col = -hs; + ++row; + continue; + } + + ecol = col; + erow = row; + + if (ecol < 0) { ecol = -ecol; } + if (erow < 0) { erow = -erow; } + + sqd = Math.sqrt(ecol * ecol + erow * erow); + + if (sqd >= sqlen) { + a = 0; + } + else if (sqd === 0) { + a = 255; + } + else { + a = (sqd / sqlen) + 0.1; + + if (a > 1.0) { + a = 1.0; + } + + a = (1 - (Math.exp(1 - 1 / a) / a)) * 255; + } + + data[i + 3] = a; + + i += 4; + + ++col; + } + + return { + center: r, + stepSize: Math.ceil(size * this.step), + brushSize: size, + kernel: data, + }; + } +} +class TegakiPen extends TegakiBrush { + constructor() { + super(); + + this.id = 2; + + this.name = 'pen'; + + this.keybind = 'p'; + + this.step = 0.05; + + this.size = 8; + this.alpha = 1.0; + this.flow = 1.0; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.useFlowDynamics = true; + + this.usePreserveAlpha = true; + } + + easeFlow(flow) { + return 1 - Math.sqrt(1 - Math.pow(flow, 3)); + } + + generateShape(size) { + var e, x, y, imageData, data, c, color, r, rr, + f, ff, bSize, bData, i, ii, xx, yy, center, brushSize; + + center = Math.floor(size / 2) + 1; + + brushSize = size + 2; + + f = 4; + + ff = f * f; + + bSize = brushSize * f; + + r = Math.floor(bSize / 2); + + rr = Math.floor((bSize + 1) % 2); + + imageData = new ImageData(bSize, bSize); + bData = new Uint32Array(imageData.data.buffer); + + color = 0x55000000; + + x = r; + y = 0; + e = 1 - r; + c = r; + + while (x >= y) { + bData[c + x - rr + (c + y - rr) * bSize] = color; + bData[c + y - rr + (c + x - rr) * bSize] = color; + + bData[c - y + (c + x - rr) * bSize] = color; + bData[c - x + (c + y - rr) * bSize] = color; + + bData[c - y + (c - x) * bSize] = color; + bData[c - x + (c - y) * bSize] = color; + + bData[c + y - rr + (c - x) * bSize] = color; + bData[c + x - rr + (c - y) * bSize] = color; + + ++y; + + if (e <= 0) { + e += 2 * y + 1; + } + else { + x--; + e += 2 * (y - x) + 1; + } + } + + color = 0xFF000000; + + x = r - 3; + y = 0; + e = 1 - r; + c = r; + + while (x >= y) { + bData[c + x - rr + (c + y - rr) * bSize] = color; + bData[c + y - rr + (c + x - rr) * bSize] = color; + + bData[c - y + (c + x - rr) * bSize] = color; + bData[c - x + (c + y - rr) * bSize] = color; + + bData[c - y + (c - x) * bSize] = color; + bData[c - x + (c - y) * bSize] = color; + + bData[c + y - rr + (c - x) * bSize] = color; + bData[c + x - rr + (c - y) * bSize] = color; + + ++y; + + if (e <= 0) { + e += 2 * y + 1; + } + else { + x--; + e += 2 * (y - x) + 1; + } + } + + if (r > 0) { + Tegaki.tools.bucket.fill(imageData, r, r, this.rgb, 1.0); + } + + bData = imageData.data; + data = new ImageData(brushSize, brushSize).data; + + for (x = 0; x < brushSize; ++x) { + for (y = 0; y < brushSize; ++y) { + i = (y * brushSize + x) * 4 + 3; + + color = 0; + + for (xx = 0; xx < f; ++xx) { + for (yy = 0; yy < f; ++yy) { + ii = ((yy + y * f) * bSize + (xx + x * f)) * 4 + 3; + color += bData[ii]; + } + } + + data[i] = color / ff; + } + } + + return { + center: center, + stepSize: Math.ceil(size * this.step), + brushSize: brushSize, + kernel: data, + }; + } +} +class TegakiBucket extends TegakiTool { + constructor() { + super(); + + this.id = 4; + + this.name = 'bucket'; + + this.keybind = 'g'; + + this.step = 100.0; + + this.useSize = false; + this.useFlow = false; + + this.noCursor = true; + } + + fill(imageData, x, y, color, alpha) { + var r, g, b, px, tr, tg, tb, ta, q, pxMap, yy, xx, yn, ys, + yyy, yyn, yys, xd, data, w, h; + + w = imageData.width; + h = imageData.height; + + r = color[0]; + g = color[1]; + b = color[2]; + + px = (y * w + x) * 4; + + data = imageData.data; + + tr = data[px]; + tg = data[px + 1]; + tb = data[px + 2]; + ta = data[px + 3]; + + pxMap = new Uint8Array(w * h * 4); + + q = []; + + q[0] = x; + q[1] = y; + + while (q.length) { + yy = q.pop(); + xx = q.pop(); + + yn = (yy - 1); + ys = (yy + 1); + + yyy = yy * w; + yyn = yn * w; + yys = ys * w; + + xd = xx; + + while (xd >= 0) { + px = (yyy + xd) * 4; + + if (!this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + break; + } + + this.blendPixel(data, px, r, g, b, alpha); + + pxMap[px] = 1; + + if (yn >= 0) { + px = (yyn + xd) * 4; + + if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + q.push(xd); + q.push(yn); + } + } + + if (ys < h) { + px = (yys + xd) * 4; + + if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + q.push(xd); + q.push(ys); + } + } + + xd--; + } + + xd = xx + 1; + + while (xd < w) { + px = (yyy + xd) * 4; + + if (!this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + break; + } + + this.blendPixel(data, px, r, g, b, alpha); + + pxMap[px] = 1; + + if (yn >= 0) { + px = (yyn + xd) * 4; + + if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + q.push(xd); + q.push(yn); + } + } + + if (ys < h) { + px = (yys + xd) * 4; + + if (this.testPixel(data, px, pxMap, tr, tg, tb, ta)) { + q.push(xd); + q.push(ys); + } + } + + ++xd; + } + } + } + + brushFn(x, y) { + if (x < 0 || y < 0 || x >= Tegaki.baseWidth || y >= Tegaki.baseHeight) { + return; + } + + this.fill(Tegaki.activeLayer.imageData, x, y, this.rgb, this.alpha); + + // TODO: write back only the tainted rect + Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData, 0, 0); + } + + blendPixel(data, px, r, g, b, a) { + var sr, sg, sb, sa, dr, dg, db, da; + + sr = data[px]; + sg = data[px + 1]; + sb = data[px + 2]; + sa = data[px + 3] / 255; + + da = sa + a - sa * a; + + dr = ((r * a) + (sr * sa) * (1 - a)) / da; + dg = ((g * a) + (sg * sa) * (1 - a)) / da; + db = ((b * a) + (sb * sa) * (1 - a)) / da; + + data[px] = (r > sr) ? Math.ceil(dr) : Math.floor(dr); + data[px + 1] = (g > sg) ? Math.ceil(dg) : Math.floor(dg); + data[px + 2] = (b > sb) ? Math.ceil(db) : Math.floor(db); + data[px + 3] = Math.ceil(da * 255); + } + + testPixel(data, px, pxMap, tr, tg, tb, ta) { + return !pxMap[px] && (data[px] == tr + && data[++px] == tg + && data[++px] == tb + && data[++px] == ta) + ; + } + + start(x, y) { + this.brushFn(x, y); + } + + draw(x, y) { + this.brushFn(x, y); + } + + setSize(size) {} +} +class TegakiTone extends TegakiPencil { + constructor() { + super(); + + this.id = 5; + + this.name = 'tone'; + + this.keybind = 't'; + + this.step = 0.01; + + this.useFlow = false; + + this.size = 8; + this.alpha = 0.5; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.usePreserveAlpha = true; + + this.matrix = [ + [0, 8, 2, 10], + [12, 4, 14, 6], + [3, 11, 1 ,9], + [15, 7, 13, 5] + ]; + + this.mapCache = null; + this.mapWidth = 0; + this.mapHeight = 0; + } + + start(x, y) { + if (this.mapWidth !== Tegaki.baseWidth || this.mapHeight !== Tegaki.baseHeight) { + this.generateMapCache(true); + } + + super.start(x, y); + } + + brushFn(x, y, offsetX, offsetY) { + var data, kernel, brushSize, map, idx, preserveAlpha, + px, mx, mapWidth, xx, yy, ix, iy, canvasWidth, canvasHeight; + + data = Tegaki.activeLayer.imageData.data; + + canvasWidth = Tegaki.baseWidth; + canvasHeight = Tegaki.baseHeight; + + kernel = this.kernel; + + brushSize = this.brushSize; + + mapWidth = this.mapWidth; + + preserveAlpha = this.preserveAlphaEnabled; + + idx = Math.round(this.brushAlpha * 16) - 1; + + if (idx < 0) { + return; + } + + map = this.mapCache[idx]; + + for (yy = 0; yy < brushSize; ++yy) { + iy = y + yy + offsetY; + + if (iy < 0 || iy >= canvasHeight) { + continue; + } + + for (xx = 0; xx < brushSize; ++xx) { + ix = x + xx + offsetX; + + if (ix < 0 || ix >= canvasWidth) { + continue; + } + + if (kernel[(yy * brushSize + xx) * 4 + 3] === 0) { + continue; + } + + mx = iy * canvasWidth + ix; + px = mx * 4; + + if (map[mx] === 0) { + data[px] = this.rgb[0]; + data[px + 1] = this.rgb[1]; + data[px + 2] = this.rgb[2]; + + if (!preserveAlpha) { + data[px + 3] = 255; + } + } + } + } + } + + generateMap(w, h, idx) { + var data, x, y; + + data = new Uint8Array(w * h); + + for (y = 0; y < h; ++y) { + for (x = 0; x < w; ++x) { + if (idx < this.matrix[y % 4][x % 4]) { + data[w * y + x] = 1; + } + } + } + + return data; + } + + generateMapCache(force) { + var i, cacheSize; + + cacheSize = this.matrix.length * this.matrix[0].length; + + if (!this.mapCache) { + this.mapCache = new Array(cacheSize); + } + + if (!force && this.mapCache[0] + && this.mapWidth === Tegaki.baseWidth + && this.mapHeight === Tegaki.baseHeight) { + return; + } + + this.mapWidth = Tegaki.baseWidth; + this.mapHeight = Tegaki.baseHeight; + + for (i = 0; i < cacheSize; ++i) { + this.mapCache[i] = this.generateMap(this.mapWidth, this.mapHeight, i); + } + } + + setAlpha(alpha) { + super.setAlpha(alpha); + this.generateMapCache(); + } +} +class TegakiPipette extends TegakiTool { + constructor() { + super(); + + this.id = 6; + + this.name = 'pipette'; + + this.keybind = 'i'; + + this.step = 100.0; + + this.useSize = false; + this.useAlpha = false; + this.useFlow = false; + + this.noCursor = true; + } + + start(posX, posY) { + this.draw(posX, posY); + } + + draw(posX, posY) { + var c, ctx; + + if (true) { + ctx = Tegaki.flatten().getContext('2d'); + } + else { + ctx = Tegaki.activeLayer.ctx; + } + + c = $T.getColorAt(ctx, posX, posY); + + Tegaki.setToolColor(c); + } + + set() { + Tegaki.onToolChanged(this); + } + + commit() {} + + setSize() {} + + setAlpha() {} +} +class TegakiBlur extends TegakiBrush { + constructor() { + super(); + + this.id = 7; + + this.name = 'blur'; + + this.step = 0.25; + + this.useFlow = false; + + this.size = 32; + this.alpha = 0.5; + + this.useAlphaDynamics = true; + this.usePreserveAlpha = false; + } + + writeImageData(x, y, w, h) { + var xx, yy, ix, iy, px, canvasWidth, aData, bData; + + aData = Tegaki.activeLayer.imageData.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + + for (xx = 0; xx < w; ++xx) { + ix = x + xx; + + for (yy = 0; yy < h; ++yy) { + iy = y + yy; + + px = (iy * canvasWidth + ix) * 4; + + aData[px] = bData[px]; + aData[px + 1] = bData[px + 1]; + aData[px + 2] = bData[px + 2]; + aData[px + 3] = bData[px + 3]; + } + } + + super.writeImageData(x, y, w, h); + } + + readImageData(x, y, w, h) { + var xx, yy, ix, iy, px, canvasWidth, aData, bData; + + aData = Tegaki.activeLayer.imageData.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + + for (xx = 0; xx < w; ++xx) { + ix = x + xx; + + for (yy = 0; yy < h; ++yy) { + iy = y + yy; + + px = (iy * canvasWidth + ix) * 4; + + bData[px] = aData[px]; + bData[px + 1] = aData[px + 1]; + bData[px + 2] = aData[px + 2]; + bData[px + 3] = aData[px + 3]; + } + } + } + + brushFn(x, y, offsetX, offsetY) { + var i, j, size, aData, bData, limX, limY, + kernel, alpha, alpha0, ix, iy, canvasWidth, canvasHeight, + sx, sy, r, g, b, a, kx, ky, px, pa, acc, aa; + + alpha0 = this.brushAlpha; + alpha = alpha0 * alpha0 * alpha0; + + if (alpha <= 0.0) { + return; + } + + size = this.brushSize; + + kernel = this.kernel; + + aData = Tegaki.activeLayer.imageData.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + canvasHeight = Tegaki.baseHeight; + + limX = canvasWidth - 1; + limY = canvasHeight - 1; + + for (sx = 0; sx < size; ++sx) { + ix = x + sx + offsetX; + + if (ix < 0 || ix >= canvasWidth) { + continue; + } + + for (sy = 0; sy < size; ++sy) { + iy = y + sy + offsetY; + + if (iy < 0 || iy >= canvasHeight) { + continue; + } + + i = (sy * size + sx) * 4; + + px = (iy * canvasWidth + ix) * 4; + + if (kernel[i + 3] === 0 || ix <= 0 || iy <= 0 || ix >= limX || iy >= limY) { + continue; + } + + r = g = b = a = acc = 0; + + for (kx = -1; kx < 2; ++kx) { + for (ky = -1; ky < 2; ++ky) { + j = ((iy - ky) * canvasWidth + (ix - kx)) * 4; + + pa = aData[j + 3]; + + if (kx === 0 && ky === 0) { + aa = pa * alpha0; + acc += alpha0; + } + else { + aa = pa * alpha; + acc += alpha; + } + + r = r + aData[j] * aa; ++j; + g = g + aData[j] * aa; ++j; + b = b + aData[j] * aa; + a = a + aa; + } + } + + a = a / acc; + + if (a <= 0.0) { + continue; + } + + bData[px] = Math.round((r / acc) / a); + bData[px + 1] = Math.round((g / acc) / a); + bData[px + 2] = Math.round((b / acc) / a); + bData[px + 3] = Math.round(a); + } + } + } +} + +TegakiBlur.prototype.generateShape = TegakiPencil.prototype.generateShape; +class TegakiEraser extends TegakiBrush { + constructor() { + super(); + + this.id = 8; + + this.name = 'eraser'; + + this.keybind = 'e'; + + this.step = 0.1; + + this.size = 8; + this.alpha = 1.0; + + this.useFlow = false; + + this.useSizeDynamics = true; + this.useAlphaDynamics = true; + this.usePreserveAlpha = false; + + this.tipId = 0; + this.tipList = [ 'pencil', 'pen', 'airbrush' ]; + } + + brushFn(x, y, offsetX, offsetY) { + var aData, bData, gData, kernel, canvasWidth, canvasHeight, + ka, ba, px, xx, yy, ix, iy, + brushSize, brushAlpha; + + brushAlpha = this.brushAlpha; + brushSize = this.brushSize; + + kernel = this.kernel; + + aData = Tegaki.activeLayer.imageData.data; + gData = Tegaki.ghostBuffer.data; + bData = Tegaki.blendBuffer.data; + + canvasWidth = Tegaki.baseWidth; + canvasHeight = Tegaki.baseHeight; + + for (yy = 0; yy < brushSize; ++yy) { + iy = y + yy + offsetY; + + if (iy < 0 || iy >= canvasHeight) { + continue; + } + + for (xx = 0; xx < brushSize; ++xx) { + ix = x + xx + offsetX; + + if (ix < 0 || ix >= canvasWidth) { + continue; + } + + ka = kernel[(yy * brushSize + xx) * 4 + 3] / 255; + + px = (iy * canvasWidth + ix) * 4 + 3; + + if (gData[px] === 0) { + gData[px] = aData[px]; + } + + ba = bData[px] / 255; + ba = ba + ka * (brushAlpha - ba); + + bData[px] = Math.floor(ba * 255); + aData[px] = Math.floor(gData[px] * (1 - ba)); + } + } + } + + generateShape(size) { + if (this.tipId === 0) { + return this.generateShapePencil(size); + } + else if (this.tipId === 1) { + return this.generateShapePen(size); + } + else { + return this.generateShapeAirbrush(size); + } + } +} + +TegakiEraser.prototype.generateShapePencil = TegakiPencil.prototype.generateShape; +TegakiEraser.prototype.generateShapePen = TegakiPen.prototype.generateShape; +TegakiEraser.prototype.generateShapeAirbrush = TegakiAirbrush.prototype.generateShape; +var $T = { + docEl: document.documentElement, + + id: function(id) { + return document.getElementById(id); + }, + + cls: function(klass, root) { + return (root || document).getElementsByClassName(klass); + }, + + on: function(o, e, h) { + o.addEventListener(e, h, false); + }, + + off: function(o, e, h) { + o.removeEventListener(e, h, false); + }, + + el: function(name) { + return document.createElement(name); + }, + + frag: function() { + return document.createDocumentFragment(); + }, + + copyImageData(imageData) { + return new ImageData( + new Uint8ClampedArray(imageData.data), + imageData.width + ); + }, + + copyCanvas: function(source, clone) { + var canvas; + + if (!clone) { + canvas = $T.el('canvas'); + canvas.width = source.width; + canvas.height = source.height; + } + else { + canvas = source.cloneNode(false); + } + + canvas.getContext('2d').drawImage(source, 0, 0); + + return canvas; + }, + + clearCtx: function(ctx) { + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + }, + + hexToRgb: function(hex) { + var c = hex.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i); + + if (c) { + return [ + parseInt(c[1], 16), + parseInt(c[2], 16), + parseInt(c[3], 16) + ]; + } + + return null; + }, + + RgbToHex: function(r, g, b) { + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + }, + + getColorAt: function(ctx, posX, posY) { + var rgba = ctx.getImageData(posX, posY, 1, 1).data; + + return '#' + + ('0' + rgba[0].toString(16)).slice(-2) + + ('0' + rgba[1].toString(16)).slice(-2) + + ('0' + rgba[2].toString(16)).slice(-2); + }, + + generateFilename: function() { + return 'tegaki_' + (new Date()).toISOString().split('.')[0].replace(/[^0-9]/g, '_'); + }, + + sortAscCb: function(a, b) { + if (a > b) { return 1; } + if (a < b) { return -1; } + return 0; + }, + + sortDescCb: function(a, b) { + if (a > b) { return -1; } + if (a < b) { return 1; } + return 0; + }, + + msToHms: function(ms) { + var h, m, s, ary; + + s = 0 | (ms / 1000); + h = 0 | (s / 3600); + m = 0 | ((s - h * 3600) / 60); + s = s - h * 3600 - m * 60; + + ary = []; + + if (h) { + ary.push(h < 10 ? ('0' + h) : h); + } + + if (m) { + ary.push(m < 10 ? ('0' + m) : m); + } + else { + ary.push('00'); + } + + if (s) { + ary.push(s < 10 ? ('0' + s) : s); + } + else { + ary.push('00'); + } + + return ary.join(':'); + }, + + calcThumbSize(w, h, maxSide) { + var r; + + if (w > maxSide) { + r = maxSide / w; + w = maxSide; + h = h * r; + } + + if (h > maxSide) { + r = maxSide / h; + h = maxSide; + w = w * r; + } + + return [Math.ceil(w), Math.ceil(h)]; + } +}; +class TegakiBinReader { + constructor(buf) { + this.pos = 0; + this.view = new DataView(buf); + this.buf = buf; + } + + readInt8() { + var data = this.view.getInt8(this.pos); + this.pos += 1; + return data; + } + + readUint8() { + var data = this.view.getUint8(this.pos); + this.pos += 1; + return data; + } + + readInt16() { + var data = this.view.getInt16(this.pos); + this.pos += 2; + return data; + } + + readUint16() { + var data = this.view.getUint16(this.pos); + this.pos += 2; + return data; + } + + readUint32() { + var data = this.view.getUint32(this.pos); + this.pos += 4; + return data; + } + + readFloat32() { + var data = this.view.getFloat32(this.pos); + this.pos += 4; + return data; + } +} + +class TegakiBinWriter { + constructor(buf) { + this.pos = 0; + this.view = new DataView(buf); + this.buf = buf; + } + + writeInt8(val) { + this.view.setInt8(this.pos, val); + this.pos += 1; + } + + writeUint8(val) { + this.view.setUint8(this.pos, val); + this.pos += 1; + } + + writeInt16(val) { + this.view.setInt16(this.pos, val); + this.pos += 2; + } + + writeUint16(val) { + this.view.setUint16(this.pos, val); + this.pos += 2; + } + + writeUint32(val) { + this.view.setUint32(this.pos, val); + this.pos += 4; + } + + writeFloat32(val) { + this.view.setFloat32(this.pos, val); + this.pos += 4; + } +} +var TegakiCursor = { + size: 0, + radius: 0, + + points: null, + + tmpCtx: null, + + cursorCtx: null, + + flatCtxAbove: null, + flatCtxBelow: null, + + cached: false, + + init: function(w, h) { + var el; + + this.tmpCtx = $T.el('canvas').getContext('2d'); + + el = $T.el('canvas'); + el.id = 'tegaki-cursor-layer'; + el.width = w; + el.height = h; + Tegaki.layersCnt.appendChild(el); + + this.cursorCtx = el.getContext('2d'); + + el = $T.el('canvas'); + el.width = w; + el.height = h; + this.flatCtxAbove = el.getContext('2d'); + + el = $T.el('canvas'); + el.width = w; + el.height = h; + this.flatCtxBelow = el.getContext('2d'); + }, + + updateCanvasSize: function(w, h) { + this.cursorCtx.canvas.width = w; + this.cursorCtx.canvas.height = h; + + this.flatCtxAbove.canvas.width = w; + this.flatCtxAbove.canvas.height = h; + + this.flatCtxBelow.canvas.width = w; + this.flatCtxBelow.canvas.height = h; + }, + + render: function(x, y) { + var i, size, srcImg, srcData, destImg, destData, activeLayer; + + if (!this.cached) { + this.buildCache(); + } + + size = this.size; + x = x - this.radius; + y = y - this.radius; + + $T.clearCtx(this.cursorCtx); + $T.clearCtx(this.tmpCtx); + + this.tmpCtx.drawImage(this.flatCtxBelow.canvas, x, y, size, size, 0, 0, size, size); + + activeLayer = Tegaki.activeLayer; + + if (activeLayer.visible) { + if (activeLayer.alpha < 1.0) { + this.tmpCtx.globalAlpha = activeLayer.alpha; + this.tmpCtx.drawImage(Tegaki.activeLayer.canvas, x, y, size, size, 0, 0, size, size); + this.tmpCtx.globalAlpha = 1.0; + } + else { + this.tmpCtx.drawImage(Tegaki.activeLayer.canvas, x, y, size, size, 0, 0, size, size); + } + } + + this.tmpCtx.drawImage(this.flatCtxAbove.canvas, x, y, size, size, 0, 0, size, size); + + srcImg = this.tmpCtx.getImageData(0, 0, size, size); + srcData = new Uint32Array(srcImg.data.buffer); + + destImg = this.cursorCtx.createImageData(size, size); + destData = new Uint32Array(destImg.data.buffer); + + for (i of this.points) { + destData[i] = srcData[i] ^ 0x00FFFF7F; + } + + this.cursorCtx.putImageData(destImg, x, y); + }, + + buildCache: function() { + var i, layer, ctx, len, layerId; + + ctx = this.flatCtxBelow; + ctx.globalAlpha = 1.0; + $T.clearCtx(ctx); + + ctx.drawImage(Tegaki.canvas, 0, 0); + + layerId = Tegaki.activeLayer.id; + + for (i = 0, len = Tegaki.layers.length; i < len; ++i) { + layer = Tegaki.layers[i]; + + if (!layer.visible) { + continue; + } + + if (layer.id === layerId) { + ctx = this.flatCtxAbove; + ctx.globalAlpha = 1.0; + $T.clearCtx(ctx); + continue; + } + + ctx.globalAlpha = layer.alpha; + ctx.drawImage(layer.canvas, 0, 0); + } + + this.cached = true; + }, + + invalidateCache() { + this.cached = false; + }, + + destroy() { + this.size = 0; + this.radius = 0; + this.points = null; + this.tmpCtx = null; + this.cursorCtx = null; + this.flatCtxAbove = null; + this.flatCtxBelow = null; + }, + + generate: function(size) { + var e, x, y, c, r, rr, points; + + r = 0 | ((size) / 2); + + rr = 0 | ((size + 1) % 2); + + points = []; + + x = r; + y = 0; + e = 1 - r; + c = r; + + while (x >= y) { + points.push(c + x - rr + (c + y - rr) * size); + points.push(c + y - rr + (c + x - rr) * size); + + points.push(c - y + (c + x - rr) * size); + points.push(c - x + (c + y - rr) * size); + + points.push(c - y + (c - x) * size); + points.push(c - x + (c - y) * size); + + points.push(c + y - rr + (c - x) * size); + points.push(c + x - rr + (c - y) * size); + + ++y; + + if (e <= 0) { + e += 2 * y + 1; + } + else { + x--; + e += 2 * (y - x) + 1; + } + } + + this.tmpCtx.canvas.width = size; + this.tmpCtx.canvas.height = size; + + this.size = size; + this.radius = r; + this.points = points; + } +}; +var TegakiHistory = { + maxSize: 50, + + undoStack: [], + redoStack: [], + + pendingAction: null, + + clear: function() { + this.undoStack = []; + this.redoStack = []; + this.pendingAction = null; + + this.onChange(0); + }, + + push: function(action) { + if (!action) { + return; + } + + if (action.coalesce) { + if (this.undoStack[this.undoStack.length - 1] instanceof action.constructor) { + if (this.undoStack[this.undoStack.length - 1].coalesce(action)) { + return; + } + } + } + + this.undoStack.push(action); + + if (this.undoStack.length > this.maxSize) { + this.undoStack.shift(); + } + + if (this.redoStack.length > 0) { + this.redoStack = []; + } + + this.onChange(0); + }, + + undo: function() { + var action; + + if (!this.undoStack.length) { + return; + } + + action = this.undoStack.pop(); + + action.undo(); + + this.redoStack.push(action); + + this.onChange(-1); + }, + + redo: function() { + var action; + + if (!this.redoStack.length) { + return; + } + + action = this.redoStack.pop(); + + action.redo(); + + this.undoStack.push(action); + + this.onChange(1); + }, + + onChange: function(type) { + Tegaki.onHistoryChange(this.undoStack.length, this.redoStack.length, type); + }, + + sortPosLayerCallback: function(a, b) { + if (a[0] > b[0]) { return 1; } + if (a[0] < b[0]) { return -1; } + return 0; + } +}; + +var TegakiHistoryActions = { + Dummy: function() { + this.undo = function() {}; + this.redo = function() {}; + }, + + Draw: function(layerId) { + this.coalesce = false; + + this.imageDataBefore = null; + this.imageDataAfter = null; + this.layerId = layerId; + }, + + DeleteLayers: function(layerPosMap, params) { + var item; + + this.coalesce = false; + + this.layerPosMap = []; + + for (item of layerPosMap.sort(TegakiHistory.sortPosLayerCallback)) { + item[1] = TegakiLayers.cloneLayer(item[1]); + this.layerPosMap.push(item); + } + + this.tgtLayerId = null; + + this.aLayerIdBefore = null; + this.aLayerIdAfter = null; + + this.imageDataBefore = null; + this.imageDataAfter = null; + + this.mergeDown = false; + + if (params) { + for (let k in params) { + this[k] = params[k]; + } + } + }, + + AddLayer: function(params, aLayerIdBefore, aLayerIdAfter) { + this.coalesce = false; + + this.layer = params; + this.layerId = params.id; + this.aLayerIdBefore = aLayerIdBefore; + this.aLayerIdAfter = aLayerIdAfter; + }, + + MoveLayers: function(layers, belowPos, activeLayerId) { + this.coalesce = false; + + this.layers = layers; + this.belowPos = belowPos; + this.aLayerId = activeLayerId; + }, + + SetLayersAlpha: function(layerAlphas, newAlpha) { + this.layerAlphas = layerAlphas; + this.newAlpha = newAlpha; + }, + + SetLayerName: function(id, oldName, newName) { + this.layerId = id; + this.oldName = oldName; + this.newName = newName; + } +}; + +// --- + +TegakiHistoryActions.Draw.prototype.addCanvasState = function(imageData, type) { + if (type) { + this.imageDataAfter = $T.copyImageData(imageData); + } + else { + this.imageDataBefore = $T.copyImageData(imageData); + } +}; + +TegakiHistoryActions.Draw.prototype.exec = function(type) { + var layer = TegakiLayers.getLayerById(this.layerId); + + if (type) { + layer.ctx.putImageData(this.imageDataAfter, 0, 0); + TegakiLayers.syncLayerImageData(layer, this.imageDataAfter); + } + else { + layer.ctx.putImageData(this.imageDataBefore, 0, 0); + TegakiLayers.syncLayerImageData(layer, this.imageDataBefore); + } + + TegakiUI.updateLayerPreview(layer); + TegakiLayers.setActiveLayer(this.layerId); +}; + +TegakiHistoryActions.Draw.prototype.undo = function() { + this.exec(0); +}; + +TegakiHistoryActions.Draw.prototype.redo = function() { + this.exec(1); +}; + +TegakiHistoryActions.DeleteLayers.prototype.undo = function() { + var i, lim, refLayer, layer, pos, refId; + + for (i = 0, lim = this.layerPosMap.length; i < lim; ++i) { + [pos, layer] = this.layerPosMap[i]; + + layer = TegakiLayers.cloneLayer(layer); + + refLayer = Tegaki.layers[pos]; + + if (refLayer) { + if (refId = TegakiLayers.getLayerBelowId(refLayer.id)) { + refId = refId.id; + } + + TegakiUI.updateLayersGridAdd(layer, refId); + TegakiUI.updateLayerPreview(layer); + Tegaki.layersCnt.insertBefore(layer.canvas, refLayer.canvas); + Tegaki.layers.splice(pos, 0, layer); + } + else { + + if (!Tegaki.layers[0]) { + refLayer = Tegaki.layersCnt.children[0]; + } + else { + refLayer = Tegaki.layers[Tegaki.layers.length - 1].canvas; + } + + TegakiUI.updateLayersGridAdd(layer, TegakiLayers.getTopLayerId()); + TegakiUI.updateLayerPreview(layer); + Tegaki.layersCnt.insertBefore(layer.canvas, refLayer.nextElementSibling); + Tegaki.layers.push(layer); + } + } + + if (this.tgtLayerId) { + layer = TegakiLayers.getLayerById(this.tgtLayerId); + layer.ctx.putImageData(this.imageDataBefore, 0, 0); + TegakiLayers.syncLayerImageData(layer, this.imageDataBefore); + TegakiLayers.setLayerAlpha(layer, this.tgtLayerAlpha); + TegakiUI.updateLayerPreview(layer); + } + + TegakiLayers.setActiveLayer(this.aLayerIdBefore); +}; + +TegakiHistoryActions.DeleteLayers.prototype.redo = function() { + var layer, ids = []; + + for (layer of this.layerPosMap) { + ids.unshift(layer[1].id); + } + + if (this.tgtLayerId) { + if (!this.mergeDown) { + ids.unshift(this.tgtLayerId); + } + TegakiLayers.mergeLayers(new Set(ids)); + } + else { + TegakiLayers.deleteLayers(ids); + } + + TegakiLayers.setActiveLayer(this.aLayerIdAfter); +}; + +TegakiHistoryActions.MoveLayers.prototype.undo = function() { + var i, layer, stack, ref, posMap, len; + + stack = new Array(Tegaki.layers.length); + + posMap = {}; + + for (layer of this.layers) { + posMap[layer[1].id] = layer[0]; + } + + for (i = 0, len = Tegaki.layers.length; i < len; ++i) { + layer = Tegaki.layers[i]; + + if (posMap[layer.id] !== undefined) { + Tegaki.layers.splice(i, 1); + Tegaki.layers.splice(posMap[layer.id], 0, layer); + } + } + + TegakiUI.updayeLayersGridOrder(); + + ref = Tegaki.layersCnt.children[0]; + + for (i = Tegaki.layers.length - 1; i >= 0; i--) { + layer = Tegaki.layers[i]; + Tegaki.layersCnt.insertBefore(layer.canvas, ref.nextElementSibling); + } + + TegakiLayers.setActiveLayer(this.aLayerId); +}; + +TegakiHistoryActions.MoveLayers.prototype.redo = function() { + var layer, layers = new Set(); + + for (layer of this.layers.slice().reverse()) { + layers.add(layer[1].id); + } + + TegakiLayers.setActiveLayer(this.aLayerId); + TegakiLayers.moveLayers(layers, this.belowPos); +}; + +TegakiHistoryActions.AddLayer.prototype.undo = function() { + TegakiLayers.deleteLayers([this.layer.id]); + TegakiLayers.setActiveLayer(this.aLayerIdBefore); + Tegaki.layerCounter--; +}; + +TegakiHistoryActions.AddLayer.prototype.redo = function() { + TegakiLayers.setActiveLayer(this.aLayerIdBefore); + TegakiLayers.addLayer(this.layer); + TegakiLayers.setActiveLayer(this.aLayerIdAfter); +}; + +TegakiHistoryActions.SetLayersAlpha.prototype.undo = function() { + var id, layerAlpha, layer; + + for (layerAlpha of this.layerAlphas) { + [id, layerAlpha] = layerAlpha; + + if (layer = TegakiLayers.getLayerById(id)) { + TegakiLayers.setLayerAlpha(layer, layerAlpha); + } + } + + TegakiUI.updateLayerAlphaOpt(); +}; + +TegakiHistoryActions.SetLayersAlpha.prototype.redo = function() { + var id, layerAlpha, layer; + + for (layerAlpha of this.layerAlphas) { + [id, layerAlpha] = layerAlpha; + + if (layer = TegakiLayers.getLayerById(id)) { + TegakiLayers.setLayerAlpha(layer, this.newAlpha); + } + } + + TegakiUI.updateLayerAlphaOpt(); +}; + +TegakiHistoryActions.SetLayersAlpha.prototype.coalesce = function(action) { + var i; + + if (this.layerAlphas.length !== action.layerAlphas.length) { + return false; + } + + for (i = 0; i < this.layerAlphas.length; ++i) { + if (this.layerAlphas[i][0] !== action.layerAlphas[i][0]) { + return false; + } + } + + this.newAlpha = action.newAlpha; + + return true; +}; + +TegakiHistoryActions.SetLayerName.prototype.exec = function(type) { + var layer = TegakiLayers.getLayerById(this.layerId); + + if (layer) { + layer.name = type ? this.newName : this.oldName; + TegakiUI.updateLayerName(layer); + } +}; + +TegakiHistoryActions.SetLayerName.prototype.undo = function() { + this.exec(0); +}; + +TegakiHistoryActions.SetLayerName.prototype.redo = function() { + this.exec(1); +}; +var TegakiKeybinds = { + keyMap: {}, + + captionMap: {}, + + clear: function() { + this.keyMap = {}; + this.captionMap = {}; + }, + + bind: function(keys, klass, fn, id, caption) { + this.keyMap[keys] = [klass, fn]; + + if (id) { + this.captionMap[id] = caption; + } + }, + + getCaption(id) { + return this.captionMap[id]; + }, + + resolve: function(e) { + var fn, mods, keys, el; + + el = e.target; + + if (el.nodeName == 'INPUT' && (el.type === 'text' || el.type === 'number')) { + return; + } + + mods = []; + + if (e.ctrlKey) { + mods.push('ctrl'); + } + + if (e.shiftKey) { + mods.push('shift'); + } + + keys = e.key.toLowerCase(); + + if (mods[0]) { + keys = mods.join('+') + '+' + keys; + } + + fn = TegakiKeybinds.keyMap[keys]; + + if (fn && !e.altKey && !e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + fn[0][fn[1]](); + } + }, +}; +var TegakiLayers = { + cloneLayer: function(layer) { + var newLayer = Object.assign({}, layer); + + newLayer.canvas = $T.copyCanvas(layer.canvas, true); + newLayer.ctx = newLayer.canvas.getContext('2d'); + newLayer.imageData = $T.copyImageData(layer.imageData); + + return newLayer; + }, + + getCanvasById: function(id) { + return $T.id('tegaki-canvas-' + id); + }, + + getActiveLayer: function() { + return Tegaki.activeLayer; + }, + + getLayerPosById: function(id) { + var i, layers = Tegaki.layers; + + for (i = 0; i < layers.length; ++i) { + if (layers[i].id === id) { + return i; + } + } + + return -1; + }, + + getTopFencedLayerId: function() { + var i, id, layer, layers = Tegaki.layers; + + for (i = layers.length - 1; i >= 0; i--) { + if (TegakiLayers.selectedLayersHas(layers[i].id)) { + break; + } + } + + for (i = i - 1; i >= 0; i--) { + if (!TegakiLayers.selectedLayersHas(layers[i].id)) { + break; + } + } + + if (layer = layers[i]) { + id = layer.id; + } + else { + id = 0; + } + + return id; + }, + + getSelectedEdgeLayerPos: function(top) { + var i, layers = Tegaki.layers; + + if (top) { + for (i = Tegaki.layers.length - 1; i >= 0; i--) { + if (TegakiLayers.selectedLayersHas(layers[i].id)) { + break; + } + } + } + else { + for (i = 0; i < layers.length; ++i) { + if (TegakiLayers.selectedLayersHas(layers[i].id)) { + break; + } + } + + if (i >= layers.length) { + i = -1; + } + } + + return i; + }, + + getTopLayer: function() { + return Tegaki.layers[Tegaki.layers.length - 1]; + }, + + getTopLayerId: function() { + var layer = TegakiLayers.getTopLayer(); + + if (layer) { + return layer.id; + } + else { + return 0; + } + }, + + getLayerBelowId: function(belowId) { + var idx; + + idx = TegakiLayers.getLayerPosById(belowId); + + if (idx < 1) { + return null; + } + + return Tegaki.layers[idx - 1]; + }, + + getLayerById: function(id) { + return Tegaki.layers[TegakiLayers.getLayerPosById(id)]; + }, + + isSameLayerOrder: function(a, b) { + var i, al; + + if (a.length !== b.length) { + return false; + } + + for (i = 0; al = a[i]; ++i) { + if (al.id !== b[i].id) { + return false; + } + } + + return true; + }, + + addLayer: function(baseLayer = {}) { + var id, canvas, k, params, layer, afterNode, afterPos, + aLayerIdBefore, ctx; + + if (Tegaki.activeLayer) { + aLayerIdBefore = Tegaki.activeLayer.id; + afterPos = TegakiLayers.getLayerPosById(Tegaki.activeLayer.id); + afterNode = $T.cls('tegaki-layer', Tegaki.layersCnt)[afterPos]; + } + else { + afterPos = -1; + afterNode = null; + } + + if (!afterNode) { + afterNode = Tegaki.layersCnt.firstElementChild; + } + + canvas = $T.el('canvas'); + canvas.className = 'tegaki-layer'; + canvas.width = Tegaki.baseWidth; + canvas.height = Tegaki.baseHeight; + + id = ++Tegaki.layerCounter; + + canvas.id = 'tegaki-canvas-' + id; + canvas.setAttribute('data-id', id); + + params = { + name: TegakiStrings.layer + ' ' + id, + visible: true, + alpha: 1.0, + }; + + ctx = canvas.getContext('2d'); + + layer = { + id: id, + canvas: canvas, + ctx: ctx, + imageData: ctx.getImageData(0, 0, canvas.width, canvas.height) + }; + + for (k in params) { + if (baseLayer[k] !== undefined) { + params[k] = baseLayer[k]; + } + + layer[k] = params[k]; + } + + Tegaki.layers.splice(afterPos + 1, 0, layer); + + TegakiUI.updateLayersGridAdd(layer, aLayerIdBefore); + + Tegaki.layersCnt.insertBefore(canvas, afterNode.nextElementSibling); + + Tegaki.onLayerStackChanged(); + + return new TegakiHistoryActions.AddLayer(layer, aLayerIdBefore, id); + }, + + deleteLayers: function(ids, extraParams) { + var id, idx, layer, layers, delIndexes, params; + + params = { + aLayerIdBefore: Tegaki.activeLayer ? Tegaki.activeLayer.id : -1, + aLayerIdAfter: TegakiLayers.getTopFencedLayerId() + }; + + layers = []; + + delIndexes = []; + + for (id of ids) { + idx = TegakiLayers.getLayerPosById(id); + layer = Tegaki.layers[idx]; + + layers.push([idx, layer]); + + Tegaki.layersCnt.removeChild(layer.canvas); + + delIndexes.push(idx); + + TegakiUI.updateLayersGridRemove(id); + + TegakiUI.deleteLayerPreviewCtx(layer); + } + + delIndexes = delIndexes.sort($T.sortDescCb); + + for (idx of delIndexes) { + Tegaki.layers.splice(idx, 1); + } + + if (extraParams) { + Object.assign(params, extraParams); + } + + Tegaki.onLayerStackChanged(); + + return new TegakiHistoryActions.DeleteLayers(layers, params); + }, + + mergeLayers: function(idSet) { + var canvas, ctx, imageDataAfter, imageDataBefore, + targetLayer, action, layer, layers, delIds, mergeDown; + + layers = []; + + for (layer of Tegaki.layers) { + if (idSet.has(layer.id)) { + layers.push(layer); + } + } + + if (layers.length < 1) { + return; + } + + if (layers.length === 1) { + targetLayer = TegakiLayers.getLayerBelowId(layers[0].id); + + if (!targetLayer) { + return; + } + + layers.unshift(targetLayer); + + mergeDown = true; + } + else { + targetLayer = layers[layers.length - 1]; + + mergeDown = false; + } + + canvas = $T.el('canvas'); + canvas.width = Tegaki.baseWidth; + canvas.height = Tegaki.baseHeight; + + ctx = canvas.getContext('2d'); + + imageDataBefore = $T.copyImageData(targetLayer.imageData); + + delIds = []; + + for (layer of layers) { + if (layer.id !== targetLayer.id) { + delIds.push(layer.id); + } + + ctx.globalAlpha = layer.alpha; + ctx.drawImage(layer.canvas, 0, 0); + } + + $T.clearCtx(targetLayer.ctx); + + targetLayer.ctx.drawImage(canvas, 0, 0); + + TegakiLayers.syncLayerImageData(targetLayer); + + imageDataAfter = $T.copyImageData(targetLayer.imageData); + + action = TegakiLayers.deleteLayers(delIds, { + tgtLayerId: targetLayer.id, + tgtLayerAlpha: targetLayer.alpha, + aLayerIdAfter: targetLayer.id, + imageDataBefore: imageDataBefore, + imageDataAfter: imageDataAfter, + mergeDown: mergeDown + }); + + TegakiLayers.setLayerAlpha(targetLayer, 1.0); + + TegakiUI.updateLayerAlphaOpt(); + + TegakiUI.updateLayerPreview(targetLayer); + + Tegaki.onLayerStackChanged(); + + return action; + }, + + moveLayers: function(idSet, belowPos) { + var idx, layer, + historyLayers, updLayers, movedLayers, + tgtCanvas, updTgtPos; + + if (!idSet.size || !Tegaki.layers.length) { + return; + } + + if (belowPos >= Tegaki.layers.length) { + tgtCanvas = TegakiLayers.getTopLayer().canvas.nextElementSibling; + } + else { + layer = Tegaki.layers[belowPos]; + tgtCanvas = layer.canvas; + } + + historyLayers = []; + updLayers = []; + movedLayers = []; + + updTgtPos = belowPos; + + idx = 0; + + for (layer of Tegaki.layers) { + if (idSet.has(layer.id)) { + if (belowPos > 0 && idx <= belowPos) { + updTgtPos--; + } + + historyLayers.push([idx, layer]); + movedLayers.push(layer); + } + else { + updLayers.push(layer); + } + + ++idx; + } + + updLayers.splice(updTgtPos, 0, ...movedLayers); + + if (TegakiLayers.isSameLayerOrder(updLayers, Tegaki.layers)) { + return; + } + + Tegaki.layers = updLayers; + + for (layer of historyLayers) { + Tegaki.layersCnt.insertBefore(layer[1].canvas, tgtCanvas); + } + + TegakiUI.updayeLayersGridOrder(); + + Tegaki.onLayerStackChanged(); + + return new TegakiHistoryActions.MoveLayers( + historyLayers, belowPos, + Tegaki.activeLayer ? Tegaki.activeLayer.id : -1 + ); + }, + + setLayerVisibility: function(layer, flag) { + layer.visible = flag; + + if (flag) { + layer.canvas.classList.remove('tegaki-hidden'); + } + else { + layer.canvas.classList.add('tegaki-hidden'); + } + + Tegaki.onLayerStackChanged(); + + TegakiUI.updateLayersGridVisibility(layer.id, flag); + }, + + setLayerAlpha: function(layer, alpha) { + layer.alpha = alpha; + layer.canvas.style.opacity = alpha; + }, + + setActiveLayer: function(id) { + var idx, layer; + + if (!id) { + id = TegakiLayers.getTopLayerId(); + + if (!id) { + Tegaki.activeLayer = null; + return; + } + } + + idx = TegakiLayers.getLayerPosById(id); + + if (idx < 0) { + return; + } + + layer = Tegaki.layers[idx]; + + if (Tegaki.activeLayer) { + Tegaki.copyContextState(Tegaki.activeLayer.ctx, layer.ctx); + } + + Tegaki.activeLayer = layer; + + TegakiLayers.selectedLayersClear(); + TegakiLayers.selectedLayersAdd(id); + + TegakiUI.updateLayersGridActive(id); + TegakiUI.updateLayerAlphaOpt(); + + Tegaki.onLayerStackChanged(); + }, + + syncLayerImageData(layer, imageData = null) { + if (imageData) { + layer.imageData = $T.copyImageData(imageData); + } + else { + layer.imageData = layer.ctx.getImageData( + 0, 0, Tegaki.baseWidth, Tegaki.baseHeight + ); + } + }, + + selectedLayersHas: function(id) { + return Tegaki.selectedLayers.has(+id); + }, + + selectedLayersClear: function() { + Tegaki.selectedLayers.clear(); + TegakiUI.updateLayerAlphaOpt(); + TegakiUI.updateLayersGridSelectedClear(); + }, + + selectedLayersAdd: function(id) { + Tegaki.selectedLayers.add(+id); + TegakiUI.updateLayerAlphaOpt(); + TegakiUI.updateLayersGridSelectedSet(id, true); + }, + + selectedLayersRemove: function(id) { + Tegaki.selectedLayers.delete(+id); + TegakiUI.updateLayerAlphaOpt(); + TegakiUI.updateLayersGridSelectedSet(id, false); + }, + + selectedLayersToggle: function(id) { + if (TegakiLayers.selectedLayersHas(id)) { + TegakiLayers.selectedLayersRemove(id); + } + else { + TegakiLayers.selectedLayersAdd(id); + } + } +}; +var Tegaki = { + VERSION: '0.9.1', + + startTimeStamp: 0, + + bg: null, + canvas: null, + ctx: null, + layers: [], + + layersCnt: null, + canvasCnt: null, + + ghostBuffer: null, + blendBuffer: null, + ghostBuffer32: null, + blendBuffer32: null, + + activeLayer: null, + + layerCounter: 0, + selectedLayers: new Set(), + + activePointerId: 0, + activePointerIsPen: false, + + isPainting: false, + + offsetX: 0, + offsetY: 0, + + zoomLevel: 0, + zoomFactor: 1.0, + zoomFactorList: [0.5, 1.0, 2.0, 4.0, 8.0, 16.0], + zoomBaseLevel: 1, + + hasCustomCanvas: false, + + TWOPI: 2 * Math.PI, + + toolList: [ + TegakiPencil, + TegakiPen, + TegakiAirbrush, + TegakiBucket, + TegakiTone, + TegakiPipette, + TegakiBlur, + TegakiEraser + ], + + tools: {}, + + tool: null, + + colorPaletteId: 0, + + toolColor: '#000000', + defaultTool: 'pencil', + + bgColor: '#ffffff', + maxSize: 64, + maxLayers: 25, + baseWidth: 0, + baseHeight: 0, + + replayRecorder: null, + replayViewer: null, + + onDoneCb: null, + onCancelCb: null, + + replayMode: false, + + saveReplay: false, + + open: function(opts = {}) { + var self = Tegaki; + + if (self.bg) { + if (self.replayMode !== (opts.replayMode ? true : false)) { + self.destroy(); + } + else { + self.resume(); + return; + } + } + + self.startTimeStamp = Date.now(); + + if (opts.bgColor) { + self.bgColor = opts.bgColor; + } + + self.hasCustomCanvas = false; + + self.saveReplay = !!opts.saveReplay; + self.replayMode = !!opts.replayMode; + + self.onDoneCb = opts.onDone; + self.onCancelCb = opts.onCancel; + + self.baseWidth = opts.width || 0; + self.baseHeight = opts.height || 0; + + self.createTools(); + + self.initKeybinds(); + + [self.bg, self.canvasCnt, self.layersCnt] = TegakiUI.buildUI(); + + document.body.appendChild(self.bg); + document.body.classList.add('tegaki-backdrop'); + + if (!self.replayMode) { + self.init(); + + self.setTool(self.defaultTool); + + if (self.saveReplay) { + self.replayRecorder = new TegakiReplayRecorder(); + self.replayRecorder.start(); + } + } + else { + TegakiUI.setReplayMode(true); + + self.replayViewer = new TegakiReplayViewer(); + + if (opts.replayURL) { + self.loadReplayFromURL(opts.replayURL); + } + } + }, + + init: function() { + var self = Tegaki; + + self.createCanvas(); + + self.centerLayersCnt(); + + self.createBuffers(); + + self.updatePosOffset(); + + self.resetLayers(); + + self.bindGlobalEvents(); + + TegakiCursor.init(self.baseWidth, self.baseHeight); + + TegakiUI.updateUndoRedo(0, 0); + TegakiUI.updateZoomLevel(); + }, + + initFromReplay: function() { + var self, r; + + self = Tegaki; + r = self.replayViewer; + + self.initToolsFromReplay(); + + self.baseWidth = r.canvasWidth; + self.baseHeight = r.canvasHeight; + self.bgColor = $T.RgbToHex(...r.bgColor); + + self.toolColor = $T.RgbToHex(...r.toolColor); + }, + + initToolsFromReplay: function() { + var self, r, name, tool, rTool, prop, props; + + self = Tegaki; + r = self.replayViewer; + + for (name in self.tools) { + tool = self.tools[name]; + + if (tool.id === r.toolId) { + self.defaultTool = name; + } + + rTool = r.toolMap[tool.id]; + + props = ['step', 'size', 'alpha', 'flow', 'tipId']; + + for (prop of props) { + if (rTool[prop] !== undefined) { + tool[prop] = rTool[prop]; + } + } + + props = [ + 'sizeDynamicsEnabled', 'alphaDynamicsEnabled', 'flowDynamicsEnabled', + 'usePreserveAlpha' + ]; + + for (prop of props) { + if (rTool[prop] !== undefined) { + tool[prop] = !!rTool[prop]; + } + } + } + }, + + resetLayers: function() { + var i, len; + + if (Tegaki.layers.length) { + for (i = 0, len = Tegaki.layers.length; i < len; ++i) { + Tegaki.layersCnt.removeChild(Tegaki.layers[i].canvas); + } + + Tegaki.layers = []; + Tegaki.layerCounter = 0; + + TegakiUI.updateLayersGridClear(); + } + + TegakiLayers.addLayer(); + TegakiLayers.setActiveLayer(0); + }, + + createCanvas: function() { + var canvas, self = Tegaki; + + canvas = $T.el('canvas'); + canvas.id = 'tegaki-canvas'; + canvas.width = self.baseWidth; + canvas.height = self.baseHeight; + + self.canvas = canvas; + + self.ctx = canvas.getContext('2d'); + self.ctx.fillStyle = self.bgColor; + self.ctx.fillRect(0, 0, self.baseWidth, self.baseHeight); + + self.layersCnt.appendChild(canvas); + }, + + createTools: function() { + var klass, tool; + + for (klass of Tegaki.toolList) { + tool = new klass(); + Tegaki.tools[tool.name] = tool; + } + }, + + bindGlobalEvents: function() { + var self = Tegaki; + + if (!self.replayMode) { + $T.on(self.canvasCnt, 'pointermove', self.onPointerMove); + $T.on(self.canvasCnt, 'pointerdown', self.onPointerDown); + $T.on(document, 'pointerup', self.onPointerUp); + $T.on(document, 'pointercancel', self.onPointerUp); + + $T.on(document, 'keydown', TegakiKeybinds.resolve); + + $T.on(window, 'beforeunload', Tegaki.onTabClose); + } + else { + $T.on(document, 'visibilitychange', Tegaki.onVisibilityChange); + } + + $T.on(self.bg, 'contextmenu', self.onDummy); + $T.on(window, 'resize', self.updatePosOffset); + $T.on(window, 'scroll', self.updatePosOffset); + }, + + unBindGlobalEvents: function() { + var self = Tegaki; + + if (!self.replayMode) { + $T.off(self.canvasCnt, 'pointermove', self.onPointerMove); + $T.off(self.canvasCnt, 'pointerdown', self.onPointerDown); + $T.off(document, 'pointerup', self.onPointerUp); + $T.off(document, 'pointercancel', self.onPointerUp); + + $T.off(document, 'keydown', TegakiKeybinds.resolve); + + $T.off(window, 'beforeunload', Tegaki.onTabClose); + } + else { + $T.off(document, 'visibilitychange', Tegaki.onVisibilityChange); + } + + $T.off(self.bg, 'contextmenu', self.onDummy); + $T.off(window, 'resize', self.updatePosOffset); + $T.off(window, 'scroll', self.updatePosOffset); + }, + + createBuffers() { + Tegaki.ghostBuffer = new ImageData(Tegaki.baseWidth, Tegaki.baseHeight); + Tegaki.blendBuffer = new ImageData(Tegaki.baseWidth, Tegaki.baseHeight); + Tegaki.ghostBuffer32 = new Uint32Array(Tegaki.ghostBuffer.data.buffer); + Tegaki.blendBuffer32 = new Uint32Array(Tegaki.blendBuffer.data.buffer); + }, + + clearBuffers() { + Tegaki.ghostBuffer32.fill(0); + Tegaki.blendBuffer32.fill(0); + }, + + destroyBuffers() { + Tegaki.ghostBuffer = null; + Tegaki.blendBuffer = null; + Tegaki.ghostBuffer32 = null; + Tegaki.blendBuffer32 = null; + }, + + disableSmoothing: function(ctx) { + ctx.mozImageSmoothingEnabled = false; + ctx.webkitImageSmoothingEnabled = false; + ctx.msImageSmoothingEnabled = false; + ctx.imageSmoothingEnabled = false; + }, + + centerLayersCnt: function() { + var style = Tegaki.layersCnt.style; + + style.width = Tegaki.baseWidth + 'px'; + style.height = Tegaki.baseHeight + 'px'; + }, + + onTabClose: function(e) { + e.preventDefault(); + e.returnValue = ''; + }, + + onVisibilityChange: function(e) { + if (!Tegaki.replayMode) { + return; + } + + if (document.visibilityState === 'visible') { + if (Tegaki.replayViewer.autoPaused) { + Tegaki.replayViewer.play(); + } + } + else { + if (Tegaki.replayViewer.playing) { + Tegaki.replayViewer.autoPause(); + } + } + }, + + initKeybinds: function() { + var cls, tool; + + if (Tegaki.replayMode) { + return; + } + + TegakiKeybinds.bind('ctrl+z', TegakiHistory, 'undo', 'undo', 'Ctrl+Z'); + TegakiKeybinds.bind('ctrl+y', TegakiHistory, 'redo', 'redo', 'Ctrl+Y'); + + TegakiKeybinds.bind('+', Tegaki, 'setToolSizeUp', 'toolSize', 'Numpad +/-'); + TegakiKeybinds.bind('-', Tegaki, 'setToolSizeDown'); + + for (tool in Tegaki.tools) { + cls = Tegaki.tools[tool]; + + if (cls.keybind) { + TegakiKeybinds.bind(cls.keybind, cls, 'set'); + } + } + }, + + getCursorPos: function(e, axis) { + if (axis === 0) { + return 0 | (( + e.clientX + + window.pageXOffset + + Tegaki.canvasCnt.scrollLeft + - Tegaki.offsetX + ) / Tegaki.zoomFactor); + } + else { + return 0 | (( + e.clientY + + window.pageYOffset + + Tegaki.canvasCnt.scrollTop + - Tegaki.offsetY + ) / Tegaki.zoomFactor); + } + }, + + resume: function() { + if (Tegaki.saveReplay) { + Tegaki.replayRecorder.start(); + } + + Tegaki.bg.classList.remove('tegaki-hidden'); + document.body.classList.add('tegaki-backdrop'); + Tegaki.setZoom(0); + Tegaki.centerLayersCnt(); + Tegaki.updatePosOffset(); + Tegaki.bindGlobalEvents(); + }, + + hide: function() { + if (Tegaki.saveReplay) { + Tegaki.replayRecorder.stop(); + } + + Tegaki.bg.classList.add('tegaki-hidden'); + document.body.classList.remove('tegaki-backdrop'); + Tegaki.unBindGlobalEvents(); + }, + + destroy: function() { + Tegaki.unBindGlobalEvents(); + + TegakiKeybinds.clear(); + + TegakiHistory.clear(); + + Tegaki.bg.parentNode.removeChild(Tegaki.bg); + + document.body.classList.remove('tegaki-backdrop'); + + Tegaki.startTimeStamp = 0; + + Tegaki.bg = null; + Tegaki.canvasCnt = null; + Tegaki.layersCnt = null; + Tegaki.canvas = null; + Tegaki.ctx = null; + Tegaki.layers = []; + Tegaki.layerCounter = 0; + Tegaki.zoomLevel = 0; + Tegaki.zoomFactor = 1.0; + Tegaki.activeLayer = null; + + Tegaki.tool = null; + + TegakiCursor.destroy(); + + Tegaki.replayRecorder = null; + Tegaki.replayViewer = null; + + Tegaki.destroyBuffers(); + }, + + flatten: function(ctx) { + var i, layer, canvas, len; + + if (!ctx) { + canvas = $T.el('canvas'); + ctx = canvas.getContext('2d'); + } + else { + canvas = ctx.canvas; + } + + canvas.width = Tegaki.canvas.width; + canvas.height = Tegaki.canvas.height; + + ctx.drawImage(Tegaki.canvas, 0, 0); + + for (i = 0, len = Tegaki.layers.length; i < len; ++i) { + layer = Tegaki.layers[i]; + + if (!layer.visible) { + continue; + } + + ctx.globalAlpha = layer.alpha; + ctx.drawImage(layer.canvas, 0, 0); + } + + return canvas; + }, + + onReplayLoaded: function() { + TegakiUI.clearMsg(); + Tegaki.initFromReplay(); + Tegaki.init(); + Tegaki.setTool(Tegaki.defaultTool); + TegakiUI.updateReplayControls(); + TegakiUI.updateReplayTime(true); + TegakiUI.enableReplayControls(true); + Tegaki.replayViewer.play(); + }, + + onReplayGaplessClick: function() { + Tegaki.replayViewer.toggleGapless(); + TegakiUI.updateReplayGapless(); + }, + + onReplayPlayPauseClick: function() { + Tegaki.replayViewer.togglePlayPause(); + }, + + onReplayRewindClick: function() { + Tegaki.replayViewer.rewind(); + }, + + onReplaySlowDownClick: function() { + Tegaki.replayViewer.slowDown(); + TegakiUI.updateReplaySpeed(); + }, + + onReplaySpeedUpClick: function() { + Tegaki.replayViewer.speedUp(); + TegakiUI.updateReplaySpeed(); + }, + + onReplayTimeChanged: function() { + TegakiUI.updateReplayTime(); + }, + + onReplayPlayPauseChanged: function() { + TegakiUI.updateReplayPlayPause(); + }, + + onReplayReset: function() { + Tegaki.initFromReplay(); + Tegaki.setTool(Tegaki.defaultTool); + Tegaki.resizeCanvas(Tegaki.baseWidth, Tegaki.baseHeight); + TegakiUI.updateReplayControls(); + TegakiUI.updateReplayTime(); + }, + + onMainColorClick: function(e) { + var el; + e.preventDefault(); + el = $T.id('tegaki-colorpicker'); + el.click(); + }, + + onPaletteColorClick: function(e) { + if (e.button === 2) { + this.style.backgroundColor = Tegaki.toolColor; + this.setAttribute('data-color', Tegaki.toolColor); + } + else if (e.button === 0) { + Tegaki.setToolColor(this.getAttribute('data-color')); + } + }, + + onColorPicked: function(e) { + $T.id('tegaki-color').style.backgroundColor = this.value; + Tegaki.setToolColor(this.value); + }, + + onSwitchPaletteClick: function(e) { + var id; + + if (e.target.hasAttribute('data-prev')) { + id = Tegaki.colorPaletteId - 1; + } + else { + id = Tegaki.colorPaletteId + 1; + } + + Tegaki.setColorPalette(id); + }, + + setColorPalette: function(id) { + if (id < 0 || id >= TegakiColorPalettes.length) { + return; + } + + Tegaki.colorPaletteId = id; + TegakiUI.updateColorPalette(); + }, + + setToolSizeUp: function() { + Tegaki.setToolSize(Tegaki.tool.size + 1); + }, + + setToolSizeDown: function() { + Tegaki.setToolSize(Tegaki.tool.size - 1); + }, + + setToolSize: function(size) { + if (size > 0 && size <= Tegaki.maxSize) { + Tegaki.tool.setSize(size); + Tegaki.updateCursorStatus(); + Tegaki.recordEvent(TegakiEventSetToolSize, performance.now(), size); + TegakiUI.updateToolSize(); + } + }, + + setToolAlpha: function(alpha) { + alpha = Math.fround(alpha); + + if (alpha >= 0.0 && alpha <= 1.0) { + Tegaki.tool.setAlpha(alpha); + Tegaki.recordEvent(TegakiEventSetToolAlpha, performance.now(), alpha); + TegakiUI.updateToolAlpha(); + } + }, + + setToolFlow: function(flow) { + flow = Math.fround(flow); + + if (flow >= 0.0 && flow <= 1.0) { + Tegaki.tool.setFlow(flow); + Tegaki.recordEvent(TegakiEventSetToolFlow, performance.now(), flow); + TegakiUI.updateToolFlow(); + } + }, + + setToolColor: function(color) { + Tegaki.toolColor = color; + $T.id('tegaki-color').style.backgroundColor = color; + $T.id('tegaki-colorpicker').value = color; + Tegaki.tool.setColor(color); + Tegaki.recordEvent(TegakiEventSetColor, performance.now(), Tegaki.tool.rgb); + }, + + setToolColorRGB: function(r, g, b) { + Tegaki.setToolColor($T.RgbToHex(r, g, b)); + }, + + setTool: function(tool) { + Tegaki.tools[tool].set(); + }, + + setToolById: function(id) { + var tool; + + for (tool in Tegaki.tools) { + if (Tegaki.tools[tool].id === id) { + Tegaki.setTool(tool); + return; + } + } + }, + + setZoom: function(level) { + var idx; + + idx = level + Tegaki.zoomBaseLevel; + + if (idx >= Tegaki.zoomFactorList.length || idx < 0 || !Tegaki.canvas) { + return; + } + + Tegaki.zoomLevel = level; + Tegaki.zoomFactor = Tegaki.zoomFactorList[idx]; + + TegakiUI.updateZoomLevel(); + + Tegaki.layersCnt.style.width = Math.ceil(Tegaki.baseWidth * Tegaki.zoomFactor) + 'px'; + Tegaki.layersCnt.style.height = Math.ceil(Tegaki.baseHeight * Tegaki.zoomFactor) + 'px'; + + if (level < 0) { + Tegaki.layersCnt.classList.add('tegaki-smooth-layers'); + } + else { + Tegaki.layersCnt.classList.remove('tegaki-smooth-layers'); + } + + Tegaki.updatePosOffset(); + }, + + onZoomChange: function() { + if (this.hasAttribute('data-in')) { + Tegaki.setZoom(Tegaki.zoomLevel + 1); + } + else { + Tegaki.setZoom(Tegaki.zoomLevel - 1); + } + }, + + onNewClick: function() { + var width, height, tmp, self = Tegaki; + + width = prompt(TegakiStrings.promptWidth, self.canvas.width); + + if (!width) { return; } + + height = prompt(TegakiStrings.promptHeight, self.canvas.height); + + if (!height) { return; } + + width = +width; + height = +height; + + if (width < 1 || height < 1) { + TegakiUI.printMsg(TegakiStrings.badDimensions); + return; + } + + tmp = {}; + self.copyContextState(self.activeLayer.ctx, tmp); + self.resizeCanvas(width, height); + self.copyContextState(tmp, self.activeLayer.ctx); + + self.setZoom(0); + TegakiHistory.clear(); + + TegakiUI.updateLayerPreviewSize(); + + self.startTimeStamp = Date.now(); + + if (self.saveReplay) { + self.createTools(); + self.setTool(self.defaultTool); + self.replayRecorder = new TegakiReplayRecorder(); + self.replayRecorder.start(); + } + }, + + onOpenClick: function() { + var el, tainted; + + tainted = TegakiHistory.undoStack[0] || TegakiHistory.redoStack[0]; + + if (tainted || Tegaki.saveReplay) { + if (!confirm(TegakiStrings.confirmChangeCanvas)) { + return; + } + } + + el = $T.id('tegaki-filepicker'); + el.click(); + }, + + loadReplayFromFile: function() { + Tegaki.replayViewer.debugLoadLocal(); + }, + + loadReplayFromURL: function(url) { + TegakiUI.printMsg(TegakiStrings.loadingReplay, 0); + Tegaki.replayViewer.loadFromURL(url); + }, + + onExportClick: function() { + Tegaki.flatten().toBlob(function(b) { + var el = $T.el('a'); + el.className = 'tegaki-hidden'; + el.download = $T.generateFilename() + '.png'; + el.href = URL.createObjectURL(b); + Tegaki.bg.appendChild(el); + el.click(); + Tegaki.bg.removeChild(el); + }, 'image/png'); + }, + + onUndoClick: function() { + TegakiHistory.undo(); + }, + + onRedoClick: function() { + TegakiHistory.redo(); + }, + + onHistoryChange: function(undoSize, redoSize, type = 0) { + TegakiUI.updateUndoRedo(undoSize, redoSize); + + if (type === -1) { + Tegaki.recordEvent(TegakiEventUndo, performance.now()); + } + else if (type === 1) { + Tegaki.recordEvent(TegakiEventRedo, performance.now()); + } + }, + + onDoneClick: function() { + Tegaki.hide(); + Tegaki.onDoneCb(); + }, + + onCancelClick: function() { + if (!confirm(TegakiStrings.confirmCancel)) { + return; + } + + Tegaki.destroy(); + Tegaki.onCancelCb(); + }, + + onCloseViewerClick: function() { + Tegaki.replayViewer.destroy(); + Tegaki.destroy(); + }, + + onToolSizeChange: function() { + var val = +this.value; + + if (val < 1) { + val = 1; + } + else if (val > Tegaki.maxSize) { + val = Tegaki.maxSize; + } + + Tegaki.setToolSize(val); + }, + + onToolAlphaChange: function(e) { + var val = +this.value; + + val = val / 100; + + if (val < 0.0) { + val = 0.0; + } + else if (val > 1.0) { + val = 1.0; + } + + Tegaki.setToolAlpha(val); + }, + + onToolFlowChange: function(e) { + var val = +this.value; + + val = val / 100; + + if (val < 0.0) { + val = 0.0; + } + else if (val > 1.0) { + val = 1.0; + } + + Tegaki.setToolFlow(val); + }, + + onToolPressureSizeClick: function(e) { + if (!Tegaki.tool.useSizeDynamics) { + return; + } + + Tegaki.setToolSizeDynamics(!Tegaki.tool.sizeDynamicsEnabled); + }, + + setToolSizeDynamics: function(flag) { + Tegaki.tool.setSizeDynamics(flag); + TegakiUI.updateToolDynamics(); + Tegaki.recordEvent(TegakiEventSetToolSizeDynamics, performance.now(), +flag); + }, + + onToolPressureAlphaClick: function(e) { + if (!Tegaki.tool.useAlphaDynamics) { + return; + } + + Tegaki.setToolAlphaDynamics(!Tegaki.tool.alphaDynamicsEnabled); + }, + + setToolAlphaDynamics: function(flag) { + Tegaki.tool.setAlphaDynamics(flag); + TegakiUI.updateToolDynamics(); + Tegaki.recordEvent(TegakiEventSetToolAlphaDynamics, performance.now(), +flag); + }, + + onToolPressureFlowClick: function(e) { + if (!Tegaki.tool.useFlowDynamics) { + return; + } + + Tegaki.setToolFlowDynamics(!Tegaki.tool.flowDynamicsEnabled); + }, + + setToolFlowDynamics: function(flag) { + Tegaki.tool.setFlowDynamics(flag); + TegakiUI.updateToolDynamics(); + Tegaki.recordEvent(TegakiEventSetToolFlowDynamics, performance.now(), +flag); + }, + + onToolPreserveAlphaClick: function(e) { + if (!Tegaki.tool.usePreserveAlpha) { + return; + } + + Tegaki.setToolPreserveAlpha(!Tegaki.tool.preserveAlphaEnabled); + }, + + setToolPreserveAlpha: function(flag) { + Tegaki.tool.setPreserveAlpha(flag); + TegakiUI.updateToolPreserveAlpha(); + Tegaki.recordEvent(TegakiEventPreserveAlpha, performance.now(), +flag); + }, + + onToolTipClick: function(e) { + var tipId = +e.target.getAttribute('data-id'); + + if (tipId !== Tegaki.tool.tipId) { + Tegaki.setToolTip(tipId); + } + }, + + setToolTip: function(id) { + Tegaki.tool.setTip(id); + TegakiUI.updateToolShape(); + Tegaki.recordEvent(TegakiEventSetToolTip, performance.now(), id); + }, + + onLayerSelectorClick: function(e) { + var id = +this.getAttribute('data-id'); + + if (!id || e.target.classList.contains('tegaki-ui-cb')) { + return; + } + + if (e.ctrlKey) { + Tegaki.toggleSelectedLayer(id); + } + else { + Tegaki.setActiveLayer(id); + } + }, + + toggleSelectedLayer: function(id) { + TegakiLayers.selectedLayersToggle(id); + Tegaki.recordEvent(TegakiEventToggleLayerSelection, performance.now(), id); + }, + + setActiveLayer: function(id) { + TegakiLayers.setActiveLayer(id); + Tegaki.recordEvent(TegakiEventSetActiveLayer, performance.now(), id); + }, + + onLayerAlphaDragStart: function(e) { + TegakiUI.setupDragLabel(e, Tegaki.onLayerAlphaDragMove); + }, + + onLayerAlphaDragMove: function(delta) { + var val; + + if (!delta) { + return; + } + + val = Tegaki.activeLayer.alpha + delta / 100 ; + + if (val < 0.0) { + val = 0.0; + } + else if (val > 1.0) { + val = 1.0; + } + + Tegaki.setSelectedLayersAlpha(val); + }, + + onLayerAlphaChange: function() { + var val = +this.value; + + val = val / 100; + + if (val < 0.0) { + val = 0.0; + } + else if (val > 1.0) { + val = 1.0; + } + + Tegaki.setSelectedLayersAlpha(val); + }, + + setSelectedLayersAlpha: function(alpha) { + var layer, id, layerAlphas; + + alpha = Math.fround(alpha); + + if (alpha >= 0.0 && alpha <= 1.0 && Tegaki.selectedLayers.size > 0) { + layerAlphas = []; + + for (id of Tegaki.selectedLayers) { + if (layer = TegakiLayers.getLayerById(id)) { + layerAlphas.push([layer.id, layer.alpha]); + TegakiLayers.setLayerAlpha(layer, alpha); + } + } + + TegakiUI.updateLayerAlphaOpt(); + + TegakiHistory.push(new TegakiHistoryActions.SetLayersAlpha(layerAlphas, alpha)); + + Tegaki.recordEvent(TegakiEventSetSelectedLayersAlpha, performance.now(), alpha); + } + }, + + onLayerNameChangeClick: function(e) { + var id, name, layer; + + id = +this.getAttribute('data-id'); + + layer = TegakiLayers.getLayerById(id); + + if (!layer) { + return; + } + + if (name = prompt(undefined, layer.name)) { + Tegaki.setLayerName(id, name); + } + }, + + setLayerName: function(id, name) { + var oldName, layer; + + name = name.trim().slice(0, 25); + + layer = TegakiLayers.getLayerById(id); + + if (!layer || !name || name === layer.name) { + return; + } + + oldName = layer.name; + + layer.name = name; + + TegakiUI.updateLayerName(layer); + + TegakiHistory.push(new TegakiHistoryActions.SetLayerName(id, oldName, name)); + + Tegaki.recordEvent(TegakiEventHistoryDummy, performance.now()); + }, + + onLayerAddClick: function() { + Tegaki.addLayer(); + }, + + addLayer: function() { + var action; + + if (Tegaki.layers.length >= Tegaki.maxLayers) { + TegakiUI.printMsg(TegakiStrings.tooManyLayers); + return; + } + + TegakiHistory.push(action = TegakiLayers.addLayer()); + TegakiLayers.setActiveLayer(action.aLayerIdAfter); + Tegaki.recordEvent(TegakiEventAddLayer, performance.now()); + }, + + onLayerDeleteClick: function() { + Tegaki.deleteSelectedLayers(); + }, + + deleteSelectedLayers: function() { + var action, layerSet; + + layerSet = Tegaki.selectedLayers; + + if (layerSet.size === Tegaki.layers.length) { + return; + } + + if (!layerSet.size || Tegaki.layers.length < 2) { + return; + } + + TegakiHistory.push(action = TegakiLayers.deleteLayers(layerSet)); + TegakiLayers.selectedLayersClear(); + TegakiLayers.setActiveLayer(action.aLayerIdAfter); + Tegaki.recordEvent(TegakiEventDeleteLayers, performance.now()); + }, + + onLayerToggleVisibilityClick: function() { + Tegaki.toggleLayerVisibility(+this.getAttribute('data-id')); + }, + + toggleLayerVisibility: function(id) { + var layer = TegakiLayers.getLayerById(id); + TegakiLayers.setLayerVisibility(layer, !layer.visible); + Tegaki.recordEvent(TegakiEventToggleLayerVisibility, performance.now(), id); + }, + + onMergeLayersClick: function() { + Tegaki.mergeSelectedLayers(); + }, + + mergeSelectedLayers: function() { + var action; + + if (Tegaki.selectedLayers.size) { + if (action = TegakiLayers.mergeLayers(Tegaki.selectedLayers)) { + TegakiHistory.push(action); + TegakiLayers.setActiveLayer(action.aLayerIdAfter); + Tegaki.recordEvent(TegakiEventMergeLayers, performance.now()); + } + } + }, + + onMoveLayerClick: function(e) { + var belowPos, up; + + if (!Tegaki.selectedLayers.size) { + return; + } + + up = e.target.hasAttribute('data-up'); + + belowPos = TegakiLayers.getSelectedEdgeLayerPos(up); + + if (belowPos < 0) { + return; + } + + if (up) { + belowPos += 2; + } + else if (belowPos >= 1) { + belowPos--; + } + + Tegaki.moveSelectedLayers(belowPos); + }, + + moveSelectedLayers: function(belowPos) { + TegakiHistory.push(TegakiLayers.moveLayers(Tegaki.selectedLayers, belowPos)); + Tegaki.recordEvent(TegakiEventMoveLayers, performance.now(), belowPos); + }, + + onToolClick: function() { + Tegaki.setTool(this.getAttribute('data-tool')); + }, + + onToolChanged: function(tool) { + var el; + + Tegaki.tool = tool; + + if (el = $T.cls('tegaki-tool-active')[0]) { + el.classList.remove('tegaki-tool-active'); + } + + $T.id('tegaki-tool-btn-' + tool.name).classList.add('tegaki-tool-active'); + + Tegaki.recordEvent(TegakiEventSetTool, performance.now(), Tegaki.tool.id); + + TegakiUI.onToolChanged(); + Tegaki.updateCursorStatus(); + }, + + onLayerStackChanged: function() { + TegakiCursor.invalidateCache(); + }, + + onOpenFileSelected: function() { + var img; + + if (this.files && this.files[0]) { + img = new Image(); + img.onload = Tegaki.onOpenImageLoaded; + img.onerror = Tegaki.onOpenImageError; + + img.src = URL.createObjectURL(this.files[0]); + } + }, + + onOpenImageLoaded: function() { + var tmp = {}, self = Tegaki; + + self.hasCustomCanvas = true; + + self.copyContextState(self.activeLayer.ctx, tmp); + self.resizeCanvas(this.naturalWidth, this.naturalHeight); + self.activeLayer.ctx.drawImage(this, 0, 0); + TegakiLayers.syncLayerImageData(self.activeLayer); + self.copyContextState(tmp, self.activeLayer.ctx); + + self.setZoom(0); + + TegakiHistory.clear(); + + TegakiUI.updateLayerPreviewSize(true); + + self.startTimeStamp = Date.now(); + + if (self.saveReplay) { + self.replayRecorder.stop(); + self.replayRecorder = null; + self.saveReplay = false; + TegakiUI.setRecordingStatus(false); + } + }, + + onOpenImageError: function() { + TegakiUI.printMsg(TegakiStrings.errorLoadImage); + }, + + resizeCanvas: function(width, height) { + Tegaki.baseWidth = width; + Tegaki.baseHeight = height; + + Tegaki.createBuffers(); + + Tegaki.canvas.width = width; + Tegaki.canvas.height = height; + + TegakiCursor.updateCanvasSize(width, height); + + Tegaki.ctx.fillStyle = Tegaki.bgColor; + Tegaki.ctx.fillRect(0, 0, width, height); + + Tegaki.activeLayer = null; + + Tegaki.resetLayers(); + + Tegaki.centerLayersCnt(); + Tegaki.updatePosOffset(); + }, + + copyContextState: function(src, dest) { + var i, p, props = [ + 'lineCap', 'lineJoin', 'strokeStyle', 'fillStyle', 'globalAlpha', + 'lineWidth', 'globalCompositeOperation' + ]; + + for (i = 0; p = props[i]; ++i) { + dest[p] = src[p]; + } + }, + + updateCursorStatus: function() { + if (!Tegaki.tool.noCursor && Tegaki.tool.size > 1) { + Tegaki.cursor = true; + TegakiCursor.generate(Tegaki.tool.size); + } + else { + Tegaki.cursor = false; + $T.clearCtx(TegakiCursor.cursorCtx); + } + }, + + updatePosOffset: function() { + var aabb = Tegaki.canvas.getBoundingClientRect(); + + Tegaki.offsetX = aabb.left + window.pageXOffset + + Tegaki.canvasCnt.scrollLeft + Tegaki.layersCnt.scrollLeft; + Tegaki.offsetY = aabb.top + window.pageYOffset + + Tegaki.canvasCnt.scrollTop + Tegaki.layersCnt.scrollTop; + }, + + isScrollbarClick: function(e) { + var sbwh, scbv; + + sbwh = Tegaki.canvasCnt.offsetWidth - Tegaki.canvasCnt.clientWidth; + scbv = Tegaki.canvasCnt.offsetHeight - Tegaki.canvasCnt.clientHeight; + + if (sbwh > 0 + && e.clientX >= Tegaki.canvasCnt.offsetLeft + Tegaki.canvasCnt.clientWidth + && e.clientX <= Tegaki.canvasCnt.offsetLeft + Tegaki.canvasCnt.clientWidth + + sbwh) { + return true; + } + + if (scbv > 0 + && e.clientY >= Tegaki.canvasCnt.offsetTop + Tegaki.canvasCnt.clientHeight + && e.clientY <= Tegaki.canvasCnt.offsetTop + Tegaki.canvasCnt.clientHeight + + sbwh) { + return true; + } + + return false; + }, + + onPointerMove: function(e) { + var events, x, y, tool, ts, p; + + if (e.mozInputSource !== undefined) { + // Firefox thing where mouse events fire for no reason when the pointer is a pen + if (Tegaki.activePointerIsPen && e.pointerType === 'mouse') { + return; + } + } + else { + // Webkit thing where a pointermove event is fired at pointerdown location after a pointerup + if (Tegaki.activePointerId !== e.pointerId) { + Tegaki.activePointerId = e.pointerId; + return; + } + } + + if (Tegaki.isPainting) { + tool = Tegaki.tool; + + if (Tegaki.activePointerIsPen && e.getCoalescedEvents) { + events = e.getCoalescedEvents(); + + ts = e.timeStamp; + + for (e of events) { + x = Tegaki.getCursorPos(e, 0); + y = Tegaki.getCursorPos(e, 1); + + if (!tool.enabledDynamics()) { + Tegaki.recordEvent(TegakiEventDrawNoP, ts, x, y); + } + else { + p = TegakiPressure.toShort(e.pressure); + TegakiPressure.push(p); + Tegaki.recordEvent(TegakiEventDraw, ts, x, y, p); + } + + tool.draw(x, y); + } + } + else { + x = Tegaki.getCursorPos(e, 0); + y = Tegaki.getCursorPos(e, 1); + p = TegakiPressure.toShort(e.pressure); + Tegaki.recordEvent(TegakiEventDraw, e.timeStamp, x, y, p); + TegakiPressure.push(p); + tool.draw(x, y); + } + } + else { + x = Tegaki.getCursorPos(e, 0); + y = Tegaki.getCursorPos(e, 1); + } + + if (Tegaki.cursor) { + TegakiCursor.render(x, y); + } + }, + + onPointerDown: function(e) { + var x, y, tool, p; + + if (Tegaki.isScrollbarClick(e)) { + return; + } + + Tegaki.activePointerId = e.pointerId; + + Tegaki.activePointerIsPen = e.pointerType === 'pen'; + + if (Tegaki.activeLayer === null) { + if (e.target.parentNode === Tegaki.layersCnt) { + TegakiUI.printMsg(TegakiStrings.noActiveLayer); + } + + return; + } + + if (!TegakiLayers.getActiveLayer().visible) { + if (e.target.parentNode === Tegaki.layersCnt) { + TegakiUI.printMsg(TegakiStrings.hiddenActiveLayer); + } + + return; + } + + x = Tegaki.getCursorPos(e, 0); + y = Tegaki.getCursorPos(e, 1); + + if (e.button === 2 || e.altKey) { + e.preventDefault(); + + Tegaki.isPainting = false; + + Tegaki.tools.pipette.draw(x, y); + } + else if (e.button === 0) { + e.preventDefault(); + + tool = Tegaki.tool; + + if (!tool.enabledDynamics()) { + Tegaki.recordEvent(TegakiEventDrawStartNoP, e.timeStamp, x, y); + } + else { + p = TegakiPressure.toShort(e.pressure); + TegakiPressure.push(p); + Tegaki.recordEvent(TegakiEventDrawStart, e.timeStamp, x, y, p); + } + + Tegaki.isPainting = true; + + TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( + Tegaki.activeLayer.id + ); + + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); + + tool.start(x, y); + } + + if (Tegaki.cursor) { + TegakiCursor.render(x, y); + } + }, + + onPointerUp: function(e) { + Tegaki.activePointerId = e.pointerId; + + Tegaki.activePointerIsPen = false; + + if (Tegaki.isPainting) { + Tegaki.recordEvent(TegakiEventDrawCommit, e.timeStamp); + Tegaki.tool.commit(); + TegakiUI.updateLayerPreview(Tegaki.activeLayer); + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 1); + TegakiHistory.push(TegakiHistory.pendingAction); + Tegaki.isPainting = false; + } + }, + + onDummy: function(e) { + e.preventDefault(); + e.stopPropagation(); + }, + + recordEvent(klass, ...args) { + if (Tegaki.replayRecorder) { + Tegaki.replayRecorder.push(new klass(...args)); + } + } +}; +var TegakiColorPalettes = [ + [ + '#ffffff', '#000000', '#888888', '#b47575', '#c096c0', + '#fa9696', '#8080ff', '#ffb6ff', '#e7e58d', '#25c7c9', + '#99cb7b', '#e7962d', '#f9ddcf', '#fcece2' + ], + + [ + '#000000', '#ffffff', '#7f7f7f', '#c3c3c3', '#880015', '#b97a57', '#ed1c24', + '#ffaec9', '#ff7f27', '#ffc90e', '#fff200', '#efe4b0', '#22b14c', '#b5e61d', + '#00a2e8', '#99d9ea', '#3f48cc', '#7092be', '#a349a4', '#c8bfe7' + ], + + [ + '#000000', '#ffffff', '#8a8a8a', '#cacaca', '#fcece2', '#f9ddcf', '#e0a899', '#a05b53', + '#7a444a', '#960018', '#c41e3a', '#de4537', '#ff3300', '#ff9800', '#ffc107', + '#ffd700', '#ffeb3b', '#ffffcc', '#f3e5ab', '#cddc39', '#8bc34a', '#4caf50', '#3e8948', + '#355e3b', '#3eb489', '#f0f8ff', '#87ceeb', '#6699cc', '#007fff', '#2d68c4', '#364478', + '#352c4a', '#9c27b0', '#da70d6', '#ff0090', '#fa8072', '#f19cbb', '#c78b95' + ] +]; +var TegakiPressure = { + pressureNow: 0.0, + pressureThen: 0.0, + + toShort: function(pressure) { + return 0 | (pressure * 65535); + }, + + get: function() { + return this.pressureNow; + }, + + lerp: function(t) { + return this.pressureThen * (1.0 - t) + this.pressureNow * t; + }, + + push: function(p) { + this.pressureThen = this.pressureNow; + this.pressureNow = p / 65535; + }, + + set: function(p) { + this.pressureThen = this.pressureNow = p / 65535; + } +}; +class TegakiEvent_void { + constructor() { + this.size = 5; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + } + + static unpack(r) { + return new this(r.readUint32()); + } +} + +class TegakiEvent_c { + constructor() { + this.size = 6; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeUint8(this.value); + } + + static unpack(r) { + return new this(r.readUint32(), r.readUint8()); + } +} + +// --- + +class TegakiEventPrelude extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() {} +} + +class TegakiEventConclusion extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() {} +} + +class TegakiEventHistoryDummy extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + TegakiHistory.push(new TegakiHistoryActions.Dummy()); + } +} + +class TegakiEventDrawStart { + constructor(timeStamp, x, y, pressure) { + this.timeStamp = timeStamp; + this.x = x; + this.y = y; + this.pressure = pressure; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 11; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeInt16(this.x); + w.writeInt16(this.y); + w.writeUint16(this.pressure); + } + + static unpack(r) { + var timeStamp, x, y, pressure; + + timeStamp = r.readUint32(); + x = r.readInt16(); + y = r.readInt16(); + pressure = r.readUint16(); + + return new TegakiEventDrawStart(timeStamp, x, y, pressure); + } + + dispatch() { + TegakiPressure.set(this.pressure); + + TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( + Tegaki.activeLayer.id + ); + + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); + + Tegaki.tool.start(this.x, this.y); + } +} + +class TegakiEventDrawStartNoP { + constructor(timeStamp, x, y) { + this.timeStamp = timeStamp; + this.x = x; + this.y = y; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeInt16(this.x); + w.writeInt16(this.y); + } + + static unpack(r) { + var timeStamp, x, y; + + timeStamp = r.readUint32(); + x = r.readInt16(); + y = r.readInt16(); + + return new TegakiEventDrawStartNoP(timeStamp, x, y); + } + + dispatch() { + TegakiPressure.set(0.5); + + TegakiHistory.pendingAction = new TegakiHistoryActions.Draw( + Tegaki.activeLayer.id + ); + + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 0); + + Tegaki.tool.start(this.x, this.y); + } +} + +class TegakiEventDraw { + constructor(timeStamp, x, y, pressure) { + this.timeStamp = timeStamp; + this.x = x; + this.y = y; + this.pressure = pressure; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 11; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeInt16(this.x); + w.writeInt16(this.y); + w.writeUint16(this.pressure); + } + + static unpack(r) { + var timeStamp, x, y, pressure; + + timeStamp = r.readUint32(); + x = r.readInt16(); + y = r.readInt16(); + pressure = r.readUint16(); + + return new TegakiEventDraw(timeStamp, x, y, pressure); + } + + dispatch() { + TegakiPressure.push(this.pressure); + Tegaki.tool.draw(this.x, this.y); + } +} + +class TegakiEventDrawNoP { + constructor(timeStamp, x, y) { + this.timeStamp = timeStamp; + this.x = x; + this.y = y; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeInt16(this.x); + w.writeInt16(this.y); + } + + static unpack(r) { + var timeStamp, x, y; + + timeStamp = r.readUint32(); + x = r.readInt16(); + y = r.readInt16(); + + return new TegakiEventDraw(timeStamp, x, y); + } + + dispatch() { + TegakiPressure.push(0.5); + Tegaki.tool.draw(this.x, this.y); + } +} + +class TegakiEventDrawCommit extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.tool.commit(); + TegakiUI.updateLayerPreview(Tegaki.activeLayer); + TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData, 1); + TegakiHistory.push(TegakiHistory.pendingAction); + Tegaki.isPainting = false; + } +} + +class TegakiEventUndo extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + TegakiHistory.undo(); + } +} + +class TegakiEventRedo extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + TegakiHistory.redo(); + } +} + +class TegakiEventSetColor { + constructor(timeStamp, rgb) { + this.timeStamp = timeStamp; + [this.r, this.g, this.b] = rgb; + this.type = TegakiEvents[this.constructor.name][0]; + this.size = 8; + this.coalesce = true; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeUint8(this.r); + w.writeUint8(this.g); + w.writeUint8(this.b); + } + + static unpack(r) { + var timeStamp, rgb; + + timeStamp = r.readUint32(); + + rgb = [r.readUint8(), r.readUint8(), r.readUint8()]; + + return new TegakiEventSetColor(timeStamp, rgb); + } + + dispatch() { + Tegaki.setToolColorRGB(this.r, this.g, this.b); + } +} + +class TegakiEventSetTool extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolById(this.value); + } +} + +class TegakiEventSetToolSize extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolSize(this.value); + } +} + +class TegakiEventSetToolAlpha { + constructor(timeStamp, value) { + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeFloat32(this.value); + } + + static unpack(r) { + return new this(r.readUint32(), r.readFloat32()); + } + + dispatch() { + Tegaki.setToolAlpha(this.value); + } +} + +class TegakiEventSetToolFlow { + constructor(timeStamp, value) { + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeFloat32(this.value); + } + + static unpack(r) { + return new this(r.readUint32(), r.readFloat32()); + } + + dispatch() { + Tegaki.setToolFlow(this.value); + } +} + +class TegakiEventPreserveAlpha extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolPreserveAlpha(!!this.value); + } +} + +class TegakiEventSetToolSizeDynamics extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolSizeDynamics(!!this.value); + } +} + +class TegakiEventSetToolAlphaDynamics extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolAlphaDynamics(!!this.value); + } +} + +class TegakiEventSetToolFlowDynamics extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolFlowDynamics(!!this.value); + } +} + +class TegakiEventSetToolTip extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + } + + dispatch() { + Tegaki.setToolTip(this.value); + } +} + +class TegakiEventAddLayer extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.addLayer(); + } +} + +class TegakiEventDeleteLayers extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.deleteSelectedLayers(); + } +} + +class TegakiEventMoveLayers extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.moveSelectedLayers(this.value); + } +} + +class TegakiEventMergeLayers extends TegakiEvent_void { + constructor(timeStamp) { + super(); + this.timeStamp = timeStamp; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.mergeSelectedLayers(); + } +} + +class TegakiEventToggleLayerVisibility extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.toggleLayerVisibility(this.value); + } +} + +class TegakiEventSetActiveLayer extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.setActiveLayer(this.value); + } +} + +class TegakiEventToggleLayerSelection extends TegakiEvent_c { + constructor(timeStamp, value) { + super(); + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + } + + dispatch() { + Tegaki.toggleSelectedLayer(this.value); + } +} + +class TegakiEventSetSelectedLayersAlpha { + constructor(timeStamp, value) { + this.timeStamp = timeStamp; + this.value = value; + this.type = TegakiEvents[this.constructor.name][0]; + this.coalesce = true; + this.size = 9; + } + + pack(w) { + w.writeUint8(this.type); + w.writeUint32(this.timeStamp); + w.writeFloat32(this.value); + } + + static unpack(r) { + return new this(r.readUint32(), r.readFloat32()); + } + + dispatch() { + Tegaki.setSelectedLayersAlpha(this.value); + } +} + +const TegakiEvents = Object.freeze({ + TegakiEventPrelude: [0, TegakiEventPrelude], + + TegakiEventDrawStart: [1, TegakiEventDrawStart], + TegakiEventDraw: [2, TegakiEventDraw], + TegakiEventDrawCommit: [3, TegakiEventDrawCommit], + TegakiEventUndo: [4, TegakiEventUndo], + TegakiEventRedo: [5, TegakiEventRedo], + TegakiEventSetColor: [6, TegakiEventSetColor], + TegakiEventDrawStartNoP: [7, TegakiEventDrawStartNoP], + TegakiEventDrawNoP: [8, TegakiEventDrawNoP], + + TegakiEventSetTool: [10, TegakiEventSetTool], + TegakiEventSetToolSize: [11, TegakiEventSetToolSize], + TegakiEventSetToolAlpha: [12, TegakiEventSetToolAlpha], + TegakiEventSetToolSizeDynamics: [13, TegakiEventSetToolSizeDynamics], + TegakiEventSetToolAlphaDynamics: [14, TegakiEventSetToolAlphaDynamics], + TegakiEventSetToolTip: [15, TegakiEventSetToolTip], + TegakiEventPreserveAlpha: [16, TegakiEventPreserveAlpha], + TegakiEventSetToolFlowDynamics: [17, TegakiEventSetToolFlowDynamics], + TegakiEventSetToolFlow: [18, TegakiEventSetToolFlow], + + TegakiEventAddLayer: [20, TegakiEventAddLayer], + TegakiEventDeleteLayers: [21, TegakiEventDeleteLayers], + TegakiEventMoveLayers: [22, TegakiEventMoveLayers], + TegakiEventMergeLayers: [23, TegakiEventMergeLayers], + TegakiEventToggleLayerVisibility: [24, TegakiEventToggleLayerVisibility], + TegakiEventSetActiveLayer: [25, TegakiEventSetActiveLayer], + TegakiEventToggleLayerSelection: [26, TegakiEventToggleLayerSelection], + TegakiEventSetSelectedLayersAlpha: [27, TegakiEventSetSelectedLayersAlpha], + + TegakiEventHistoryDummy: [254, TegakiEventHistoryDummy], + + TegakiEventConclusion: [255, TegakiEventConclusion] +}); +class TegakiReplayRecorder { + constructor() { + this.formatVersion = 1; + + this.compressed = true; + + this.tegakiVersion = Tegaki.VERSION.split('.').map((v) => +v); + + this.canvasWidth = Tegaki.baseWidth; + this.canvasHeight = Tegaki.baseHeight; + + this.bgColor = $T.hexToRgb(Tegaki.bgColor); + this.toolColor = $T.hexToRgb(Tegaki.toolColor); + + this.toolId = Tegaki.tools[Tegaki.defaultTool].id; + + this.toolList = this.buildToolList(Tegaki.tools); + + this.startTimeStamp = 0; + this.endTimeStamp = 0; + + this.recording = false; + + this.events = []; + + this.mimeType = 'application/octet-stream'; + } + + buildToolList(tools) { + var k, tool, toolMap; + + toolMap = []; + + for (k in tools) { + tool = tools[k]; + + toolMap.push({ + id: tool.id, + size: tool.size, + alpha: tool.alpha, + flow: tool.flow, + step: tool.step, + sizeDynamicsEnabled: +tool.sizeDynamicsEnabled, + alphaDynamicsEnabled: +tool.alphaDynamicsEnabled, + flowDynamicsEnabled: +tool.flowDynamicsEnabled, + usePreserveAlpha: +tool.usePreserveAlpha, + tipId: tool.tipId + }); + } + + return toolMap; + } + + start() { + if (this.recording) { + return; + } + + if (this.endTimeStamp > 0) { + this.events.pop(); + this.endTimeStamp = 0; + } + + if (this.startTimeStamp === 0) { + this.events.push(new TegakiEventPrelude(performance.now())); + this.startTimeStamp = Date.now(); + } + + this.recording = true; + } + + stop() { + if (this.startTimeStamp === 0 || !this.recording) { + return; + } + + this.events.push(new TegakiEventConclusion(performance.now())); + this.endTimeStamp = Date.now(); + this.recording = false; + } + + push(e) { + if (this.recording) { + if (e.coalesce && this.events[this.events.length - 1].type === e.type) { + this.events[this.events.length - 1] = e; + } + else { + this.events.push(e); + } + } + } + + getEventStackSize() { + var e, size; + + size = 4; + + for (e of this.events) { + size += e.size; + } + + return size; + } + + getHeaderSize() { + return 12; + } + + getMetaSize() { + return 21; + } + + getToolSize() { + return 19; + } + + getToolListSize() { + return 2 + (this.toolList.length * this.getToolSize()); + } + + writeToolList(w) { + var tool, field, fields; + + fields = [ + ['id', 'Uint8'], + ['size', 'Uint8'], + ['alpha', 'Float32'], + ['step', 'Float32'], + ['sizeDynamicsEnabled', 'Uint8'], + ['alphaDynamicsEnabled', 'Uint8'], + ['usePreserveAlpha', 'Uint8'], + ['tipId', 'Int8'], + ['flow', 'Float32'], + ['flowDynamicsEnabled', 'Uint8'], + ]; + + w.writeUint8(this.toolList.length); + + w.writeUint8(this.getToolSize()); + + for (tool of this.toolList) { + for (field of fields) { + w['write' + field[1]](tool[field[0]]); + } + } + } + + writeMeta(w) { + w.writeUint16(this.getMetaSize()); + + w.writeUint32(Math.ceil(this.startTimeStamp / 1000)); + w.writeUint32(Math.ceil(this.endTimeStamp / 1000)); + + w.writeUint16(this.canvasWidth); + w.writeUint16(this.canvasHeight); + + w.writeUint8(this.bgColor[0]); + w.writeUint8(this.bgColor[1]); + w.writeUint8(this.bgColor[2]); + + w.writeUint8(this.toolColor[0]); + w.writeUint8(this.toolColor[1]); + w.writeUint8(this.toolColor[2]); + + w.writeUint8(this.toolId); + } + + writeEventStack(w) { + var event; + + w.writeUint32(this.events.length); + + for (event of this.events) { + event.pack(w); + } + } + + writeHeader(w, dataSize) { + w.writeUint8(0x54); + w.writeUint8(0x47); + w.writeUint8(0x4B); + + w.writeUint8(+this.compressed); + + w.writeUint32(dataSize); + + w.writeUint8(this.tegakiVersion[0]); + w.writeUint8(this.tegakiVersion[1]); + w.writeUint8(this.tegakiVersion[2]); + w.writeUint8(this.formatVersion); + } + + compressData(w) { + return UZIP.deflateRaw(new Uint8Array(w.buf), { level: 9 }); + } + + toUint8Array() { + var headerSize, dataSize, data, w, compData, bytes; + + if (!this.startTimeStamp || !this.endTimeStamp) { + return null; + } + + headerSize = this.getHeaderSize(); + dataSize = this.getMetaSize() + this.getToolListSize() + this.getEventStackSize(); + + data = new ArrayBuffer(dataSize); + + w = new TegakiBinWriter(data); + + this.writeMeta(w); + + this.writeToolList(w); + + this.writeEventStack(w); + + compData = this.compressData(w); + //compData = new Uint8Array(data.slice(0)); + + w = new TegakiBinWriter(new ArrayBuffer(headerSize + compData.length)); + + this.writeHeader(w, dataSize); + + bytes = new Uint8Array(w.buf); + + bytes.set(compData, headerSize); + + return bytes; + } + + toBlob() { + var ary = this.toUint8Array(); + + if (!ary) { + return null; + } + + return new Blob([ary.buffer], { type: this.mimeType }); + } +} +class TegakiReplayViewer { + constructor() { + this.formatVersion = 1; + + this.compressed = true; + + this.tegakiVersion = [0, 0, 0]; + + this.dataSize = 0; + + this.canvasWidth = 0; + this.canvasHeight = 0; + + this.bgColor = [0, 0, 0]; + this.toolColor = [0, 0, 0]; + + this.toolId = 1; + + this.toolMap = {}; + + this.startTimeStamp = 0; + this.endTimeStamp = 0; + + this.loaded = false; + this.playing = false; + this.gapless = true; + + this.autoPaused = false; + + this.destroyed = false; + + this.speedIndex = 1; + this.speedList = [0.5, 1.0, 2.0, 5.0, 10.0]; + this.speed = this.speedList[this.speedIndex]; + + this.maxEventsPerFrame = 25; + + this.maxEventCount = 8640000; + + this.events = []; + + this.preludePos = 0.0; + this.currentPos = 0.0; + this.conclusionPos = 0.0; + this.duration = 0.0; + + this.playTimeStart = 0.0; + this.playTimeCurrent = 0.0; + + this.eventIndex = 0; + + this.maxCanvasWH = 8192; + + this.maxGapTime = 3000; + + this.uiAccTime = 0; + + this.onFrameThis = this.onFrame.bind(this); + } + + destroy() { + this.destroyed = true; + this.pause(); + this.events = null; + } + + speedUp() { + if (this.speedIndex + 1 < this.speedList.length) { + this.speed = this.speedList[++this.speedIndex]; + } + } + + slowDown() { + if (this.speedIndex - 1 >= 0) { + this.speed = this.speedList[--this.speedIndex]; + } + } + + toggleGapless() { + this.gapless = !this.gapless; + } + + getCurrentPos() { + return this.currentPos; + } + + getDuration() { + return this.duration; + } + + loadFromURL(url) { + fetch(url) + .then((resp) => this.onResponseReady(resp)) + .catch((err) => this.onLoadError(err)); + } + + onResponseReady(resp) { + if (resp.ok) { + resp.arrayBuffer() + .then((buf) => this.onResponseBodyReady(buf)) + .catch((err) => this.onLoadError(err)); + } + else { + this.onLoadError(resp.statusText); + } + } + + onResponseBodyReady(data) { + this.loadFromBuffer(data); + Tegaki.onReplayLoaded(); + } + + onLoadError(err) { + TegakiUI.printMsg(TegakiStrings.errorLoadReplay + err, 0); + } + + autoPause() { + this.autoPaused = true; + this.pause(); + } + + pause(rewind) { + window.cancelAnimationFrame(this.onFrameThis); + + this.playing = false; + + if (rewind) { + this.currentPos = 0; + this.eventIndex = 0; + } + + Tegaki.onReplayTimeChanged(); + Tegaki.onReplayPlayPauseChanged(); + } + + rewind() { + this.autoPaused = false; + this.pause(true); + Tegaki.onReplayReset(); + } + + play() { + this.playTimeStart = performance.now(); + this.playTimeCurrent = this.playTimeStart; + + this.playing = true; + this.autoPaused = false; + + this.uiAccTime = 0; + + Tegaki.onReplayPlayPauseChanged(); + + window.requestAnimationFrame(this.onFrameThis); + } + + togglePlayPause() { + if (this.playing) { + this.pause(); + } + else { + this.play(); + } + } + + onFrame(ts) { + var delta = ts - this.playTimeCurrent; + + if (!this.playing) { + return; + } + + this.playTimeCurrent = ts; + + this.step(delta); + + this.uiAccTime += delta; + + if (this.uiAccTime > 1000) { + Tegaki.onReplayTimeChanged(); + this.uiAccTime = 0; + } + + if (this.currentPos < this.duration) { + window.requestAnimationFrame(this.onFrameThis); + } + else { + this.pause(); + } + } + + step(delta) { + var event, currentEventTime, i; + + this.currentPos += (delta * this.speed); + + currentEventTime = this.currentPos + this.preludePos; + + if (this.gapless && this.eventIndex < this.events.length) { + event = this.events[this.eventIndex]; + + if (event.timeStamp - currentEventTime > this.maxGapTime) { + this.currentPos = event.timeStamp - this.preludePos; + currentEventTime = event.timeStamp; + } + } + + i = 0; + + while (this.eventIndex < this.events.length) { + event = this.events[this.eventIndex]; + + if (event.timeStamp <= currentEventTime) { + if (i >= this.maxEventsPerFrame) { + this.currentPos = event.timeStamp - this.preludePos; + break; + } + + event.dispatch(); + + ++this.eventIndex; + ++i; + } + else { + break; + } + } + } + + getEventIdMap() { + var map, key, val; + + map = {}; + + for (key in TegakiEvents) { + val = TegakiEvents[key]; + map[val[0]] = val[1]; + } + + return map; + } + + readToolMap(r) { + var i, len, size, tool, field, fields, pos; + + this.toolMap = {}; + + fields = [ + ['id', 'Uint8'], + ['size', 'Uint8'], + ['alpha', 'Float32'], + ['step', 'Float32'], + ['sizeDynamicsEnabled', 'Uint8'], + ['alphaDynamicsEnabled', 'Uint8'], + ['usePreserveAlpha', 'Uint8'], + ['tipId', 'Int8'], + ['flow', 'Float32'], + ['flowDynamicsEnabled', 'Uint8'], + ]; + + len = r.readUint8(); + + size = r.readUint8(); + + for (i = 0; i < len; ++i) { + pos = r.pos + size; + + tool = {}; + + for (field of fields) { + if (r.pos >= pos) { + break; + } + + tool[field[0]] = r['read' + field[1]](); + } + + this.toolMap[tool.id] = tool; + + r.pos = pos; + } + } + + readHeader(r) { + var tgk; + + tgk = String.fromCharCode(r.readUint8(), r.readUint8(), r.readUint8()); + + if (tgk !== 'TGK') { + throw 'invalid header'; + } + + this.compressed = r.readUint8() === 1; + + this.dataSize = r.readUint32(); + + this.tegakiVersion[0] = r.readUint8(); + this.tegakiVersion[1] = r.readUint8(); + this.tegakiVersion[2] = r.readUint8(); + + this.formatVersion = r.readUint8(); + } + + decompressData(r) { + return UZIP.inflateRaw( + new Uint8Array(r.buf, r.pos), + new Uint8Array(this.dataSize) + ); + } + + readMeta(r) { + var pos, size; + + size = r.readUint16(); + + pos = r.pos + size - 2; + + this.startTimeStamp = r.readUint32() * 1000; + this.endTimeStamp = r.readUint32() * 1000; + + this.canvasWidth = r.readUint16(); + this.canvasHeight = r.readUint16(); + + if (this.canvasWidth > this.maxCanvasWH + || this.canvasHeight > this.maxCanvasWH) { + throw 'canvas too large'; + } + + this.bgColor[0] = r.readUint8(); + this.bgColor[1] = r.readUint8(); + this.bgColor[2] = r.readUint8(); + + this.toolColor[0] = r.readUint8(); + this.toolColor[1] = r.readUint8(); + this.toolColor[2] = r.readUint8(); + + this.toolId = r.readUint8(); + + r.pos = pos; + } + + readEventStack(r) { + var i, len, type, klass, event, eventMap; + + eventMap = this.getEventIdMap(); + + len = r.readUint32(); + + if (len < 1 || len > this.maxEventCount) { + throw 'invalid event count'; + } + + for (i = 0; i < len; ++i) { + type = r.readUint8(); + + klass = eventMap[type]; + + if (!klass) { + throw 'invalid event id'; + } + + event = klass.unpack(r); + + this.events.push(event); + } + + if (this.events[0].type !== TegakiEvents.TegakiEventPrelude[0]) { + throw 'invalid prelude'; + } + + if (this.events[len - 1].type !== TegakiEvents.TegakiEventConclusion[0]) { + throw 'invalid conclusion'; + } + + this.preludePos = this.events[0].timeStamp; + this.conclusionPos = this.events[len - 1].timeStamp; + + this.duration = this.conclusionPos - this.preludePos; + + if (this.duration <= 0) { + throw 'invalid duration'; + } + } + + loadFromBuffer(buffer) { + var r, data; + + if (this.destroyed || this.loaded) { + return false; + } + + r = new TegakiBinReader(buffer); + + this.readHeader(r); + + data = this.decompressData(r); + + r = new TegakiBinReader(data.buffer); + + this.readMeta(r); + + this.readToolMap(r); + + this.readEventStack(r); + + this.loaded = true; + + return true; + } +} +var TegakiUI = { + draggedNode: null, + + draggedLabelLastX: 0, + draggedLabelFn: null, + + statusTimeout: 0, + + layerPreviewCtxCache: new WeakMap(), + + getLayerPreviewSize: function() { + return $T.calcThumbSize(Tegaki.baseWidth, Tegaki.baseHeight, 24); + }, + + setupDragLabel: function(e, moveFn) { + TegakiUI.draggedLabelFn = moveFn; + TegakiUI.draggedLabelLastX = e.clientX; + $T.on(document, 'pointermove', TegakiUI.processDragLabel); + $T.on(document, 'pointerup', TegakiUI.clearDragLabel); + }, + + processDragLabel: function(e) { + TegakiUI.draggedLabelFn.call(Tegaki, e.clientX - TegakiUI.draggedLabelLastX); + TegakiUI.draggedLabelLastX = e.clientX; + }, + + clearDragLabel: function(e) { + $T.off(document, 'pointermove', TegakiUI.processDragLabel); + $T.off(document, 'pointerup', TegakiUI.clearDragLabel); + }, + + printMsg: function(str, timeout = 5000) { + TegakiUI.clearMsg(); + + $T.id('tegaki-status-output').textContent = str; + + if (timeout > 0) { + TegakiUI.statusTimeout = setTimeout(TegakiUI.clearMsg, 5000); + } + }, + + clearMsg: function() { + if (TegakiUI.statusTimeout) { + clearTimeout(TegakiUI.statusTimeout); + TegakiUI.statusTimeout = 0; + } + + $T.id('tegaki-status-output').textContent = ''; + }, + + buildUI: function() { + var bg, cnt, el, ctrl, layersCnt, canvasCnt; + + // + // Grid container + // + bg = $T.el('div'); + bg.id = 'tegaki'; + + // + // Menu area + // + el = $T.el('div'); + el.id = 'tegaki-menu-cnt'; + + if (!Tegaki.replayMode) { + el.appendChild(TegakiUI.buildMenuBar()); + } + else { + el.appendChild(TegakiUI.buildViewerMenuBar()); + el.appendChild(TegakiUI.buildReplayControls()); + } + + el.appendChild(TegakiUI.buildToolModeBar()); + + bg.appendChild(el); + + bg.appendChild(TegakiUI.buildDummyFilePicker()); + + // + // Tools area + // + cnt = $T.el('div'); + cnt.id = 'tegaki-tools-cnt'; + + cnt.appendChild(TegakiUI.buildToolsMenu()); + + bg.appendChild(cnt); + + // + // Canvas area + // + [canvasCnt, layersCnt] = TegakiUI.buildCanvasCnt(); + + bg.appendChild(canvasCnt); + + // + // Controls area + // + ctrl = $T.el('div'); + ctrl.id = 'tegaki-ctrl-cnt'; + + // Zoom control + ctrl.appendChild(TegakiUI.buildZoomCtrlGroup()); + + // Colorpicker + ctrl.appendChild( + TegakiUI.buildColorCtrlGroup(Tegaki.toolColor) + ); + + // Size control + ctrl.appendChild(TegakiUI.buildSizeCtrlGroup()); + + // Alpha control + ctrl.appendChild(TegakiUI.buildAlphaCtrlGroup()); + + // Flow control + ctrl.appendChild(TegakiUI.buildFlowCtrlGroup()); + + // Layers control + ctrl.appendChild(TegakiUI.buildLayersCtrlGroup()); + + // --- + + bg.appendChild(ctrl); + + // + // Status area + // + bg.appendChild(TegakiUI.buildStatusCnt()); + + return [bg, canvasCnt, layersCnt]; + }, + + buildDummyFilePicker: function() { + var el = $T.el('input'); + + el.type = 'file'; + el.id = 'tegaki-filepicker'; + el.className = 'tegaki-hidden'; + el.accept = 'image/png, image/jpeg'; + $T.on(el, 'change', Tegaki.onOpenFileSelected); + + return el; + }, + + buildMenuBar: function() { + var frag, btn; + + frag = $T.el('div'); + frag.id = 'tegaki-menu-bar'; + + btn = $T.el('span'); + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.newCanvas; + $T.on(btn, 'click', Tegaki.onNewClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.open; + $T.on(btn, 'click', Tegaki.onOpenClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.export; + $T.on(btn, 'click', Tegaki.onExportClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-undo-btn'; + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.undo; + btn.title = TegakiKeybinds.getCaption('undo'); + $T.on(btn, 'click', Tegaki.onUndoClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-redo-btn'; + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.redo; + btn.title = TegakiKeybinds.getCaption('redo'); + $T.on(btn, 'click', Tegaki.onRedoClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.close; + $T.on(btn, 'click', Tegaki.onCancelClick); + frag.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-finish-btn'; + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.finish; + $T.on(btn, 'click', Tegaki.onDoneClick); + frag.appendChild(btn); + + return frag; + }, + + buildViewerMenuBar: function() { + var frag, btn; + + frag = $T.el('div'); + frag.id = 'tegaki-menu-bar'; + + btn = $T.el('span'); + btn.id = 'tegaki-finish-btn'; + btn.className = 'tegaki-mb-btn'; + btn.textContent = TegakiStrings.close; + $T.on(btn, 'click', Tegaki.onCloseViewerClick); + frag.appendChild(btn); + + return frag; + }, + + buildToolModeBar: function() { + var cnt, grp, el, btn; + + cnt = $T.el('div'); + cnt.id = 'tegaki-toolmode-bar'; + + if (!Tegaki.tool) { + cnt.classList.add('tegaki-hidden'); + } + + // Dynamics + grp = $T.el('span'); + grp.id = 'tegaki-tool-mode-dynamics'; + grp.className = 'tegaki-toolmode-grp'; + + el = $T.el('span'); + el.className = 'tegaki-toolmode-lbl'; + el.textContent = TegakiStrings.pressure; + grp.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-tool-mode-dynamics-ctrl'; + el.className = 'tegaki-toolmode-ctrl'; + + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-dynamics-size'; + btn.className = 'tegaki-sw-btn'; + btn.textContent = TegakiStrings.size; + $T.on(btn, 'mousedown', Tegaki.onToolPressureSizeClick); + el.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-dynamics-alpha'; + btn.className = 'tegaki-sw-btn'; + btn.textContent = TegakiStrings.alpha; + $T.on(btn, 'mousedown', Tegaki.onToolPressureAlphaClick); + el.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-dynamics-flow'; + btn.className = 'tegaki-sw-btn'; + btn.textContent = TegakiStrings.flow; + $T.on(btn, 'mousedown', Tegaki.onToolPressureFlowClick); + el.appendChild(btn); + + grp.appendChild(el); + + cnt.appendChild(grp); + + // Preserve Alpha + grp = $T.el('span'); + grp.id = 'tegaki-tool-mode-mask'; + grp.className = 'tegaki-toolmode-grp'; + + el = $T.el('span'); + el.id = 'tegaki-toolmode-ctrl-tip'; + el.className = 'tegaki-toolmode-ctrl'; + + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-mask-alpha'; + btn.className = 'tegaki-sw-btn'; + btn.textContent = TegakiStrings.preserveAlpha; + $T.on(btn, 'mousedown', Tegaki.onToolPreserveAlphaClick); + el.appendChild(btn); + + grp.appendChild(el); + + cnt.appendChild(grp); + + // Tip + grp = $T.el('span'); + grp.id = 'tegaki-tool-mode-tip'; + grp.className = 'tegaki-toolmode-grp'; + + el = $T.el('span'); + el.className = 'tegaki-toolmode-lbl'; + el.textContent = TegakiStrings.tip; + grp.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-tool-mode-tip-ctrl'; + el.className = 'tegaki-toolmode-ctrl'; + grp.appendChild(el); + + cnt.appendChild(grp); + + return cnt; + }, + + buildToolsMenu: function() { + var grp, el, lbl, name; + + grp = $T.el('div'); + grp.id = 'tegaki-tools-grid'; + + for (name in Tegaki.tools) { + el = $T.el('span'); + el.setAttribute('data-tool', name); + + lbl = TegakiStrings[name]; + + if (Tegaki.tools[name].keybind) { + lbl += ' (' + Tegaki.tools[name].keybind.toUpperCase() + ')'; + } + + el.setAttribute('title', lbl); + el.id = 'tegaki-tool-btn-' + name; + el.className = 'tegaki-tool-btn tegaki-icon tegaki-' + name; + + $T.on(el, 'click', Tegaki.onToolClick); + + grp.appendChild(el); + } + + return grp; + }, + + buildCanvasCnt: function() { + var canvasCnt, wrap, layersCnt; + + canvasCnt = $T.el('div'); + canvasCnt.id = 'tegaki-canvas-cnt'; + + wrap = $T.el('div'); + wrap.id = 'tegaki-layers-wrap'; + + layersCnt = $T.el('div'); + layersCnt.id = 'tegaki-layers'; + + wrap.appendChild(layersCnt); + + canvasCnt.appendChild(wrap); + + return [canvasCnt, layersCnt]; + }, + + buildCtrlGroup: function(id, title) { + var cnt, el; + + cnt = $T.el('div'); + cnt.className = 'tegaki-ctrlgrp'; + + if (id) { + cnt.id = 'tegaki-ctrlgrp-' + id; + } + + if (title !== undefined) { + el = $T.el('div'); + el.className = 'tegaki-ctrlgrp-title'; + el.textContent = title; + cnt.appendChild(el); + } + + return cnt; + }, + + buildLayersCtrlGroup: function() { + var el, ctrl, row, cnt; + + ctrl = this.buildCtrlGroup('layers', TegakiStrings.layers); + + // Layer options row + row = $T.el('div'); + row.id = 'tegaki-layers-opts'; + + // Alpha + cnt = $T.el('div'); + cnt.id = 'tegaki-layer-alpha-cell'; + + el = $T.el('span'); + el.className = 'tegaki-label-xs tegaki-lbl-c tegaki-drag-lbl'; + el.textContent = TegakiStrings.alpha; + $T.on(el, 'pointerdown', Tegaki.onLayerAlphaDragStart); + cnt.appendChild(el); + + el = $T.el('input'); + el.id = 'tegaki-layer-alpha-opt'; + el.className = 'tegaki-stealth-input tegaki-range-lbl-xs'; + el.setAttribute('maxlength', 3); + $T.on(el, 'input', Tegaki.onLayerAlphaChange); + cnt.appendChild(el); + + row.appendChild(cnt); + + ctrl.appendChild(row); + + el = $T.el('div'); + el.id = 'tegaki-layers-grid'; + ctrl.appendChild(el); + + row = $T.el('div'); + row.id = 'tegaki-layers-ctrl'; + + el = $T.el('span'); + el.title = TegakiStrings.addLayer; + el.className = 'tegaki-ui-btn tegaki-icon tegaki-plus'; + $T.on(el, 'click', Tegaki.onLayerAddClick); + row.appendChild(el); + + el = $T.el('span'); + el.title = TegakiStrings.delLayers; + el.className = 'tegaki-ui-btn tegaki-icon tegaki-minus'; + $T.on(el, 'click', Tegaki.onLayerDeleteClick); + row.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-layer-merge'; + el.title = TegakiStrings.mergeLayers; + el.className = 'tegaki-ui-btn tegaki-icon tegaki-level-down'; + $T.on(el, 'click', Tegaki.onMergeLayersClick); + row.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-layer-up'; + el.title = TegakiStrings.moveLayerUp; + el.setAttribute('data-up', '1'); + el.className = 'tegaki-ui-btn tegaki-icon tegaki-up-open'; + $T.on(el, 'click', Tegaki.onMoveLayerClick); + row.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-layer-down'; + el.title = TegakiStrings.moveLayerDown; + el.className = 'tegaki-ui-btn tegaki-icon tegaki-down-open'; + $T.on(el, 'click', Tegaki.onMoveLayerClick); + row.appendChild(el); + + ctrl.appendChild(row); + + return ctrl; + }, + + buildSizeCtrlGroup: function() { + var el, ctrl, row; + + ctrl = this.buildCtrlGroup('size', TegakiStrings.size); + + row = $T.el('div'); + row.className = 'tegaki-ctrlrow'; + + el = $T.el('input'); + el.id = 'tegaki-size'; + el.className = 'tegaki-ctrl-range'; + el.min = 1; + el.max = Tegaki.maxSize; + el.type = 'range'; + el.title = TegakiKeybinds.getCaption('toolSize'); + $T.on(el, 'input', Tegaki.onToolSizeChange); + row.appendChild(el); + + el = $T.el('input'); + el.id = 'tegaki-size-lbl'; + el.setAttribute('maxlength', 3); + el.className = 'tegaki-stealth-input tegaki-range-lbl'; + $T.on(el, 'input', Tegaki.onToolSizeChange); + row.appendChild(el); + + ctrl.appendChild(row); + + return ctrl; + }, + + buildAlphaCtrlGroup: function() { + var el, ctrl, row; + + ctrl = this.buildCtrlGroup('alpha', TegakiStrings.alpha); + + row = $T.el('div'); + row.className = 'tegaki-ctrlrow'; + + el = $T.el('input'); + el.id = 'tegaki-alpha'; + el.className = 'tegaki-ctrl-range'; + el.min = 0; + el.max = 100; + el.step = 1; + el.type = 'range'; + $T.on(el, 'input', Tegaki.onToolAlphaChange); + row.appendChild(el); + + el = $T.el('input'); + el.id = 'tegaki-alpha-lbl'; + el.setAttribute('maxlength', 3); + el.className = 'tegaki-stealth-input tegaki-range-lbl'; + $T.on(el, 'input', Tegaki.onToolAlphaChange); + row.appendChild(el); + + ctrl.appendChild(row); + + return ctrl; + }, + + buildFlowCtrlGroup: function() { + var el, ctrl, row; + + ctrl = this.buildCtrlGroup('flow', TegakiStrings.flow); + + row = $T.el('div'); + row.className = 'tegaki-ctrlrow'; + + el = $T.el('input'); + el.id = 'tegaki-flow'; + el.className = 'tegaki-ctrl-range'; + el.min = 0; + el.max = 100; + el.step = 1; + el.type = 'range'; + $T.on(el, 'input', Tegaki.onToolFlowChange); + row.appendChild(el); + + el = $T.el('input'); + el.id = 'tegaki-flow-lbl'; + el.setAttribute('maxlength', 3); + el.className = 'tegaki-stealth-input tegaki-range-lbl'; + $T.on(el, 'input', Tegaki.onToolFlowChange); + row.appendChild(el); + + ctrl.appendChild(row); + + return ctrl; + }, + + buildZoomCtrlGroup: function() { + var el, btn, ctrl; + + ctrl = this.buildCtrlGroup('zoom', TegakiStrings.zoom); + + btn = $T.el('div'); + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-plus'; + btn.id = 'tegaki-zoomin-btn'; + btn.setAttribute('data-in', 1); + $T.on(btn, 'click', Tegaki.onZoomChange); + ctrl.appendChild(btn); + + btn = $T.el('div'); + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-minus'; + btn.id = 'tegaki-zoomout-btn'; + btn.setAttribute('data-out', 1); + $T.on(btn, 'click', Tegaki.onZoomChange); + ctrl.appendChild(btn); + + el = $T.el('div'); + el.id = 'tegaki-zoom-lbl'; + ctrl.appendChild(el); + + return ctrl; + }, + + buildColorCtrlGroup: function(mainColor) { + var el, cnt, btn, ctrl, color, edge, i, palette, cls; + + edge = / Edge\//i.test(window.navigator.userAgent); + + ctrl = this.buildCtrlGroup('color', TegakiStrings.color); + + cnt = $T.el('div'); + cnt.id = 'tegaki-color-ctrl'; + + el = $T.el('div'); + el.id = 'tegaki-color'; + edge && el.classList.add('tegaki-hidden'); + el.style.backgroundColor = mainColor; + $T.on(el, 'mousedown', Tegaki.onMainColorClick); + cnt.appendChild(el); + + el = $T.el('div'); + el.id = 'tegaki-palette-switcher'; + + btn = $T.el('span'); + btn.id = 'tegaki-palette-prev-btn'; + btn.title = TegakiStrings.switchPalette; + btn.setAttribute('data-prev', '1'); + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-left-open tegaki-disabled'; + $T.on(btn, 'click', Tegaki.onSwitchPaletteClick); + el.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-palette-next-btn'; + btn.title = TegakiStrings.switchPalette; + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-right-open'; + $T.on(btn, 'click', Tegaki.onSwitchPaletteClick); + el.appendChild(btn); + + cnt.appendChild(el); + + ctrl.appendChild(cnt); + + cnt = $T.el('div'); + cnt.id = 'tegaki-color-grids'; + + for (i = 0; i < TegakiColorPalettes.length; ++i) { + el = $T.el('div'); + + el.setAttribute('data-id', i); + + cls = 'tegaki-color-grid'; + + palette = TegakiColorPalettes[i]; + + if (palette.length <= 18) { + cls += ' tegaki-color-grid-20'; + } + else { + cls += ' tegaki-color-grid-15'; + } + + if (i > 0) { + cls += ' tegaki-hidden'; + } + + el.className = cls; + + for (color of palette) { + btn = $T.el('div'); + btn.title = TegakiStrings.paletteSlotReplace; + btn.className = 'tegaki-color-btn'; + btn.setAttribute('data-color', color); + btn.style.backgroundColor = color; + $T.on(btn, 'mousedown', Tegaki.onPaletteColorClick); + el.appendChild(btn); + } + + cnt.appendChild(el); + } + + ctrl.appendChild(cnt); + + el = $T.el('input'); + el.id = 'tegaki-colorpicker'; + !edge && el.classList.add('tegaki-hidden'); + el.value = color; + el.type = 'color'; + $T.on(el, 'change', Tegaki.onColorPicked); + + ctrl.appendChild(el); + + return ctrl; + }, + + buildStatusCnt: function() { + var cnt, el; + + cnt = $T.el('div'); + cnt.id = 'tegaki-status-cnt'; + + if (Tegaki.saveReplay) { + el = $T.el('div'); + el.id = 'tegaki-status-replay'; + el.textContent = '⬤'; + el.setAttribute('title', TegakiStrings.recordingEnabled); + cnt.appendChild(el); + } + + el = $T.el('div'); + el.id = 'tegaki-status-output'; + cnt.appendChild(el); + + el = $T.el('div'); + el.id = 'tegaki-status-version'; + el.textContent = 'tegaki.js v' + Tegaki.VERSION; + cnt.appendChild(el); + + return cnt; + }, + + buildReplayControls: function() { + var cnt, btn, el; + + cnt = $T.el('div'); + cnt.id = 'tegaki-replay-controls'; + cnt.className = 'tegaki-hidden'; + + btn = $T.el('span'); + btn.id = 'tegaki-replay-gapless-btn'; + btn.className = 'tegaki-ui-cb-w'; + $T.on(btn, 'click', Tegaki.onReplayGaplessClick); + + el = $T.el('span'); + el.id = 'tegaki-replay-gapless-cb'; + el.className = 'tegaki-ui-cb'; + btn.appendChild(el); + + el = $T.el('span'); + el.className = 'tegaki-menu-lbl'; + el.textContent = TegakiStrings.gapless; + btn.appendChild(el); + + cnt.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-replay-play-btn'; + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-play'; + btn.setAttribute('title', TegakiStrings.play); + $T.on(btn, 'click', Tegaki.onReplayPlayPauseClick); + cnt.appendChild(btn); + + btn = $T.el('span'); + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-to-start'; + btn.setAttribute('title', TegakiStrings.rewind); + $T.on(btn, 'click', Tegaki.onReplayRewindClick); + cnt.appendChild(btn); + + btn = $T.el('span'); + btn.id = 'tegaki-replay-slower-btn'; + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-fast-bw'; + btn.setAttribute('title', TegakiStrings.slower); + $T.on(btn, 'click', Tegaki.onReplaySlowDownClick); + cnt.appendChild(btn); + + el = $T.el('span'); + el.id = 'tegaki-replay-speed-lbl'; + el.className = 'tegaki-menu-lbl'; + el.textContent = '1.0'; + cnt.appendChild(el); + + btn = $T.el('span'); + btn.id = 'tegaki-replay-faster-btn'; + btn.className = 'tegaki-ui-btn tegaki-icon tegaki-fast-fw'; + btn.setAttribute('title', TegakiStrings.faster); + $T.on(btn, 'click', Tegaki.onReplaySpeedUpClick); + cnt.appendChild(btn); + + el = $T.el('span'); + el.id = 'tegaki-replay-now-lbl'; + el.className = 'tegaki-menu-lbl'; + el.textContent = '00:00'; + cnt.appendChild(el); + + el = $T.el('span'); + el.id = 'tegaki-replay-end-lbl'; + el.className = 'tegaki-menu-lbl'; + el.textContent = '00:00'; + cnt.appendChild(el); + + return cnt; + }, + + buildLayerGridCell: function(layer) { + var cnt, el, cell; + + cnt = $T.el('div'); + cnt.id = 'tegaki-layers-cell-' + layer.id; + cnt.className = 'tegaki-layers-cell'; + cnt.setAttribute('data-id', layer.id); + cnt.draggable = true; + cnt.setAttribute('data-id', layer.id); + + $T.on(cnt, 'pointerdown', TegakiUI.onLayerSelectorPtrDown); + $T.on(cnt, 'pointerup', Tegaki.onLayerSelectorClick); + + $T.on(cnt, 'dragstart', TegakiUI.onLayerDragStart); + $T.on(cnt, 'dragover', TegakiUI.onLayerDragOver); + $T.on(cnt, 'drop', TegakiUI.onLayerDragDrop); + $T.on(cnt, 'dragend', TegakiUI.onLayerDragEnd); + $T.on(cnt, 'dragleave', TegakiUI.onLayerDragLeave); + $T.on(cnt, 'dragexit', TegakiUI.onLayerDragLeave); + + // visibility toggle + cell = $T.el('div'); + cell.className = 'tegaki-layers-cell-v'; + + el = $T.el('span'); + el.id = 'tegaki-layers-cb-v-' + layer.id; + el.className = 'tegaki-ui-cb'; + el.setAttribute('data-id', layer.id); + el.title = TegakiStrings.toggleVisibility; + $T.on(el, 'click', Tegaki.onLayerToggleVisibilityClick); + + if (layer.visible) { + el.className += ' tegaki-ui-cb-a'; + } + + cell.appendChild(el); + cnt.appendChild(cell); + + // preview + cell = $T.el('div'); + cell.className = 'tegaki-layers-cell-p'; + + el = $T.el('canvas'); + el.id = 'tegaki-layers-p-canvas-' + layer.id; + el.className = 'tegaki-alpha-bg-xs'; + [el.width, el.height] = TegakiUI.getLayerPreviewSize(); + + cell.appendChild(el); + cnt.appendChild(cell); + + // name + cell = $T.el('div'); + cell.className = 'tegaki-layers-cell-n'; + + el = $T.el('div'); + el.id = 'tegaki-layer-name-' + layer.id; + el.className = 'tegaki-ellipsis'; + el.setAttribute('data-id', layer.id); + el.textContent = layer.name; + $T.on(el, 'dblclick', Tegaki.onLayerNameChangeClick); + + cell.appendChild(el); + cnt.appendChild(cell); + + return cnt; + }, + + // --- + + onLayerSelectorPtrDown: function(e) { + if (e.pointerType === 'mouse') { + if (this.hasAttribute('data-nodrag')) { + this.removeAttribute('data-nodrag'); + $T.on(this, 'dragstart', TegakiUI.onLayerDragStart); + } + } + else if (!this.hasAttribute('data-nodrag')) { + this.setAttribute('data-nodrag', 1); + $T.off(this, 'dragstart', TegakiUI.onLayerDragStart); + } + }, + + onLayerDragStart: function(e) { + var el, id; + + if (e.ctrlKey) { + return; + } + + TegakiUI.draggedNode = null; + + if (!$T.id('tegaki-layers-grid').children[1]) { + e.preventDefault(); + return; + } + + id = +e.target.getAttribute('data-id'); + + el = $T.el('div'); + el.className = 'tegaki-invis'; + e.dataTransfer.setDragImage(el, 0, 0); + e.dataTransfer.setData('text/plain', id); + e.dataTransfer.effectAllowed = 'move'; + + TegakiUI.draggedNode = e.target; + + TegakiUI.updateLayersGridDragExt(true); + }, + + onLayerDragOver: function(e) { + e.preventDefault(); + + e.dataTransfer.dropEffect = 'move'; + + TegakiUI.updateLayersGridDragEffect( + e.target, + +TegakiUI.draggedNode.getAttribute('data-id') + ); + }, + + onLayerDragLeave: function(e) { + TegakiUI.updateLayersGridDragEffect(); + }, + + onLayerDragEnd: function(e) { + TegakiUI.draggedNode = null; + TegakiUI.updateLayersGridDragExt(false); + TegakiUI.updateLayersGridDragEffect(); + }, + + onLayerDragDrop: function(e) { + var tgtId, srcId, belowPos; + + e.preventDefault(); + + TegakiUI.draggedNode = null; + + [tgtId] = TegakiUI.layersGridFindDropTgt(e.target); + srcId = +e.dataTransfer.getData('text/plain'); + + TegakiUI.updateLayersGridDragEffect(e.target.parentNode); + TegakiUI.updateLayersGridDragExt(false); + + if (!TegakiUI.layersGridCanDrop(tgtId, srcId)) { + return; + } + + if (!tgtId) { + belowPos = Tegaki.layers.length; + } + else { + belowPos = TegakiLayers.getLayerPosById(tgtId); + } + + if (!TegakiLayers.selectedLayersHas(srcId)) { + Tegaki.setActiveLayer(srcId); + } + + Tegaki.moveSelectedLayers(belowPos); + }, + + updateLayersGridDragExt: function(flag) { + var cnt, el; + + cnt = $T.id('tegaki-layers-grid'); + + if (!cnt.children[1]) { + return; + } + + if (flag) { + el = $T.el('div'); + el.id = 'tegaki-layers-cell-dx'; + el.draggable = true; + $T.on(el, 'dragover', TegakiUI.onLayerDragOver); + $T.on(el, 'drop', TegakiUI.onLayerDragDrop); + cnt.parentNode.insertBefore(el, cnt); + } + else { + if (el = $T.id('tegaki-layers-cell-dx')) { + el.parentNode.removeChild(el); + } + } + }, + + updateLayersGridDragEffect: function(tgt, srcId) { + var el, nodes, tgtId; + + nodes = $T.cls('tegaki-layers-cell-d', $T.id('tegaki-ctrlgrp-layers')); + + for (el of nodes) { + el.classList.remove('tegaki-layers-cell-d'); + } + + if (!tgt || !srcId) { + return; + } + + [tgtId, tgt] = TegakiUI.layersGridFindDropTgt(tgt); + + if (!TegakiUI.layersGridCanDrop(tgtId, srcId)) { + return; + } + + if (!tgt) { + tgt = $T.id('tegaki-layers-grid'); + } + + tgt.classList.add('tegaki-layers-cell-d'); + }, + + layersGridFindDropTgt: function(tgt) { + var tgtId, cnt; + + tgtId = +tgt.getAttribute('data-id'); + + cnt = $T.id('tegaki-ctrlgrp-layers'); + + while (!tgt.draggable && tgt !== cnt) { + tgt = tgt.parentNode; + tgtId = +tgt.getAttribute('data-id'); + } + + if (tgt === cnt || !tgt.draggable) { + return [0, null]; + } + + return [tgtId, tgt]; + }, + + layersGridCanDrop: function(tgtId, srcId) { + var srcEl; + + if (tgtId === srcId) { + return false; + } + + srcEl = $T.id('tegaki-layers-cell-' + srcId); + + if (!srcEl.previousElementSibling) { + if (!tgtId) { + return false; + } + } + else if (+srcEl.previousElementSibling.getAttribute('data-id') === tgtId) { + return false; + } + + return true; + }, + + // --- + + setReplayMode: function(flag) { + Tegaki.bg.classList[flag ? 'add' : 'remove']('tegaki-replay-mode'); + }, + + // --- + + onToolChanged: function() { + $T.id('tegaki-toolmode-bar').classList.remove('tegaki-hidden'); + TegakiUI.updateToolSize(); + TegakiUI.updateToolAlpha(); + TegakiUI.updateToolFlow(); + TegakiUI.updateToolModes(); + }, + + // --- + + updateLayerAlphaOpt: function() { + var el = $T.id('tegaki-layer-alpha-opt'); + el.value = Math.round(Tegaki.activeLayer.alpha * 100); + }, + + updateLayerName: function(layer) { + var el; + + if (el = $T.id('tegaki-layer-name-' + layer.id)) { + el.textContent = layer.name; + } + }, + + updateLayerPreview: function(layer) { + var canvas, ctx; + + canvas = $T.id('tegaki-layers-p-canvas-' + layer.id); + + if (!canvas) { + return; + } + + ctx = TegakiUI.getLayerPreviewCtx(layer); + + if (!ctx) { + ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = false; + TegakiUI.setLayerPreviewCtx(layer, ctx); + } + + $T.clearCtx(ctx); + ctx.drawImage(layer.canvas, 0, 0, canvas.width, canvas.height); + }, + + updateLayerPreviewSize: function(regen) { + var el, layer, size; + + size = TegakiUI.getLayerPreviewSize(); + + for (layer of Tegaki.layers) { + if (el = $T.id('tegaki-layers-p-canvas-' + layer.id)) { + [el.width, el.height] = size; + + if (regen) { + TegakiUI.updateLayerPreview(layer); + } + } + } + }, + + getLayerPreviewCtx: function(layer) { + TegakiUI.layerPreviewCtxCache.get(layer); + }, + + setLayerPreviewCtx: function(layer, ctx) { + TegakiUI.layerPreviewCtxCache.set(layer, ctx); + }, + + deleteLayerPreviewCtx: function(layer) { + TegakiUI.layerPreviewCtxCache.delete(layer); + }, + + updateLayersGridClear: function() { + $T.id('tegaki-layers-grid').innerHTML = ''; + }, + + updateLayersGrid: function() { + var layer, el, frag, cnt; + + frag = $T.frag(); + + for (layer of Tegaki.layers) { + el = TegakiUI.buildLayerGridCell(layer); + frag.insertBefore(el, frag.firstElementChild); + } + + TegakiUI.updateLayersGridClear(); + + cnt.appendChild(frag); + }, + + updateLayersGridActive: function(layerId) { + var el; + + el = $T.cls('tegaki-layers-cell-a', $T.id('tegaki-layers-grid'))[0]; + + if (el) { + el.classList.remove('tegaki-layers-cell-a'); + } + + el = $T.id('tegaki-layers-cell-' + layerId); + + if (el) { + el.classList.add('tegaki-layers-cell-a'); + } + + TegakiUI.updateLayerAlphaOpt(); + }, + + updateLayersGridAdd: function(layer, aboveId) { + var el, cnt, ref; + + el = TegakiUI.buildLayerGridCell(layer); + + cnt = $T.id('tegaki-layers-grid'); + + if (aboveId) { + ref = $T.id('tegaki-layers-cell-' + aboveId); + } + else { + ref = null; + } + + cnt.insertBefore(el, ref); + }, + + updateLayersGridRemove: function(id) { + var el; + + if (el = $T.id('tegaki-layers-cell-' + id)) { + el.parentNode.removeChild(el); + } + }, + + updayeLayersGridOrder: function() { + var layer, cnt, el; + + cnt = $T.id('tegaki-layers-grid'); + + for (layer of Tegaki.layers) { + el = $T.id('tegaki-layers-cell-' + layer.id); + cnt.insertBefore(el, cnt.firstElementChild); + } + }, + + updateLayersGridVisibility: function(id, flag) { + var el; + + el = $T.id('tegaki-layers-cb-v-' + id); + + if (!el) { + return; + } + + if (flag) { + el.classList.add('tegaki-ui-cb-a'); + } + else { + el.classList.remove('tegaki-ui-cb-a'); + } + }, + + updateLayersGridSelectedClear: function() { + var layer, el; + + for (layer of Tegaki.layers) { + if (el = $T.id('tegaki-layers-cell-' + layer.id)) { + el.classList.remove('tegaki-layers-cell-s'); + } + } + }, + + updateLayersGridSelectedSet: function(id, flag) { + var el; + + if (el = $T.id('tegaki-layers-cell-' + id)) { + if (flag) { + el.classList.add('tegaki-layers-cell-s'); + } + else { + el.classList.remove('tegaki-layers-cell-s'); + } + } + }, + + updateToolSize: function() { + var el = $T.id('tegaki-ctrlgrp-size'); + + if (Tegaki.tool.useSize) { + el.classList.remove('tegaki-hidden'); + + $T.id('tegaki-size-lbl').value = Tegaki.tool.size; + $T.id('tegaki-size').value = Tegaki.tool.size; + } + else { + el.classList.add('tegaki-hidden'); + } + }, + + updateToolAlpha: function() { + var val, el = $T.id('tegaki-ctrlgrp-alpha'); + + if (Tegaki.tool.useAlpha) { + el.classList.remove('tegaki-hidden'); + + val = Math.round(Tegaki.tool.alpha * 100); + $T.id('tegaki-alpha-lbl').value = val; + $T.id('tegaki-alpha').value = val; + } + else { + el.classList.add('tegaki-hidden'); + } + }, + + updateToolFlow: function() { + var val, el = $T.id('tegaki-ctrlgrp-flow'); + + if (Tegaki.tool.useFlow) { + el.classList.remove('tegaki-hidden'); + + val = Math.round(Tegaki.tool.flow * 100); + $T.id('tegaki-flow-lbl').value = val; + $T.id('tegaki-flow').value = val; + } + else { + el.classList.add('tegaki-hidden'); + } + }, + + updateToolDynamics: function() { + var ctrl, cb; + + ctrl = $T.id('tegaki-tool-mode-dynamics'); + + if (!Tegaki.tool.usesDynamics()) { + ctrl.classList.add('tegaki-hidden'); + } + else { + cb = $T.id('tegaki-tool-mode-dynamics-size'); + + if (Tegaki.tool.useSizeDynamics) { + if (Tegaki.tool.sizeDynamicsEnabled) { + cb.classList.add('tegaki-sw-btn-a'); + } + else { + cb.classList.remove('tegaki-sw-btn-a'); + } + + cb.classList.remove('tegaki-hidden'); + } + else { + cb.classList.add('tegaki-hidden'); + } + + cb = $T.id('tegaki-tool-mode-dynamics-alpha'); + + if (Tegaki.tool.useAlphaDynamics) { + if (Tegaki.tool.alphaDynamicsEnabled) { + cb.classList.add('tegaki-sw-btn-a'); + } + else { + cb.classList.remove('tegaki-sw-btn-a'); + } + + cb.classList.remove('tegaki-hidden'); + } + else { + cb.classList.add('tegaki-hidden'); + } + + cb = $T.id('tegaki-tool-mode-dynamics-flow'); + + if (Tegaki.tool.useFlowDynamics) { + if (Tegaki.tool.flowDynamicsEnabled) { + cb.classList.add('tegaki-sw-btn-a'); + } + else { + cb.classList.remove('tegaki-sw-btn-a'); + } + + cb.classList.remove('tegaki-hidden'); + } + else { + cb.classList.add('tegaki-hidden'); + } + + ctrl.classList.remove('tegaki-hidden'); + } + }, + + updateToolShape: function() { + var tipId, ctrl, cnt, btn, tipList; + + ctrl = $T.id('tegaki-tool-mode-tip'); + + if (!Tegaki.tool.tipList) { + ctrl.classList.add('tegaki-hidden'); + } + else { + tipList = Tegaki.tool.tipList; + + cnt = $T.id('tegaki-tool-mode-tip-ctrl'); + + cnt.innerHTML = ''; + + for (tipId = 0; tipId < tipList.length; ++tipId) { + btn = $T.el('span'); + btn.id = 'tegaki-tool-mode-tip-' + tipId; + btn.className = 'tegaki-sw-btn'; + btn.setAttribute('data-id', tipId); + btn.textContent = TegakiStrings[tipList[tipId]]; + + $T.on(btn, 'mousedown', Tegaki.onToolTipClick); + + cnt.appendChild(btn); + + if (Tegaki.tool.tipId === tipId) { + btn.classList.add('tegaki-sw-btn-a'); + } + } + + ctrl.classList.remove('tegaki-hidden'); + } + }, + + updateToolPreserveAlpha: function() { + var cb, ctrl; + + ctrl = $T.id('tegaki-tool-mode-mask'); + + if (!Tegaki.tool.usePreserveAlpha) { + ctrl.classList.add('tegaki-hidden'); + } + else { + cb = $T.id('tegaki-tool-mode-mask-alpha'); + + if (Tegaki.tool.preserveAlphaEnabled) { + cb.classList.add('tegaki-sw-btn-a'); + } + else { + cb.classList.remove('tegaki-sw-btn-a'); + } + + ctrl.classList.remove('tegaki-hidden'); + } + }, + + updateToolModes: function() { + var el, flag; + + TegakiUI.updateToolShape(); + TegakiUI.updateToolDynamics(); + TegakiUI.updateToolPreserveAlpha(); + + flag = false; + + for (el of $T.id('tegaki-toolmode-bar').children) { + if (!flag && !el.classList.contains('tegaki-hidden')) { + el.classList.add('tegaki-ui-borderless'); + flag = true; + } + else { + el.classList.remove('tegaki-ui-borderless'); + } + } + }, + + updateUndoRedo: function(undoSize, redoSize) { + var u, r; + + if (Tegaki.replayMode) { + return; + } + + u = $T.id('tegaki-undo-btn').classList; + r = $T.id('tegaki-redo-btn').classList; + + if (undoSize) { + if (u.contains('tegaki-disabled')) { + u.remove('tegaki-disabled'); + } + } + else { + if (!u.contains('tegaki-disabled')) { + u.add('tegaki-disabled'); + } + } + + if (redoSize) { + if (r.contains('tegaki-disabled')) { + r.remove('tegaki-disabled'); + } + } + else { + if (!r.contains('tegaki-disabled')) { + r.add('tegaki-disabled'); + } + } + }, + + updateZoomLevel: function() { + $T.id('tegaki-zoom-lbl').textContent = (Tegaki.zoomFactor * 100) + '%'; + + if (Tegaki.zoomLevel + Tegaki.zoomBaseLevel >= Tegaki.zoomFactorList.length) { + $T.id('tegaki-zoomin-btn').classList.add('tegaki-disabled'); + } + else { + $T.id('tegaki-zoomin-btn').classList.remove('tegaki-disabled'); + } + + if (Tegaki.zoomLevel + Tegaki.zoomBaseLevel <= 0) { + $T.id('tegaki-zoomout-btn').classList.add('tegaki-disabled'); + } + else { + $T.id('tegaki-zoomout-btn').classList.remove('tegaki-disabled'); + } + }, + + updateColorPalette: function() { + var el, nodes, id; + + id = Tegaki.colorPaletteId; + + nodes = $T.cls('tegaki-color-grid', $T.id('tegaki-color-grids')); + + for (el of nodes) { + if (+el.getAttribute('data-id') === id) { + el.classList.remove('tegaki-hidden'); + } + else { + el.classList.add('tegaki-hidden'); + } + } + + el = $T.id('tegaki-palette-prev-btn'); + + if (id === 0) { + el.classList.add('tegaki-disabled'); + } + else { + el.classList.remove('tegaki-disabled'); + } + + el = $T.id('tegaki-palette-next-btn'); + + if (id === TegakiColorPalettes.length - 1) { + el.classList.add('tegaki-disabled'); + } + else { + el.classList.remove('tegaki-disabled'); + } + }, + + updateReplayTime: function(full) { + var now, end, r = Tegaki.replayViewer; + + now = r.getCurrentPos(); + + end = r.getDuration(); + + if (now > end) { + now = end; + } + + $T.id('tegaki-replay-now-lbl').textContent = $T.msToHms(now); + + if (full) { + $T.id('tegaki-replay-end-lbl').textContent = $T.msToHms(end); + } + }, + + updateReplayControls: function() { + TegakiUI.updateReplayGapless(); + TegakiUI.updateReplayPlayPause(); + TegakiUI.updateReplaySpeed(); + }, + + updateReplayGapless: function() { + var el, r = Tegaki.replayViewer; + + el = $T.id('tegaki-replay-gapless-cb'); + + if (r.gapless) { + el.classList.add('tegaki-ui-cb-a'); + } + else { + el.classList.remove('tegaki-ui-cb-a'); + } + }, + + updateReplayPlayPause: function() { + var el, r = Tegaki.replayViewer; + + el = $T.id('tegaki-replay-play-btn'); + + if (r.playing) { + el.classList.remove('tegaki-play'); + el.classList.add('tegaki-pause'); + el.setAttribute('title', TegakiStrings.pause); + } + else { + el.classList.add('tegaki-play'); + el.classList.remove('tegaki-pause'); + el.setAttribute('title', TegakiStrings.play); + + if (r.getCurrentPos() < r.getDuration()) { + el.classList.remove('tegaki-disabled'); + } + else { + el.classList.add('tegaki-disabled'); + } + } + }, + + updateReplaySpeed: function() { + var el, r = Tegaki.replayViewer; + + $T.id('tegaki-replay-speed-lbl').textContent = r.speed.toFixed(1); + + el = $T.id('tegaki-replay-slower-btn'); + + if (r.speedIndex === 0) { + el.classList.add('tegaki-disabled'); + } + else { + el.classList.remove('tegaki-disabled'); + } + + el = $T.id('tegaki-replay-faster-btn'); + + if (r.speedIndex === r.speedList.length - 1) { + el.classList.add('tegaki-disabled'); + } + else { + el.classList.remove('tegaki-disabled'); + } + }, + + enableReplayControls: function(flag) { + if (flag) { + $T.id('tegaki-replay-controls').classList.remove('tegaki-hidden'); + } + else { + $T.id('tegaki-replay-controls').classList.add('tegaki-hidden'); + } + }, + + setRecordingStatus: function(flag) { + var el = $T.id('tegaki-status-replay'); + + if (flag) { + el.classList.remove('tegaki-hidden'); + } + else { + el.classList.add('tegaki-hidden'); + } + } +}; +/*! UZIP.js, © 2018 Photopea, MIT License */ + +var UZIP = {}; +if(typeof module == "object") module.exports = UZIP; + +UZIP.inflateRaw = function(file, buf) { return UZIP.F.inflate(file, buf); } + +UZIP.deflateRaw = function(data, opts) { + if(opts==null) opts={level:6}; + var buf=new Uint8Array(50+Math.floor(data.length*1.1)); + var off = UZIP.F.deflateRaw(data, buf, off, opts.level); + return new Uint8Array(buf.buffer, 0, off); +} + +UZIP.bin = { + readUshort : function(buff,p) { return (buff[p]) | (buff[p+1]<<8); }, + writeUshort: function(buff,p,n){ buff[p] = (n)&255; buff[p+1] = (n>>8)&255; }, + readUint : function(buff,p) { return (buff[p+3]*(256*256*256)) + ((buff[p+2]<<16) | (buff[p+1]<< 8) | buff[p]); }, + writeUint : function(buff,p,n){ buff[p]=n&255; buff[p+1]=(n>>8)&255; buff[p+2]=(n>>16)&255; buff[p+3]=(n>>24)&255; }, + readASCII : function(buff,p,l){ var s = ""; for(var i=0; i> 6)); buff[p+i+1] = (128|((code>> 0)&63)); i+=2; } + else if((code&(0xffffffff-(1<<16)+1))==0) { buff[p+i] = (224|(code>>12)); buff[p+i+1] = (128|((code>> 6)&63)); buff[p+i+2] = (128|((code>>0)&63)); i+=3; } + else if((code&(0xffffffff-(1<<21)+1))==0) { buff[p+i] = (240|(code>>18)); buff[p+i+1] = (128|((code>>12)&63)); buff[p+i+2] = (128|((code>>6)&63)); buff[p+i+3] = (128|((code>>0)&63)); i+=4; } + else throw "e"; + } + return i; + }, + sizeUTF8 : function(str) { + var strl = str.length, i=0; + for(var ci=0; ci>>3; + } + + var lits = U.lits, strt=U.strt, prev=U.prev, li=0, lc=0, bs=0, ebits=0, c=0, nc=0; // last_item, literal_count, block_start + if(dlen>2) { nc=UZIP.F._hash(data,0); strt[nc]=0; } + var nmch=0,nmci=0; + + for(i=0; i14000 || lc>26697) && (dlen-i)>100) { + if(cvrd>>16)>>16)>(mch>>>16)) mch=0; + }//*/ + var len = mch>>>16, dst = mch&0xffff; //if(i-dst<0) throw "e"; + if(mch!=0) { + var len = mch>>>16, dst = mch&0xffff; //if(i-dst<0) throw "e"; + var lgi = goodIndex(len, U.of0); U.lhst[257+lgi]++; + var dgi = goodIndex(dst, U.df0); U.dhst[ dgi]++; ebits += U.exb[lgi] + U.dxb[dgi]; + lits[li] = (len<<23)|(i-cvrd); lits[li+1] = (dst<<16)|(lgi<<8)|dgi; li+=2; + cvrd = i + len; + } + else { U.lhst[data[i]]++; } + lc++; + } + } + if(bs!=i || data.length==0) { + if(cvrd>>3; +} +UZIP.F._bestMatch = function(data, i, prev, c, nice, chain) { + var ci = (i&0x7fff), pi=prev[ci]; + //console.log("----", i); + var dif = ((ci-pi + (1<<15)) & 0x7fff); if(pi==ci || c!=UZIP.F._hash(data,i-dif)) return 0; + var tl=0, td=0; // top length, top distance + var dlim = Math.min(0x7fff, i); + while(dif<=dlim && --chain!=0 && pi!=ci /*&& c==UZIP.F._hash(data,i-dif)*/) { + if(tl==0 || (data[i+tl]==data[i+tl-dif])) { + var cl = UZIP.F._howLong(data, i, dif); + if(cl>tl) { + tl=cl; td=dif; if(tl>=nice) break; //* + if(dif+2maxd) { maxd=curd; pi = ei; } + } //*/ + } + } + + ci=pi; pi = prev[ci]; + dif += ((ci-pi + (1<<15)) & 0x7fff); + } + return (tl<<16)|td; +} +UZIP.F._howLong = function(data, i, dif) { + if(data[i]!=data[i-dif] || data[i+1]!=data[i+1-dif] || data[i+2]!=data[i+2-dif]) return 0; + var oi=i, l = Math.min(data.length, i+258); i+=3; + //while(i+4>>23), end = off+(qb&((1<<23)-1)); + while(off>16), lgi=(qc>>8)&255, dgi=(qc&255); + pos = UZIP.F._writeLit(257+lgi, ltree, out, pos); + putsE(out, pos, len-U.of0[lgi]); pos+=U.exb[lgi]; + + pos = UZIP.F._writeLit(dgi, dtree, out, pos); + putsF(out, pos, dst-U.df0[dgi]); pos+=U.dxb[dgi]; off+=len; + } + } + pos = UZIP.F._writeLit(256, ltree, out, pos); + } + //console.log(pos-opos, fxdSize, dynSize, cstSize); + return pos; +} +UZIP.F._copyExact = function(data,off,len,out,pos) { + var p8 = (pos>>>3); + out[p8]=(len); out[p8+1]=(len>>>8); out[p8+2]=255-out[p8]; out[p8+3]=255-out[p8+1]; p8+=4; + out.set(new Uint8Array(data.buffer, off, len), p8); + //for(var i=0; i4 && U.itree[(U.ordr[numh-1]<<1)+1]==0) numh--; + return [ML, MD, MH, numl, numd, numh, lset, dset]; +} +UZIP.F.getSecond= function(a) { var b=[]; for(var i=0; i>1)+","; return b; } +UZIP.F.contSize = function(tree, hst) { var s=0; for(var i=0; i15) { UZIP.F._putsE(out, pos, rst, rsl); pos+=rsl; } + } + return pos; +} +UZIP.F._lenCodes = function(tree, set) { + var len=tree.length; while(len!=2 && tree[len-1]==0) len-=2; // when no distances, keep one code with length 0 + for(var i=0; i>>1, 138); + if(zc<11) set.push(17, zc-3); + else set.push(18, zc-11); + i += zc*2-2; + } + else if(l==prv && nxt==l && nnxt==l) { + var lz = i+5; + while(lz+2>>1, 6); + set.push(16, zc-3); + i += zc*2-2; + } + else set.push(l, 0); + } + return len>>>1; +} +UZIP.F._hufTree = function(hst, tree, MAXL) { + var list=[], hl = hst.length, tl=tree.length, i=0; + for(i=0; iMAXL) { UZIP.F.restrictDepth(l2, MAXL, maxl); maxl = MAXL; } + for(i=0; iMD) { var od=dps[i].d; dps[i].d=MD; dbt+=bCost-(1<<(maxl-od)); } else break; + dbt = dbt>>>(maxl-MD); + while(dbt>0) { var od=dps[i].d; if(od=0; i--) if(dps[i].d==MD && dbt<0) { dps[i].d--; dbt++; } if(dbt!=0) console.log("debt left"); +} + +UZIP.F._goodIndex = function(v, arr) { + var i=0; if(arr[i|16]<=v) i|=16; if(arr[i|8]<=v) i|=8; if(arr[i|4]<=v) i|=4; if(arr[i|2]<=v) i|=2; if(arr[i|1]<=v) i|=1; return i; +} +UZIP.F._writeLit = function(ch, ltree, out, pos) { + UZIP.F._putsF(out, pos, ltree[ch<<1]); + return pos+ltree[(ch<<1)+1]; +} + + + + + + + + +UZIP.F.inflate = function(data, buf) { + var u8=Uint8Array; + if(data[0]==3 && data[1]==0) return (buf ? buf : new u8(0)); + var F=UZIP.F, bitsF = F._bitsF, bitsE = F._bitsE, decodeTiny = F._decodeTiny, makeCodes = F.makeCodes, codes2map=F.codes2map, get17 = F._get17; + var U = F.U; + + var noBuf = (buf==null); + if(noBuf) buf = new u8((data.length>>>2)<<3); + + var BFINAL=0, BTYPE=0, HLIT=0, HDIST=0, HCLEN=0, ML=0, MD=0; + var off = 0, pos = 0; + var lmap, dmap; + + while(BFINAL==0) { + BFINAL = bitsF(data, pos , 1); + BTYPE = bitsF(data, pos+1, 2); pos+=3; + //console.log(BFINAL, BTYPE); + + if(BTYPE==0) { + if((pos&7)!=0) pos+=8-(pos&7); + var p8 = (pos>>>3)+4, len = data[p8-4]|(data[p8-3]<<8); //console.log(len);//bitsF(data, pos, 16), + if(noBuf) buf=UZIP.F._check(buf, off+len); + buf.set(new u8(data.buffer, data.byteOffset+p8, len), off); + //for(var i=0; itl)tl=l; } pos+=3*HCLEN; //console.log(itree); + makeCodes(U.itree, tl); + codes2map(U.itree, tl, U.imap); + + lmap = U.lmap; dmap = U.dmap; + + pos = decodeTiny(U.imap, (1<>>24))-1; pos+=(ml&0xffffff); + makeCodes(U.ltree, mx0); + codes2map(U.ltree, mx0, lmap); + + //var md = decodeTiny(U.imap, (1<>>24))-1; pos+=(md&0xffffff); + makeCodes(U.dtree, mx1); + codes2map(U.dtree, mx1, dmap); + } + //var ooff=off, opos=pos; + while(true) { + var code = lmap[get17(data, pos) & ML]; pos += code&15; + var lit = code>>>4; //U.lhst[lit]++; + if((lit>>>8)==0) { buf[off++] = lit; } + else if(lit==256) { break; } + else { + var end = off+lit-254; + if(lit>264) { var ebs = U.ldef[lit-257]; end = off + (ebs>>>3) + bitsE(data, pos, ebs&7); pos += ebs&7; } + //UZIP.F.dst[end-off]++; + + var dcode = dmap[get17(data, pos) & MD]; pos += dcode&15; + var dlit = dcode>>>4; + var dbs = U.ddef[dlit], dst = (dbs>>>4) + bitsF(data, pos, dbs&15); pos += dbs&15; + + //var o0 = off-dst, stp = Math.min(end-off, dst); + //if(stp>20) while(off>>3); + } + //console.log(UZIP.F.dst); + //console.log(tlen, dlen, off-tlen+tcnt); + return buf.length==off ? buf : buf.slice(0,off); +} +UZIP.F._check=function(buf, len) { + var bl=buf.length; if(len<=bl) return buf; + var nbuf = new Uint8Array(Math.max(bl<<1,len)); nbuf.set(buf,0); + //for(var i=0; i>>4; + if(lit<=15) { tree[i]=lit; i++; } + else { + var ll = 0, n = 0; + if(lit==16) { + n = (3 + bitsE(data, pos, 2)); pos += 2; ll = tree[i-1]; + } + else if(lit==17) { + n = (3 + bitsE(data, pos, 3)); pos += 3; + } + else if(lit==18) { + n = (11 + bitsE(data, pos, 7)); pos += 7; + } + var ni = i+n; + while(i>>1; + while(imx)mx=v; i++; } + while(i>1; + var cl = tree[i+1], val = (lit<<4)|cl; // : (0x8000 | (U.of0[lit-257]<<7) | (U.exb[lit-257]<<4) | cl); + var rest = (MAX_BITS-cl), i0 = tree[i]<>>(15-MAX_BITS); + while(i0!=i1) { + var p0 = r15[i0]>>>(15-MAX_BITS); + map[p0]=val; i0++; + } + } +} +UZIP.F.revCodes = function(tree, MAX_BITS) { + var r15 = UZIP.F.U.rev15, imb = 15-MAX_BITS; + for(var i=0; i>>imb; } +} + +// used only in deflate +UZIP.F._putsE= function(dt, pos, val ) { val = val<<(pos&7); var o=(pos>>>3); dt[o]|=val; dt[o+1]|=(val>>>8); } +UZIP.F._putsF= function(dt, pos, val ) { val = val<<(pos&7); var o=(pos>>>3); dt[o]|=val; dt[o+1]|=(val>>>8); dt[o+2]|=(val>>>16); } + +UZIP.F._bitsE= function(dt, pos, length) { return ((dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) )>>>(pos&7))&((1<>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16))>>>(pos&7))&((1<>>3] | (dt[(pos>>>3)+1]<<8))>>>(pos&7))&511; +} */ +UZIP.F._get17= function(dt, pos) { // return at least 17 meaningful bytes + return (dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16) )>>>(pos&7); +} +UZIP.F._get25= function(dt, pos) { // return at least 17 meaningful bytes + return (dt[pos>>>3] | (dt[(pos>>>3)+1]<<8) | (dt[(pos>>>3)+2]<<16) | (dt[(pos>>>3)+3]<<24) )>>>(pos&7); +} +UZIP.F.U = function(){ + var u16=Uint16Array, u32=Uint32Array; + return { + next_code : new u16(16), + bl_count : new u16(16), + ordr : [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ], + of0 : [3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999], + exb : [0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0], + ldef : new u16(32), + df0 : [1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 65535, 65535], + dxb : [0,0,0,0,1,1,2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0], + ddef : new u32(32), + flmap: new u16( 512), fltree: [], + fdmap: new u16( 32), fdtree: [], + lmap : new u16(32768), ltree : [], ttree:[], + dmap : new u16(32768), dtree : [], + imap : new u16( 512), itree : [], + //rev9 : new u16( 512) + rev15: new u16(1<<15), + lhst : new u32(286), dhst : new u32( 30), ihst : new u32(19), + lits : new u32(15000), + strt : new u16(1<<16), + prev : new u16(1<<15) + }; +} (); + +(function(){ + var U = UZIP.F.U; + var len = 1<<15; + for(var i=0; i>> 1) | ((x & 0x55555555) << 1)); + x = (((x & 0xcccccccc) >>> 2) | ((x & 0x33333333) << 2)); + x = (((x & 0xf0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f) << 4)); + x = (((x & 0xff00ff00) >>> 8) | ((x & 0x00ff00ff) << 8)); + U.rev15[i] = (((x >>> 16) | (x << 16)))>>>17; + } + + function pushV(tgt, n, sv) { while(n--!=0) tgt.push(0,sv); } + + for(var i=0; i<32; i++) { U.ldef[i]=(U.of0[i]<<3)|U.exb[i]; U.ddef[i]=(U.df0[i]<<4)|U.dxb[i]; } + + pushV(U.fltree, 144, 8); pushV(U.fltree, 255-143, 9); pushV(U.fltree, 279-255, 7); pushV(U.fltree,287-279,8); + /* + var i = 0; + for(; i<=143; i++) U.fltree.push(0,8); + for(; i<=255; i++) U.fltree.push(0,9); + for(; i<=279; i++) U.fltree.push(0,7); + for(; i<=287; i++) U.fltree.push(0,8); + */ + UZIP.F.makeCodes(U.fltree, 9); + UZIP.F.codes2map(U.fltree, 9, U.flmap); + UZIP.F.revCodes (U.fltree, 9) + + pushV(U.fdtree,32,5); + //for(i=0;i<32; i++) U.fdtree.push(0,5); + UZIP.F.makeCodes(U.fdtree, 5); + UZIP.F.codes2map(U.fdtree, 5, U.fdmap); + UZIP.F.revCodes (U.fdtree, 5) + + pushV(U.itree,19,0); pushV(U.ltree,286,0); pushV(U.dtree,30,0); pushV(U.ttree,320,0); + /* + for(var i=0; i< 19; i++) U.itree.push(0,0); + for(var i=0; i<286; i++) U.ltree.push(0,0); + for(var i=0; i< 30; i++) U.dtree.push(0,0); + for(var i=0; i<320; i++) U.ttree.push(0,0); + */ +})() + + + + + + + + + + + + + + + + diff --git a/js/tegaki.min.js b/js/tegaki.min.js new file mode 100644 index 0000000..0616a9d --- /dev/null +++ b/js/tegaki.min.js @@ -0,0 +1,5 @@ +/*! tegaki.js, MIT License */"use strict";var TegakiStrings={badDimensions:"Invalid dimensions.",promptWidth:"Canvas width in pixels",promptHeight:"Canvas height in pixels",confirmDelLayers:"Delete selected layers?",confirmMergeLayers:"Merge selected layers?",tooManyLayers:"Layer limit reached.",errorLoadImage:"Could not load the image.",noActiveLayer:"No active layer.",hiddenActiveLayer:"The active layer is not visible.",confirmCancel:"Are you sure? Your work will be lost.",confirmChangeCanvas:"Are you sure? Changing the canvas will clear all layers and history and disable replay recording.",color:"Color",size:"Size",alpha:"Opacity",flow:"Flow",zoom:"Zoom",layers:"Layers",switchPalette:"Switch color palette",paletteSlotReplace:"Right click to replace with the current color",layer:"Layer",addLayer:"Add layer",delLayers:"Delete layers",mergeLayers:"Merge layers",moveLayerUp:"Move up",moveLayerDown:"Move down",toggleVisibility:"Toggle visibility",newCanvas:"New",open:"Open",save:"Save",saveAs:"Save As","export":"Export",undo:"Undo",redo:"Redo",close:"Close",finish:"Finish",tip:"Tip",pressure:"Pressure",preserveAlpha:"Preserve Alpha",pen:"Pen",pencil:"Pencil",airbrush:"Airbrush",pipette:"Pipette",blur:"Blur",eraser:"Eraser",bucket:"Bucket",tone:"Tone",gapless:"Gapless",play:"Play",pause:"Pause",rewind:"Rewind",slower:"Slower",faster:"Faster",recordingEnabled:"Recording replay",errorLoadReplay:"Could not load the replay: ",loadingReplay:"Loading replay\u2026"};class TegakiTool{constructor(){this.id=0,this.name=null,this.keybind=null,this.useFlow=!1,this.useSizeDynamics=!1,this.useAlphaDynamics=!1,this.useFlowDynamics=!1,this.usePreserveAlpha=!1,this.step=0,this.size=1,this.alpha=1,this.flow=1,this.useSize=!0,this.useAlpha=!0,this.useFlow=!0,this.noCursor=!1,this.color="#000000",this.rgb=[0,0,0],this.brushSize=0,this.brushAlpha=0,this.brushFlow=0,this.stepSize=0,this.center=0,this.sizeDynamicsEnabled=!1,this.alphaDynamicsEnabled=!1,this.flowDynamicsEnabled=!1,this.preserveAlphaEnabled=!1,this.tip=-1,this.tipList=null,this.stepAcc=0,this.shapeCache=null,this.kernel=null}brushFn(e,t,a,i){}start(e,t){}commit(){}draw(e,t){}usesDynamics(){return this.useSizeDynamics||this.useAlphaDynamics||this.useFlowDynamics}enabledDynamics(){return this.sizeDynamicsEnabled||this.alphaDynamicsEnabled||this.flowDynamicsEnabled}setSize(e){this.size=e}setAlpha(e){this.alpha=e,this.brushAlpha=e}setFlow(e){this.flow=e,this.brushFlow=this.easeFlow(e)}easeFlow(e){return e}setColor(e){this.rgb=$T.hexToRgb(e)}setSizeDynamics(e){this.useSizeDynamics&&(e||this.setSize(this.size),this.sizeDynamicsEnabled=e)}setAlphaDynamics(e){this.useAlphaDynamics&&(e||this.setAlpha(this.alpha),this.alphaDynamicsEnabled=e)}setFlowDynamics(e){this.useFlowDynamics&&(e||this.setFlow(this.flow),this.flowDynamicsEnabled=e)}setPreserveAlpha(e){this.preserveAlphaEnabled=e}set(){this.setAlpha(this.alpha),this.setFlow(this.flow),this.setSize(this.size),this.setColor(Tegaki.toolColor),Tegaki.onToolChanged(this)}}class TegakiBrush extends TegakiTool{constructor(){super()}generateShape(e){}brushFn(e,t,a,i){var s,r,n,o,l,g,d,c,h,p,k,T,u,y,f,v,m,b,L,C,w,S,I,U,P,A,D,E,$;for($=this.preserveAlphaEnabled,g=this.kernel,D=this.brushAlpha,E=this.brushFlow,A=this.brushSize,s=Tegaki.activeLayer.imageData.data,r=Tegaki.ghostBuffer.data,n=Tegaki.blendBuffer.data,o=Tegaki.baseWidth,l=Tegaki.baseHeight,o,f=this.rgb[0],v=this.rgb[1],m=this.rgb[2],c=0;c=l))for(d=0;d=o||(T=g[4*(c*A+d)+3]/255)<=0||(y=n[(U=4*(p*o+h))+3]/255,y+=T*E*(D-y),(P=Math.ceil(255*y))>n[U+3]&&(0===n[U]&&(r[U]=s[U],r[U+1]=s[U+1],r[U+2]=s[U+2],r[U+3]=s[U+3]),n[U]=1,n[U+3]=P,w=r[U],S=r[U+1],I=r[U+2],b=(f*y+w*(k=r[U+3]/255)*(1-y))/(u=k+y-k*y),L=(v*y+S*k*(1-y))/u,C=(m*y+I*k*(1-y))/u,s[U]=f>w?Math.ceil(b):Math.floor(b),s[U+1]=v>S?Math.ceil(L):Math.floor(L),s[U+2]=m>I?Math.ceil(C):Math.floor(C),$||(s[U+3]=Math.ceil(255*u))))}generateShapeCache(e){var t,a;if(this.shapeCache||(this.shapeCache=new Array(Tegaki.maxSize)),!this.shapeCache[0]||e)for(t=0;tg?l:0!==g?-g:0)/2,0!==l&&(l=-l),m=!1,p=s,k=r;h+=Math.max(Math.abs(p-s),Math.abs(k-r)),p=s,k=r,h>=this.stepSize&&(this.enabledDynamics()?(v=T>0?1-Math.sqrt((e-s)*(e-s)+(t-r)*(t-r))/T:0,this.updateDynamics(v)&&(this.brushFn(s-this.center-n,r-this.center-o,n,o),m=!0)):(this.brushFn(s-this.center-n,r-this.center-o,n,o),m=!0),h=0),s!==e||r!==t;)(c=d)>l&&(d-=g,s+=a),c=i;)r[n+a-g+(n+i-g)*e]=o,r[n+i-g+(n+a-g)*e]=o,r[n-i+(n+a-g)*e]=o,r[n-a+(n+i-g)*e]=o,r[n-i+(n-a)*e]=o,r[n-a+(n-i)*e]=o,r[n+i-g+(n-a)*e]=o,r[n+a-g+(n-i)*e]=o,++i,t+=t<=0?2*i+1:2*(i- --a)+1;return l>0&&Tegaki.tools.bucket.fill(s,l,l,this.rgb,1),{center:l,stepSize:Math.ceil(e*this.step),brushSize:e,kernel:s.data}}}class TegakiAirbrush extends TegakiBrush{constructor(){super(),this.id=3,this.name="airbrush",this.keybind="a",this.step=.1,this.size=32,this.alpha=1,this.useSizeDynamics=!0,this.useAlphaDynamics=!0,this.useFlowDynamics=!0,this.usePreserveAlpha=!0}easeFlow(e){return 1-Math.sqrt(1-e)}generateShape(e){var t,a,i,s,r,n,o,l,g,d,c,h;for(a=e,e*=2,i=new ImageData(e,e).data,s=e*e*4,n=Math.sqrt(a*a),l=g=-(o=Math.round(a)),t=0;t=o?(l=-o,++g):(c=g,(d=l)<0&&(d=-d),c<0&&(c=-c),(r=Math.sqrt(d*d+c*c))>=n?h=0:0===r?h=255:((h=r/n+.1)>1&&(h=1),h=255*(1-Math.exp(1-1/h)/h)),i[t+3]=h,t+=4,++l);return{center:a,stepSize:Math.ceil(e*this.step),brushSize:e,kernel:i}}}class TegakiPen extends TegakiBrush{constructor(){super(),this.id=2,this.name="pen",this.keybind="p",this.step=.05,this.size=8,this.alpha=1,this.flow=1,this.useSizeDynamics=!0,this.useAlphaDynamics=!0,this.useFlowDynamics=!0,this.usePreserveAlpha=!0}easeFlow(e){return 1-Math.sqrt(1-Math.pow(e,3))}generateShape(e){var t,a,i,s,r,n,o,l,g,d,c,h,p,k,T,u,y,f;for(y=Math.floor(e/2)+1,c=(d=4)*d,h=(f=e+2)*d,l=Math.floor(h/2),g=Math.floor((h+1)%2),s=new ImageData(h,h),p=new Uint32Array(s.data.buffer),o=1426063360,a=l,i=0,t=1-l,n=l;a>=i;)p[n+a-g+(n+i-g)*h]=o,p[n+i-g+(n+a-g)*h]=o,p[n-i+(n+a-g)*h]=o,p[n-a+(n+i-g)*h]=o,p[n-i+(n-a)*h]=o,p[n-a+(n-i)*h]=o,p[n+i-g+(n-a)*h]=o,p[n+a-g+(n-i)*h]=o,++i,t+=t<=0?2*i+1:2*(i- --a)+1;for(o=4278190080,a=l-3,i=0,t=1-l,n=l;a>=i;)p[n+a-g+(n+i-g)*h]=o,p[n+i-g+(n+a-g)*h]=o,p[n-i+(n+a-g)*h]=o,p[n-a+(n+i-g)*h]=o,p[n-i+(n-a)*h]=o,p[n-a+(n-i)*h]=o,p[n+i-g+(n-a)*h]=o,p[n+a-g+(n-i)*h]=o,++i,t+=t<=0?2*i+1:2*(i- --a)+1;for(l>0&&Tegaki.tools.bucket.fill(s,l,l,this.rgb,1),p=s.data,r=new ImageData(f,f).data,a=0;a=0&&(l=4*(v+L),this.testPixel(C,l,k,g,d,c,h));)this.blendPixel(C,l,r,n,o,s),k[l]=1,y>=0&&(l=4*(m+L),this.testPixel(C,l,k,g,d,c,h)&&(p.push(L),p.push(y))),f=0&&(l=4*(m+L),this.testPixel(C,l,k,g,d,c,h)&&(p.push(L),p.push(y))),f=Tegaki.baseWidth||t>=Tegaki.baseHeight||(this.fill(Tegaki.activeLayer.imageData,e,t,this.rgb,this.alpha),Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData,0,0))}blendPixel(e,t,a,i,s,r){var n,o,l,g,d,c,h,p;n=e[t],o=e[t+1],l=e[t+2],d=(a*r+n*(g=e[t+3]/255)*(1-r))/(p=g+r-g*r),c=(i*r+o*g*(1-r))/p,h=(s*r+l*g*(1-r))/p,e[t]=a>n?Math.ceil(d):Math.floor(d),e[t+1]=i>o?Math.ceil(c):Math.floor(c),e[t+2]=s>l?Math.ceil(h):Math.floor(h),e[t+3]=Math.ceil(255*p)}testPixel(e,t,a,i,s,r,n){return!a[t]&&e[t]==i&&e[++t]==s&&e[++t]==r&&e[++t]==n}start(e,t){this.brushFn(e,t)}draw(e,t){this.brushFn(e,t)}setSize(e){}}class TegakiTone extends TegakiPencil{constructor(){super(),this.id=5,this.name="tone",this.keybind="t",this.step=.01,this.useFlow=!1,this.size=8,this.alpha=.5,this.useSizeDynamics=!0,this.useAlphaDynamics=!0,this.usePreserveAlpha=!0,this.matrix=[[0,8,2,10],[12,4,14,6],[3,11,1,9],[15,7,13,5]],this.mapCache=null,this.mapWidth=0,this.mapHeight=0}start(e,t){this.mapWidth===Tegaki.baseWidth&&this.mapHeight===Tegaki.baseHeight||this.generateMapCache(!0),super.start(e,t)}brushFn(e,t,a,i){var s,r,n,o,l,g,d,c,h,p,k,T,u,y;if(s=Tegaki.activeLayer.imageData.data,u=Tegaki.baseWidth,y=Tegaki.baseHeight,r=this.kernel,n=this.brushSize,this.mapWidth,g=this.preserveAlphaEnabled,!((l=Math.round(16*this.brushAlpha)-1)<0))for(o=this.mapCache[l],p=0;p=y))for(h=0;h=u||0!==r[4*(p*n+h)+3]&&(d=4*(c=T*u+k),0===o[c]&&(s[d]=this.rgb[0],s[d+1]=this.rgb[1],s[d+2]=this.rgb[2],g||(s[d+3]=255)))}generateMap(e,t,a){var i,s,r;for(i=new Uint8Array(e*t),r=0;r=T))for(f=0;f=u||(S=4*(k*T+p),0===d[4*(f*r+y)+3]||p<=0||k<=0||p>=l||k>=g))){for(v=m=b=L=U=0,C=-1;C<2;++C)for(w=-1;w<2;++w)I=n[(s=4*((k-w)*T+(p-C)))+3],0===C&&0===w?(P=I*h,U+=h):(P=I*c,U+=c),v+=n[s]*P,m+=n[++s]*P,b+=n[++s]*P,L+=P;(L/=U)<=0||(o[S]=Math.round(v/U/L),o[S+1]=Math.round(m/U/L),o[S+2]=Math.round(b/U/L),o[S+3]=Math.round(L))}}}TegakiBlur.prototype.generateShape=TegakiPencil.prototype.generateShape;class TegakiEraser extends TegakiBrush{constructor(){super(),this.id=8,this.name="eraser",this.keybind="e",this.step=.1,this.size=8,this.alpha=1,this.useFlow=!1,this.useSizeDynamics=!0,this.useAlphaDynamics=!0,this.usePreserveAlpha=!1,this.tipId=0,this.tipList=["pencil","pen","airbrush"]}brushFn(e,t,a,i){var s,r,n,o,l,g,d,c,h,p,k,T,u,y,f;for(f=this.brushAlpha,y=this.brushSize,o=this.kernel,s=Tegaki.activeLayer.imageData.data,n=Tegaki.ghostBuffer.data,r=Tegaki.blendBuffer.data,l=Tegaki.baseWidth,g=Tegaki.baseHeight,k=0;k=g))for(p=0;p=l||(d=o[4*(k*y+p)+3]/255,0===n[h=4*(u*l+T)+3]&&(n[h]=s[h]),c=r[h]/255,c+=d*(f-c),r[h]=Math.floor(255*c),s[h]=Math.floor(n[h]*(1-c)))}generateShape(e){return 0===this.tipId?this.generateShapePencil(e):1===this.tipId?this.generateShapePen(e):this.generateShapeAirbrush(e)}}TegakiEraser.prototype.generateShapePencil=TegakiPencil.prototype.generateShape,TegakiEraser.prototype.generateShapePen=TegakiPen.prototype.generateShape,TegakiEraser.prototype.generateShapeAirbrush=TegakiAirbrush.prototype.generateShape;var $T={docEl:document.documentElement,id:function(e){return document.getElementById(e)},cls:function(e,t){return(t||document).getElementsByClassName(e)},on:function(e,t,a){e.addEventListener(t,a,!1)},off:function(e,t,a){e.removeEventListener(t,a,!1)},el:function(e){return document.createElement(e)},frag:function(){return document.createDocumentFragment()},copyImageData:e=>new ImageData(new Uint8ClampedArray(e.data),e.width),copyCanvas:function(e,t){var a;return t?a=e.cloneNode(!1):((a=$T.el("canvas")).width=e.width,a.height=e.height),a.getContext("2d").drawImage(e,0,0),a},clearCtx:function(e){e.clearRect(0,0,e.canvas.width,e.canvas.height)},hexToRgb:function(e){var t=e.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i);return t?[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]:null},RgbToHex:function(e,t,a){return"#"+((1<<24)+(e<<16)+(t<<8)+a).toString(16).slice(1)},getColorAt:function(e,t,a){var i=e.getImageData(t,a,1,1).data;return"#"+("0"+i[0].toString(16)).slice(-2)+("0"+i[1].toString(16)).slice(-2)+("0"+i[2].toString(16)).slice(-2)},generateFilename:function(){return"tegaki_"+(new Date).toISOString().split(".")[0].replace(/[^0-9]/g,"_")},sortAscCb:function(e,t){return e>t?1:et?-1:ea&&(i=a/e,e=a,t*=i),t>a&&(i=a/t,t=a,e*=i),[Math.ceil(e),Math.ceil(t)]}};class TegakiBinReader{constructor(e){this.pos=0,this.view=new DataView(e),this.buf=e}readInt8(){var e=this.view.getInt8(this.pos);return this.pos+=1,e}readUint8(){var e=this.view.getUint8(this.pos);return this.pos+=1,e}readInt16(){var e=this.view.getInt16(this.pos);return this.pos+=2,e}readUint16(){var e=this.view.getUint16(this.pos);return this.pos+=2,e}readUint32(){var e=this.view.getUint32(this.pos);return this.pos+=4,e}readFloat32(){var e=this.view.getFloat32(this.pos);return this.pos+=4,e}}class TegakiBinWriter{constructor(e){this.pos=0,this.view=new DataView(e),this.buf=e}writeInt8(e){this.view.setInt8(this.pos,e),this.pos+=1}writeUint8(e){this.view.setUint8(this.pos,e),this.pos+=1}writeInt16(e){this.view.setInt16(this.pos,e),this.pos+=2}writeUint16(e){this.view.setUint16(this.pos,e),this.pos+=2}writeUint32(e){this.view.setUint32(this.pos,e),this.pos+=4}writeFloat32(e){this.view.setFloat32(this.pos,e),this.pos+=4}}var TegakiCursor={size:0,radius:0,points:null,cursorCtx:null,offsetX:0,offsetY:0,lastX:0,lastY:0,lastSize:0,init:function(){var e;(e=$T.el("canvas")).id="tegaki-cursor-layer",[e.width,e.height]=TegakiCursor.getMaxCanvasSize(),Tegaki.canvasCnt.appendChild(e),this.offsetX=e.offsetLeft,this.offsetY=e.offsetTop,this.cursorCtx=e.getContext("2d")},getCanvas:function(){return this.cursorCtx?this.cursorCtx.canvas:null},getMaxCanvasSize:function(){let e=Tegaki.canvasCnt.offsetWidth,t=Tegaki.canvasCnt.offsetHeight,[a,i]=Tegaki.getScrollbarSizes();return[e-a,t-i]},updateCanvasSize:function(){let e=this.cursorCtx.canvas,[t,a]=this.getMaxCanvasSize();t===e.width&&a===e.height||(e.width=t,e.height=a,this.offsetX=e.offsetLeft,this.offsetY=e.offsetTop)},render:function(e,t){var a,i,s,r,n;for(a=0|e-this.offsetX-this.radius,i=0|t-this.offsetY-this.radius,this.clear(),this.lastX=a,this.lastY=i,this.lastSize=this.size,r=this.cursorCtx.createImageData(this.size,this.size),n=new Uint32Array(r.data.buffer),s=0;s=i;)o.push(s+a-n+(s+i-n)*e),o.push(s+i-n+(s+a-n)*e),o.push(s-i+(s+a-n)*e),o.push(s-a+(s+i-n)*e),o.push(s-i+(s-a)*e),o.push(s-a+(s-i)*e),o.push(s+i-n+(s-a)*e),o.push(s+a-n+(s-i)*e),++i,t+=t<=0?2*i+1:2*(i- --a)+1;return this.size=e,this.radius=r,this.points=o,!0}},TegakiHistory={maxSize:50,undoStack:[],redoStack:[],pendingAction:null,clear:function(){this.undoStack=[],this.redoStack=[],this.pendingAction=null,this.onChange(0)},push:function(e){e&&(e.coalesce&&this.undoStack[this.undoStack.length-1]instanceof e.constructor&&this.undoStack[this.undoStack.length-1].coalesce(e)||(this.undoStack.push(e),this.undoStack.length>this.maxSize&&this.undoStack.shift(),this.redoStack.length>0&&(this.redoStack=[]),this.onChange(0)))},undo:function(){var e;this.undoStack.length&&((e=this.undoStack.pop()).undo(),this.redoStack.push(e),this.onChange(-1))},redo:function(){var e;this.redoStack.length&&((e=this.redoStack.pop()).redo(),this.undoStack.push(e),this.onChange(1))},onChange:function(e){Tegaki.onHistoryChange(this.undoStack.length,this.redoStack.length,e)},sortPosLayerCallback:function(e,t){return e[0]>t[0]?1:e[0]=0;e--)t=Tegaki.layers[e],Tegaki.layersCnt.insertBefore(t.canvas,a.nextElementSibling);TegakiLayers.setActiveLayer(this.aLayerId)},TegakiHistoryActions.MoveLayers.prototype.redo=function(){var e,t=new Set;for(e of this.layers.slice().reverse())t.add(e[1].id);TegakiLayers.setActiveLayer(this.aLayerId),TegakiLayers.moveLayers(t,this.belowPos)},TegakiHistoryActions.AddLayer.prototype.undo=function(){TegakiLayers.deleteLayers([this.layer.id]),TegakiLayers.setActiveLayer(this.aLayerIdBefore),Tegaki.layerCounter--},TegakiHistoryActions.AddLayer.prototype.redo=function(){TegakiLayers.setActiveLayer(this.aLayerIdBefore),TegakiLayers.addLayer(this.layer),TegakiLayers.setActiveLayer(this.aLayerIdAfter)},TegakiHistoryActions.SetLayersAlpha.prototype.undo=function(){var e,t,a;for(t of this.layerAlphas)[e,t]=t,(a=TegakiLayers.getLayerById(e))&&TegakiLayers.setLayerAlpha(a,t);TegakiUI.updateLayerAlphaOpt()},TegakiHistoryActions.SetLayersAlpha.prototype.redo=function(){var e,t,a;for(t of this.layerAlphas)[e,t]=t,(a=TegakiLayers.getLayerById(e))&&TegakiLayers.setLayerAlpha(a,this.newAlpha);TegakiUI.updateLayerAlphaOpt()},TegakiHistoryActions.SetLayersAlpha.prototype.coalesce=function(e){var t;if(this.layerAlphas.length!==e.layerAlphas.length)return!1;for(t=0;t=0&&!TegakiLayers.selectedLayersHas(a[e].id);e--);for(e-=1;e>=0&&TegakiLayers.selectedLayersHas(a[e].id);e--);return(t=a[e])?t.id:0},getSelectedEdgeLayerPos:function(e){var t,a=Tegaki.layers;if(e)for(t=Tegaki.layers.length-1;t>=0&&!TegakiLayers.selectedLayersHas(a[t].id);t--);else{for(t=0;t=a.length&&(t=-1)}return t},getTopLayer:function(){return Tegaki.layers[Tegaki.layers.length-1]},getTopLayerId:function(){var e=TegakiLayers.getTopLayer();return e?e.id:0},getLayerBelowId:function(e){var t;return(t=TegakiLayers.getLayerPosById(e))<1?null:Tegaki.layers[t-1]},getLayerById:function(e){return Tegaki.layers[TegakiLayers.getLayerPosById(e)]},isSameLayerOrder:function(e,t){var a,i;if(e.length!==t.length)return!1;for(a=0;i=e[a];++a)if(i.id!==t[a].id)return!1;return!0},addLayer:function(e={}){var t,a,i,s,r,n,o,l,g;Tegaki.activeLayer?(l=Tegaki.activeLayer.id,o=TegakiLayers.getLayerPosById(Tegaki.activeLayer.id),n=$T.cls("tegaki-layer",Tegaki.layersCnt)[o]):(o=-1,n=null),n||(n=Tegaki.layersCnt.firstElementChild),(a=$T.el("canvas")).className="tegaki-layer",a.width=Tegaki.baseWidth,a.height=Tegaki.baseHeight,t=++Tegaki.layerCounter,a.id="tegaki-canvas-"+t,a.setAttribute("data-id",t),s={name:TegakiStrings.layer+" "+t,visible:!0,alpha:1},r={id:t,canvas:a,ctx:g=a.getContext("2d"),imageData:g.getImageData(0,0,a.width,a.height)};for(i in s)e[i]!==undefined&&(s[i]=e[i]),r[i]=s[i];return Tegaki.layers.splice(o+1,0,r),TegakiUI.updateLayersGridAdd(r,l),Tegaki.layersCnt.insertBefore(a,n.nextElementSibling),Tegaki.onLayerStackChanged(),new TegakiHistoryActions.AddLayer(r,l,t)},deleteLayers:function(e,t){var a,i,s,r,n,o;o={aLayerIdBefore:Tegaki.activeLayer?Tegaki.activeLayer.id:-1,aLayerIdAfter:TegakiLayers.getTopFencedLayerId()},r=[],n=[];for(a of e)i=TegakiLayers.getLayerPosById(a),s=Tegaki.layers[i],r.push([i,s]),Tegaki.layersCnt.removeChild(s.canvas),n.push(i),TegakiUI.updateLayersGridRemove(a),TegakiUI.deleteLayerPreviewCtx(s);n=n.sort($T.sortDescCb);for(i of n)Tegaki.layers.splice(i,1);return t&&Object.assign(o,t),Tegaki.onLayerStackChanged(),new TegakiHistoryActions.DeleteLayers(r,o)},mergeLayers:function(e){var t,a,i,s,r,n,o,l,g,d;l=[];for(o of Tegaki.layers)e.has(o.id)&&l.push(o);if(!(l.length<1)){if(1===l.length){if(!(r=TegakiLayers.getLayerBelowId(l[0].id)))return;l.unshift(r),d=!0}else r=l[l.length-1],d=!1;(t=$T.el("canvas")).width=Tegaki.baseWidth,t.height=Tegaki.baseHeight,a=t.getContext("2d"),s=$T.copyImageData(r.imageData),g=[];for(o of l)o.id!==r.id&&g.push(o.id),a.globalAlpha=o.alpha,a.drawImage(o.canvas,0,0);return $T.clearCtx(r.ctx),r.ctx.drawImage(t,0,0),TegakiLayers.syncLayerImageData(r),i=$T.copyImageData(r.imageData),n=TegakiLayers.deleteLayers(g,{tgtLayerId:r.id,tgtLayerAlpha:r.alpha,aLayerIdAfter:r.id,imageDataBefore:s,imageDataAfter:i,mergeDown:d}),TegakiLayers.setLayerAlpha(r,1),TegakiUI.updateLayerAlphaOpt(),TegakiUI.updateLayerPreview(r),Tegaki.onLayerStackChanged(),n}},moveLayers:function(e,t){var a,i,s,r,n,o,l;if(e.size&&Tegaki.layers.length){o=t>=Tegaki.layers.length?TegakiLayers.getTopLayer().canvas.nextElementSibling:(i=Tegaki.layers[t]).canvas,s=[],r=[],n=[],l=t,a=0;for(i of Tegaki.layers)e.has(i.id)?(t>0&&a<=t&&l--,s.push([a,i]),n.push(i)):r.push(i),++a;if(r.splice(l,0,...n),!TegakiLayers.isSameLayerOrder(r,Tegaki.layers)){Tegaki.layers=r;for(i of s)Tegaki.layersCnt.insertBefore(i[1].canvas,o);return TegakiUI.updayeLayersGridOrder(),Tegaki.onLayerStackChanged(),new TegakiHistoryActions.MoveLayers(s,t,Tegaki.activeLayer?Tegaki.activeLayer.id:-1)}}},setLayerVisibility:function(e,t){e.visible=t,t?e.canvas.classList.remove("tegaki-hidden"):e.canvas.classList.add("tegaki-hidden"),Tegaki.onLayerStackChanged(),TegakiUI.updateLayersGridVisibility(e.id,t)},setLayerAlpha:function(e,t){e.alpha=t,e.canvas.style.opacity=t},setActiveLayer:function(e){var t,a;e||(e=TegakiLayers.getTopLayerId())?(t=TegakiLayers.getLayerPosById(e))<0||(a=Tegaki.layers[t],Tegaki.activeLayer&&Tegaki.copyContextState(Tegaki.activeLayer.ctx,a.ctx),Tegaki.activeLayer=a,TegakiLayers.selectedLayersClear(),TegakiLayers.selectedLayersAdd(e),TegakiUI.updateLayersGridActive(e),TegakiUI.updateLayerAlphaOpt(),Tegaki.onLayerStackChanged()):Tegaki.activeLayer=null},syncLayerImageData(e,t=null){e.imageData=t?$T.copyImageData(t):e.ctx.getImageData(0,0,Tegaki.baseWidth,Tegaki.baseHeight)},selectedLayersHas:function(e){return Tegaki.selectedLayers.has(+e)},selectedLayersClear:function(){Tegaki.selectedLayers.clear(),TegakiUI.updateLayerAlphaOpt(),TegakiUI.updateLayersGridSelectedClear()},selectedLayersAdd:function(e){Tegaki.selectedLayers.add(+e),TegakiUI.updateLayerAlphaOpt(),TegakiUI.updateLayersGridSelectedSet(e,!0)},selectedLayersRemove:function(e){Tegaki.selectedLayers["delete"](+e),TegakiUI.updateLayerAlphaOpt(),TegakiUI.updateLayersGridSelectedSet(e,!1)},selectedLayersToggle:function(e){TegakiLayers.selectedLayersHas(e)?TegakiLayers.selectedLayersRemove(e):TegakiLayers.selectedLayersAdd(e)}},Tegaki={VERSION:"0.9.4",startTimeStamp:0,bg:null,canvas:null,ctx:null,layers:[],layersCnt:null,canvasCnt:null,ghostBuffer:null,blendBuffer:null,ghostBuffer32:null,blendBuffer32:null,activeLayer:null,layerCounter:0,selectedLayers:new Set,activePointerId:0,activePointerIsPen:!1,isPainting:!1,offsetX:0,offsetY:0,zoomLevel:0,zoomFactor:1,zoomFactorList:[.5,1,2,4,8,16],zoomBaseLevel:1,hasCustomCanvas:!1,TWOPI:2*Math.PI,toolList:[TegakiPencil,TegakiPen,TegakiAirbrush,TegakiBucket,TegakiTone,TegakiPipette,TegakiBlur,TegakiEraser],tools:{},tool:null,colorPaletteId:0,toolColor:"#000000",defaultTool:"pencil",bgColor:"#ffffff",maxSize:64,maxLayers:25,baseWidth:0,baseHeight:0,replayRecorder:null,replayViewer:null,onDoneCb:null,onCancelCb:null,replayMode:!1,saveReplay:!1,open:function(e={}){var t=Tegaki;if(t.bg){if(t.replayMode===!!e.replayMode)return void t.resume();t.destroy()}t.startTimeStamp=Date.now(),e.bgColor&&(t.bgColor=e.bgColor),t.hasCustomCanvas=!1,t.saveReplay=!!e.saveReplay,t.replayMode=!!e.replayMode,t.onDoneCb=e.onDone,t.onCancelCb=e.onCancel,t.baseWidth=e.width||0,t.baseHeight=e.height||0,t.createTools(),t.initKeybinds(),[t.bg,t.canvasCnt,t.layersCnt]=TegakiUI.buildUI(),document.body.appendChild(t.bg),document.body.classList.add("tegaki-backdrop"),t.replayMode?(TegakiUI.setReplayMode(!0),t.replayViewer=new TegakiReplayViewer,e.replayURL&&t.loadReplayFromURL(e.replayURL)):(t.init(),t.setTool(t.defaultTool),t.saveReplay&&(t.replayRecorder=new TegakiReplayRecorder,t.replayRecorder.start()))},init:function(){var e=Tegaki;e.createCanvas(),e.updateLayersCntSize(),e.createBuffers(),e.updatePosOffset(),e.resetLayers(),TegakiCursor.init(e.baseWidth,e.baseHeight),e.bindGlobalEvents(),TegakiUI.updateUndoRedo(0,0),TegakiUI.updateZoomLevel()},initFromReplay:function(){var e,t;t=(e=Tegaki).replayViewer,e.initToolsFromReplay(),e.baseWidth=t.canvasWidth,e.baseHeight=t.canvasHeight, +e.bgColor=$T.RgbToHex(...t.bgColor),e.toolColor=$T.RgbToHex(...t.toolColor)},initToolsFromReplay:function(){var e,t,a,i,s,r,n;t=(e=Tegaki).replayViewer;for(a in e.tools){(i=e.tools[a]).id===t.toolId&&(e.defaultTool=a),s=t.toolMap[i.id],n=["step","size","alpha","flow","tipId"];for(r of n)s[r]!==undefined&&(i[r]=s[r]);n=["sizeDynamicsEnabled","alphaDynamicsEnabled","flowDynamicsEnabled","usePreserveAlpha"];for(r of n)s[r]!==undefined&&(i[r]=!!s[r])}},resetLayers:function(){var e,t;if(Tegaki.layers.length){for(e=0,t=Tegaki.layers.length;e=TegakiColorPalettes.length||(Tegaki.colorPaletteId=e,TegakiUI.updateColorPalette())},setToolSizeUp:function(){Tegaki.setToolSize(Tegaki.tool.size+1)},setToolSizeDown:function(){Tegaki.setToolSize(Tegaki.tool.size-1)},setToolSize:function(e){e>0&&e<=Tegaki.maxSize&&(Tegaki.tool.setSize(e),Tegaki.updateCursorStatus(),Tegaki.recordEvent(TegakiEventSetToolSize,performance.now(),e),TegakiUI.updateToolSize())},setToolAlpha:function(e){(e=Math.fround(e))>=0&&e<=1&&(Tegaki.tool.setAlpha(e),Tegaki.recordEvent(TegakiEventSetToolAlpha,performance.now(),e),TegakiUI.updateToolAlpha())},setToolFlow:function(e){(e=Math.fround(e))>=0&&e<=1&&(Tegaki.tool.setFlow(e),Tegaki.recordEvent(TegakiEventSetToolFlow,performance.now(),e),TegakiUI.updateToolFlow())},setToolColor:function(e){Tegaki.toolColor=e,$T.id("tegaki-color").style.backgroundColor=e,$T.id("tegaki-colorpicker").value=e,Tegaki.tool.setColor(e),Tegaki.recordEvent(TegakiEventSetColor,performance.now(),Tegaki.tool.rgb)},setToolColorRGB:function(e,t,a){Tegaki.setToolColor($T.RgbToHex(e,t,a))},setTool:function(e){Tegaki.tools[e].set()},setToolById:function(e){var t;for(t in Tegaki.tools)if(Tegaki.tools[t].id===e)return void Tegaki.setTool(t)},setZoom:function(e){var t;(t=e+Tegaki.zoomBaseLevel)>=Tegaki.zoomFactorList.length||t<0||!Tegaki.canvas||(Tegaki.zoomLevel=e,Tegaki.zoomFactor=Tegaki.zoomFactorList[t],TegakiUI.updateZoomLevel(),Tegaki.layersCnt.style.width=Math.ceil(Tegaki.baseWidth*Tegaki.zoomFactor)+"px",Tegaki.layersCnt.style.height=Math.ceil(Tegaki.baseHeight*Tegaki.zoomFactor)+"px",Tegaki.updateCursorStatus(),Tegaki.updatePosOffset())},onZoomChange:function(){this.hasAttribute("data-in")?Tegaki.setZoom(Tegaki.zoomLevel+1):Tegaki.setZoom(Tegaki.zoomLevel-1),TegakiCursor.updateCanvasSize()},onNewClick:function(){var e,t,a,i=Tegaki;(e=prompt(TegakiStrings.promptWidth,i.canvas.width))&&(t=prompt(TegakiStrings.promptHeight,i.canvas.height))&&(t=+t,(e=+e)<1||t<1?TegakiUI.printMsg(TegakiStrings.badDimensions):(a={},i.copyContextState(i.activeLayer.ctx,a),i.resizeCanvas(e,t),i.copyContextState(a,i.activeLayer.ctx),i.setZoom(0),TegakiHistory.clear(),TegakiUI.updateLayerPreviewSize(),i.startTimeStamp=Date.now(),i.saveReplay&&(i.createTools(),i.setTool(i.defaultTool),i.replayRecorder=new TegakiReplayRecorder,i.replayRecorder.start())))},onOpenClick:function(){(!(TegakiHistory.undoStack[0]||TegakiHistory.redoStack[0])&&!Tegaki.saveReplay||confirm(TegakiStrings.confirmChangeCanvas))&&$T.id("tegaki-filepicker").click()},loadReplayFromFile:function(){Tegaki.replayViewer.debugLoadLocal()},loadReplayFromURL:function(e){TegakiUI.printMsg(TegakiStrings.loadingReplay,0),Tegaki.replayViewer.loadFromURL(e)},onExportClick:function(){Tegaki.flatten().toBlob(function(e){var t=$T.el("a");t.className="tegaki-hidden",t.download=$T.generateFilename()+".png",t.href=URL.createObjectURL(e),Tegaki.bg.appendChild(t),t.click(),Tegaki.bg.removeChild(t)},"image/png")},onUndoClick:function(){TegakiHistory.undo()},onRedoClick:function(){TegakiHistory.redo()},onHistoryChange:function(e,t,a=0){TegakiUI.updateUndoRedo(e,t),-1===a?Tegaki.recordEvent(TegakiEventUndo,performance.now()):1===a&&Tegaki.recordEvent(TegakiEventRedo,performance.now())},onDoneClick:function(){Tegaki.hide(),Tegaki.onDoneCb()},onCancelClick:function(){confirm(TegakiStrings.confirmCancel)&&(Tegaki.destroy(),Tegaki.onCancelCb())},onCloseViewerClick:function(){Tegaki.replayViewer.destroy(),Tegaki.destroy()},onToolSizeChange:function(){var e=+this.value;e<1?e=1:e>Tegaki.maxSize&&(e=Tegaki.maxSize),Tegaki.setToolSize(e)},onToolAlphaChange:function(){var e=+this.value;(e/=100)<0?e=0:e>1&&(e=1),Tegaki.setToolAlpha(e)},onToolFlowChange:function(){var e=+this.value;(e/=100)<0?e=0:e>1&&(e=1),Tegaki.setToolFlow(e)},onToolPressureSizeClick:function(){Tegaki.tool.useSizeDynamics&&Tegaki.setToolSizeDynamics(!Tegaki.tool.sizeDynamicsEnabled)},setToolSizeDynamics:function(e){Tegaki.tool.setSizeDynamics(e),TegakiUI.updateToolDynamics(),Tegaki.recordEvent(TegakiEventSetToolSizeDynamics,performance.now(),+e)},onToolPressureAlphaClick:function(){Tegaki.tool.useAlphaDynamics&&Tegaki.setToolAlphaDynamics(!Tegaki.tool.alphaDynamicsEnabled)},setToolAlphaDynamics:function(e){Tegaki.tool.setAlphaDynamics(e),TegakiUI.updateToolDynamics(),Tegaki.recordEvent(TegakiEventSetToolAlphaDynamics,performance.now(),+e)},onToolPressureFlowClick:function(){Tegaki.tool.useFlowDynamics&&Tegaki.setToolFlowDynamics(!Tegaki.tool.flowDynamicsEnabled)},setToolFlowDynamics:function(e){Tegaki.tool.setFlowDynamics(e),TegakiUI.updateToolDynamics(),Tegaki.recordEvent(TegakiEventSetToolFlowDynamics,performance.now(),+e)},onToolPreserveAlphaClick:function(){Tegaki.tool.usePreserveAlpha&&Tegaki.setToolPreserveAlpha(!Tegaki.tool.preserveAlphaEnabled)},setToolPreserveAlpha:function(e){Tegaki.tool.setPreserveAlpha(e),TegakiUI.updateToolPreserveAlpha(),Tegaki.recordEvent(TegakiEventPreserveAlpha,performance.now(),+e)},onToolTipClick:function(e){var t=+e.target.getAttribute("data-id");t!==Tegaki.tool.tipId&&Tegaki.setToolTip(t)},setToolTip:function(e){Tegaki.tool.setTip(e),TegakiUI.updateToolShape(),Tegaki.recordEvent(TegakiEventSetToolTip,performance.now(),e)},onLayerSelectorClick:function(e){var t=+this.getAttribute("data-id");t&&!e.target.classList.contains("tegaki-ui-cb")&&(e.ctrlKey?Tegaki.toggleSelectedLayer(t):Tegaki.setActiveLayer(t))},toggleSelectedLayer:function(e){TegakiLayers.selectedLayersToggle(e),Tegaki.recordEvent(TegakiEventToggleLayerSelection,performance.now(),e)},setActiveLayer:function(e){TegakiLayers.setActiveLayer(e),Tegaki.recordEvent(TegakiEventSetActiveLayer,performance.now(),e)},onLayerAlphaDragStart:function(e){TegakiUI.setupDragLabel(e,Tegaki.onLayerAlphaDragMove)},onLayerAlphaDragMove:function(e){var t;e&&((t=Tegaki.activeLayer.alpha+e/100)<0?t=0:t>1&&(t=1),Tegaki.setSelectedLayersAlpha(t))},onLayerAlphaChange:function(){var e=+this.value;(e/=100)<0?e=0:e>1&&(e=1),Tegaki.setSelectedLayersAlpha(e)},setSelectedLayersAlpha:function(e){var t,a,i;if((e=Math.fround(e))>=0&&e<=1&&Tegaki.selectedLayers.size>0){i=[];for(a of Tegaki.selectedLayers)(t=TegakiLayers.getLayerById(a))&&(i.push([t.id,t.alpha]),TegakiLayers.setLayerAlpha(t,e));TegakiUI.updateLayerAlphaOpt(),TegakiHistory.push(new TegakiHistoryActions.SetLayersAlpha(i,e)),Tegaki.recordEvent(TegakiEventSetSelectedLayersAlpha,performance.now(),e)}},onLayerNameChangeClick:function(){var e,t,a;e=+this.getAttribute("data-id"),(a=TegakiLayers.getLayerById(e))&&(t=prompt(undefined,a.name))&&Tegaki.setLayerName(e,t)},setLayerName:function(e,t){var a,i;t=t.trim().slice(0,25),(i=TegakiLayers.getLayerById(e))&&t&&t!==i.name&&(a=i.name,i.name=t,TegakiUI.updateLayerName(i),TegakiHistory.push(new TegakiHistoryActions.SetLayerName(e,a,t)),Tegaki.recordEvent(TegakiEventHistoryDummy,performance.now()))},onLayerAddClick:function(){Tegaki.addLayer()},addLayer:function(){var e;Tegaki.layers.length>=Tegaki.maxLayers?TegakiUI.printMsg(TegakiStrings.tooManyLayers):(TegakiHistory.push(e=TegakiLayers.addLayer()),TegakiLayers.setActiveLayer(e.aLayerIdAfter),Tegaki.recordEvent(TegakiEventAddLayer,performance.now()))},onLayerDeleteClick:function(){Tegaki.deleteSelectedLayers()},deleteSelectedLayers:function(){var e,t;(t=Tegaki.selectedLayers).size!==Tegaki.layers.length&&(!t.size||Tegaki.layers.length<2||(TegakiHistory.push(e=TegakiLayers.deleteLayers(t)),TegakiLayers.selectedLayersClear(),TegakiLayers.setActiveLayer(e.aLayerIdAfter),Tegaki.recordEvent(TegakiEventDeleteLayers,performance.now())))},onLayerToggleVisibilityClick:function(){Tegaki.toggleLayerVisibility(+this.getAttribute("data-id"))},toggleLayerVisibility:function(e){var t=TegakiLayers.getLayerById(e);TegakiLayers.setLayerVisibility(t,!t.visible),Tegaki.recordEvent(TegakiEventToggleLayerVisibility,performance.now(),e)},onMergeLayersClick:function(){Tegaki.mergeSelectedLayers()},mergeSelectedLayers:function(){var e;Tegaki.selectedLayers.size&&(e=TegakiLayers.mergeLayers(Tegaki.selectedLayers))&&(TegakiHistory.push(e),TegakiLayers.setActiveLayer(e.aLayerIdAfter),Tegaki.recordEvent(TegakiEventMergeLayers,performance.now()))},onMoveLayerClick:function(e){var t,a;Tegaki.selectedLayers.size&&(a=e.target.hasAttribute("data-up"),(t=TegakiLayers.getSelectedEdgeLayerPos(a))<0||(a?t+=2:t>=1&&t--,Tegaki.moveSelectedLayers(t)))},moveSelectedLayers:function(e){TegakiHistory.push(TegakiLayers.moveLayers(Tegaki.selectedLayers,e)),Tegaki.recordEvent(TegakiEventMoveLayers,performance.now(),e)},onToolClick:function(){Tegaki.setTool(this.getAttribute("data-tool"))},onToolChanged:function(e){var t;Tegaki.tool=e,(t=$T.cls("tegaki-tool-active")[0])&&t.classList.remove("tegaki-tool-active"),$T.id("tegaki-tool-btn-"+e.name).classList.add("tegaki-tool-active"),Tegaki.recordEvent(TegakiEventSetTool,performance.now(),Tegaki.tool.id),TegakiUI.onToolChanged(),Tegaki.updateCursorStatus()},onLayerStackChanged:function(){},onOpenFileSelected:function(){var e;this.files&&this.files[0]&&((e=new Image).onload=Tegaki.onOpenImageLoaded,e.onerror=Tegaki.onOpenImageError,e.src=URL.createObjectURL(this.files[0]))},onOpenImageLoaded:function(){var e={},t=Tegaki;t.hasCustomCanvas=!0,t.copyContextState(t.activeLayer.ctx,e),t.resizeCanvas(this.naturalWidth,this.naturalHeight),t.activeLayer.ctx.drawImage(this,0,0),TegakiLayers.syncLayerImageData(t.activeLayer),t.copyContextState(e,t.activeLayer.ctx),t.setZoom(0),TegakiHistory.clear(),TegakiUI.updateLayerPreviewSize(!0),t.startTimeStamp=Date.now(),t.saveReplay&&(t.replayRecorder.stop(),t.replayRecorder=null,t.saveReplay=!1,TegakiUI.setRecordingStatus(!1))},onOpenImageError:function(){TegakiUI.printMsg(TegakiStrings.errorLoadImage)},resizeCanvas:function(e,t){Tegaki.baseWidth=e,Tegaki.baseHeight=t,Tegaki.createBuffers(),Tegaki.canvas.width=e,Tegaki.canvas.height=t,Tegaki.ctx.fillStyle=Tegaki.bgColor,Tegaki.ctx.fillRect(0,0,e,t),Tegaki.activeLayer=null,Tegaki.resetLayers(),Tegaki.updateLayersCntSize(),Tegaki.updatePosOffset(),TegakiCursor.updateCanvasSize()},copyContextState:function(e,t){var a,i,s=["lineCap","lineJoin","strokeStyle","fillStyle","globalAlpha","lineWidth","globalCompositeOperation"];for(a=0;i=s[a];++a)t[i]=e[i]},updateCursorStatus:function(){!Tegaki.tool.noCursor&&Tegaki.tool.size>1?Tegaki.cursor=TegakiCursor.generate(Tegaki.tool.size):(Tegaki.cursor=!1,TegakiCursor.clear())},updatePosOffset:function(){var e=Tegaki.canvas.getBoundingClientRect();Tegaki.offsetX=e.left+window.pageXOffset+Tegaki.canvasCnt.scrollLeft+Tegaki.layersCnt.scrollLeft,Tegaki.offsetY=e.top+window.pageYOffset+Tegaki.canvasCnt.scrollTop+Tegaki.layersCnt.scrollTop},getScrollbarSizes:function(){return[Tegaki.canvasCnt.offsetWidth-Tegaki.canvasCnt.clientWidth,Tegaki.canvasCnt.offsetHeight-Tegaki.canvasCnt.clientHeight]},isScrollbarClick:function(e){var[t,a]=Tegaki.getScrollbarSizes();return t>0&&e.clientX>=Tegaki.canvasCnt.offsetLeft+Tegaki.canvasCnt.clientWidth&&e.clientX<=Tegaki.canvasCnt.offsetLeft+Tegaki.canvasCnt.clientWidth+t||a>0&&e.clientY>=Tegaki.canvasCnt.offsetTop+Tegaki.canvasCnt.clientHeight&&e.clientY<=Tegaki.canvasCnt.offsetTop+Tegaki.canvasCnt.clientHeight+t},onPointerMove:function(e){var t,a,i,s,r,n;if(Tegaki.cursor&&TegakiCursor.render(e.clientX,e.clientY),e.mozInputSource!==undefined){if(Tegaki.activePointerIsPen&&"mouse"===e.pointerType)return}else if(Tegaki.activePointerId!==e.pointerId)return void(Tegaki.activePointerId=e.pointerId);if(Tegaki.isPainting)if(s=Tegaki.tool,Tegaki.activePointerIsPen&&e.getCoalescedEvents){t=e.getCoalescedEvents(),r=e.timeStamp;for(e of t)a=Tegaki.getPointerPos(e,0),i=Tegaki.getPointerPos(e,1),s.enabledDynamics()?(n=TegakiPressure.toShort(e.pressure),TegakiPressure.push(n),Tegaki.recordEvent(TegakiEventDraw,r,a,i,n)):Tegaki.recordEvent(TegakiEventDrawNoP,r,a,i),s.draw(a,i)}else a=Tegaki.getPointerPos(e,0),i=Tegaki.getPointerPos(e,1),n=TegakiPressure.toShort(e.pressure),Tegaki.recordEvent(TegakiEventDraw,e.timeStamp,a,i,n),TegakiPressure.push(n),s.draw(a,i);else a=Tegaki.getPointerPos(e,0),i=Tegaki.getPointerPos(e,1)},onPointerDown:function(e){var t,a,i,s;Tegaki.cursor&&TegakiCursor.render(e.clientX,e.clientY),Tegaki.isScrollbarClick(e)||(Tegaki.activePointerId=e.pointerId,Tegaki.activePointerIsPen="pen"===e.pointerType,null!==Tegaki.activeLayer?TegakiLayers.getActiveLayer().visible?(t=Tegaki.getPointerPos(e,0),a=Tegaki.getPointerPos(e,1),2===e.button||e.altKey?(e.preventDefault(),Tegaki.isPainting=!1,Tegaki.tools.pipette.draw(t,a)):0===e.button&&(e.preventDefault(),(i=Tegaki.tool).enabledDynamics()?(s=TegakiPressure.toShort(e.pressure),TegakiPressure.push(s),Tegaki.recordEvent(TegakiEventDrawStart,e.timeStamp,t,a,s)):Tegaki.recordEvent(TegakiEventDrawStartNoP,e.timeStamp,t,a),Tegaki.isPainting=!0,TegakiHistory.pendingAction=new TegakiHistoryActions.Draw(Tegaki.activeLayer.id),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,0),i.start(t,a))):e.target.parentNode===Tegaki.layersCnt&&TegakiUI.printMsg(TegakiStrings.hiddenActiveLayer):e.target.parentNode===Tegaki.layersCnt&&TegakiUI.printMsg(TegakiStrings.noActiveLayer))},onPointerUp:function(e){Tegaki.activePointerId=e.pointerId,Tegaki.activePointerIsPen=!1,Tegaki.isPainting&&(Tegaki.recordEvent(TegakiEventDrawCommit,e.timeStamp),Tegaki.tool.commit(),TegakiUI.updateLayerPreview(Tegaki.activeLayer),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,1),TegakiHistory.push(TegakiHistory.pendingAction),Tegaki.isPainting=!1)},onPointerOut:function(){TegakiCursor.clearAll()},onDummy:function(e){e.preventDefault(),e.stopPropagation()},recordEvent(e,...t){Tegaki.replayRecorder&&Tegaki.replayRecorder.push(new e(...t))}},TegakiColorPalettes=[["#ffffff","#000000","#888888","#b47575","#c096c0","#fa9696","#8080ff","#ffb6ff","#e7e58d","#25c7c9","#99cb7b","#e7962d","#f9ddcf","#fcece2"],["#000000","#ffffff","#7f7f7f","#c3c3c3","#880015","#b97a57","#ed1c24","#ffaec9","#ff7f27","#ffc90e","#fff200","#efe4b0","#22b14c","#b5e61d","#00a2e8","#99d9ea","#3f48cc","#7092be","#a349a4","#c8bfe7"],["#000000","#ffffff","#8a8a8a","#cacaca","#fcece2","#f9ddcf","#e0a899","#a05b53","#7a444a","#960018","#c41e3a","#de4537","#ff3300","#ff9800","#ffc107","#ffd700","#ffeb3b","#ffffcc","#f3e5ab","#cddc39","#8bc34a","#4caf50","#3e8948","#355e3b","#3eb489","#f0f8ff","#87ceeb","#6699cc","#007fff","#2d68c4","#364478","#352c4a","#9c27b0","#da70d6","#ff0090","#fa8072","#f19cbb","#c78b95"]],TegakiPressure={pressureNow:0,pressureThen:0,toShort:function(e){return 0|65535*e},get:function(){return this.pressureNow},lerp:function(e){return this.pressureThen*(1-e)+this.pressureNow*e},push:function(e){this.pressureThen=this.pressureNow,this.pressureNow=e/65535},set:function(e){this.pressureThen=this.pressureNow=e/65535}};class TegakiEvent_void{constructor(){this.size=5}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp)}static unpack(e){return new this(e.readUint32())}}class TegakiEvent_c{constructor(){this.size=6}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeUint8(this.value)}static unpack(e){return new this(e.readUint32(),e.readUint8())}}class TegakiEventPrelude extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){}}class TegakiEventConclusion extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){}}class TegakiEventHistoryDummy extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){TegakiHistory.push(new TegakiHistoryActions.Dummy)}}class TegakiEventDrawStart{constructor(e,t,a,i){this.timeStamp=e,this.x=t,this.y=a,this.pressure=i,this.type=TegakiEvents[this.constructor.name][0],this.size=11}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeInt16(this.x),e.writeInt16(this.y),e.writeUint16(this.pressure)}static unpack(e){var t,a,i,s;return t=e.readUint32(),a=e.readInt16(),i=e.readInt16(),s=e.readUint16(),new TegakiEventDrawStart(t,a,i,s)}dispatch(){TegakiPressure.set(this.pressure),TegakiHistory.pendingAction=new TegakiHistoryActions.Draw(Tegaki.activeLayer.id),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,0),Tegaki.tool.start(this.x,this.y)}}class TegakiEventDrawStartNoP{constructor(e,t,a){this.timeStamp=e,this.x=t,this.y=a,this.type=TegakiEvents[this.constructor.name][0],this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeInt16(this.x),e.writeInt16(this.y)}static unpack(e){var t,a,i;return t=e.readUint32(),a=e.readInt16(),i=e.readInt16(),new TegakiEventDrawStartNoP(t,a,i)}dispatch(){TegakiPressure.set(.5),TegakiHistory.pendingAction=new TegakiHistoryActions.Draw(Tegaki.activeLayer.id),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,0),Tegaki.tool.start(this.x,this.y)}}class TegakiEventDraw{constructor(e,t,a,i){this.timeStamp=e,this.x=t,this.y=a,this.pressure=i,this.type=TegakiEvents[this.constructor.name][0],this.size=11}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeInt16(this.x),e.writeInt16(this.y),e.writeUint16(this.pressure)}static unpack(e){var t,a,i,s;return t=e.readUint32(),a=e.readInt16(),i=e.readInt16(),s=e.readUint16(),new TegakiEventDraw(t,a,i,s)}dispatch(){TegakiPressure.push(this.pressure),Tegaki.tool.draw(this.x,this.y)}}class TegakiEventDrawNoP{constructor(e,t,a){this.timeStamp=e,this.x=t,this.y=a,this.type=TegakiEvents[this.constructor.name][0],this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeInt16(this.x),e.writeInt16(this.y)}static unpack(e){var t,a,i;return t=e.readUint32(),a=e.readInt16(),i=e.readInt16(),new TegakiEventDrawNoP(t,a,i)}dispatch(){TegakiPressure.push(.5),Tegaki.tool.draw(this.x,this.y)}}class TegakiEventDrawCommit extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){Tegaki.tool.commit(),TegakiUI.updateLayerPreview(Tegaki.activeLayer),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,1),TegakiHistory.push(TegakiHistory.pendingAction),Tegaki.isPainting=!1}}class TegakiEventUndo extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){TegakiHistory.undo()}}class TegakiEventRedo extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){TegakiHistory.redo()}}class TegakiEventSetColor{constructor(e,t){this.timeStamp=e,[this.r,this.g,this.b]=t,this.type=TegakiEvents[this.constructor.name][0],this.size=8,this.coalesce=!0}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeUint8(this.r),e.writeUint8(this.g),e.writeUint8(this.b)}static unpack(e){var t,a;return t=e.readUint32(),a=[e.readUint8(),e.readUint8(),e.readUint8()],new TegakiEventSetColor(t,a)}dispatch(){Tegaki.setToolColorRGB(this.r,this.g,this.b)}}class TegakiEventSetTool extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolById(this.value)}}class TegakiEventSetToolSize extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolSize(this.value)}}class TegakiEventSetToolAlpha{constructor(e,t){this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0,this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeFloat32(this.value)}static unpack(e){return new this(e.readUint32(),e.readFloat32())}dispatch(){Tegaki.setToolAlpha(this.value)}}class TegakiEventSetToolFlow{constructor(e,t){this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0,this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeFloat32(this.value)}static unpack(e){return new this(e.readUint32(),e.readFloat32())}dispatch(){Tegaki.setToolFlow(this.value)}}class TegakiEventPreserveAlpha extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolPreserveAlpha(!!this.value)}}class TegakiEventSetToolSizeDynamics extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolSizeDynamics(!!this.value)}}class TegakiEventSetToolAlphaDynamics extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolAlphaDynamics(!!this.value)}}class TegakiEventSetToolFlowDynamics extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolFlowDynamics(!!this.value)}}class TegakiEventSetToolTip extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolTip(this.value)}}class TegakiEventAddLayer extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){Tegaki.addLayer()}}class TegakiEventDeleteLayers extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){Tegaki.deleteSelectedLayers()}}class TegakiEventMoveLayers extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0]}dispatch(){Tegaki.moveSelectedLayers(this.value)}}class TegakiEventMergeLayers extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){Tegaki.mergeSelectedLayers()}}class TegakiEventToggleLayerVisibility extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0]}dispatch(){Tegaki.toggleLayerVisibility(this.value)}}class TegakiEventSetActiveLayer extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0]}dispatch(){Tegaki.setActiveLayer(this.value)}}class TegakiEventToggleLayerSelection extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0]}dispatch(){Tegaki.toggleSelectedLayer(this.value)}}class TegakiEventSetSelectedLayersAlpha{constructor(e,t){this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0,this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeFloat32(this.value)}static unpack(e){return new this(e.readUint32(),e.readFloat32())}dispatch(){Tegaki.setSelectedLayersAlpha(this.value)}}const TegakiEvents=Object.freeze({TegakiEventPrelude:[0,TegakiEventPrelude],TegakiEventDrawStart:[1,TegakiEventDrawStart],TegakiEventDraw:[2,TegakiEventDraw],TegakiEventDrawCommit:[3,TegakiEventDrawCommit],TegakiEventUndo:[4,TegakiEventUndo],TegakiEventRedo:[5,TegakiEventRedo],TegakiEventSetColor:[6,TegakiEventSetColor],TegakiEventDrawStartNoP:[7,TegakiEventDrawStartNoP],TegakiEventDrawNoP:[8,TegakiEventDrawNoP],TegakiEventSetTool:[10,TegakiEventSetTool],TegakiEventSetToolSize:[11,TegakiEventSetToolSize],TegakiEventSetToolAlpha:[12,TegakiEventSetToolAlpha],TegakiEventSetToolSizeDynamics:[13,TegakiEventSetToolSizeDynamics],TegakiEventSetToolAlphaDynamics:[14,TegakiEventSetToolAlphaDynamics],TegakiEventSetToolTip:[15,TegakiEventSetToolTip],TegakiEventPreserveAlpha:[16,TegakiEventPreserveAlpha],TegakiEventSetToolFlowDynamics:[17,TegakiEventSetToolFlowDynamics],TegakiEventSetToolFlow:[18,TegakiEventSetToolFlow],TegakiEventAddLayer:[20,TegakiEventAddLayer],TegakiEventDeleteLayers:[21,TegakiEventDeleteLayers],TegakiEventMoveLayers:[22,TegakiEventMoveLayers],TegakiEventMergeLayers:[23,TegakiEventMergeLayers],TegakiEventToggleLayerVisibility:[24,TegakiEventToggleLayerVisibility],TegakiEventSetActiveLayer:[25,TegakiEventSetActiveLayer],TegakiEventToggleLayerSelection:[26,TegakiEventToggleLayerSelection],TegakiEventSetSelectedLayersAlpha:[27,TegakiEventSetSelectedLayersAlpha],TegakiEventHistoryDummy:[254,TegakiEventHistoryDummy],TegakiEventConclusion:[255,TegakiEventConclusion]});class TegakiReplayRecorder{constructor(){this.formatVersion=1,this.compressed=!0,this.tegakiVersion=Tegaki.VERSION.split(".").map(e=>+e),this.canvasWidth=Tegaki.baseWidth,this.canvasHeight=Tegaki.baseHeight,this.bgColor=$T.hexToRgb(Tegaki.bgColor),this.toolColor=$T.hexToRgb(Tegaki.toolColor),this.toolId=Tegaki.tools[Tegaki.defaultTool].id,this.toolList=this.buildToolList(Tegaki.tools),this.startTimeStamp=0,this.endTimeStamp=0,this.recording=!1,this.events=[],this.mimeType="application/octet-stream"}buildToolList(e){var t,a,i;i=[];for(t in e)a=e[t],i.push({id:a.id,size:a.size,alpha:a.alpha,flow:a.flow,step:a.step,sizeDynamicsEnabled:+a.sizeDynamicsEnabled,alphaDynamicsEnabled:+a.alphaDynamicsEnabled,flowDynamicsEnabled:+a.flowDynamicsEnabled,usePreserveAlpha:+a.usePreserveAlpha,tipId:a.tipId});return i}start(){this.recording||(this.endTimeStamp>0&&(this.events.pop(),this.endTimeStamp=0),0===this.startTimeStamp&&(this.events.push(new TegakiEventPrelude(performance.now())),this.startTimeStamp=Date.now()),this.recording=!0)}stop(){ +0!==this.startTimeStamp&&this.recording&&(this.events.push(new TegakiEventConclusion(performance.now())),this.endTimeStamp=Date.now(),this.recording=!1)}push(e){this.recording&&(e.coalesce&&this.events[this.events.length-1].type===e.type?this.events[this.events.length-1]=e:this.events.push(e))}getEventStackSize(){var e,t;t=4;for(e of this.events)t+=e.size;return t}getHeaderSize(){return 12}getMetaSize(){return 21}getToolSize(){return 19}getToolListSize(){return 2+this.toolList.length*this.getToolSize()}writeToolList(e){var t,a,i;i=[["id","Uint8"],["size","Uint8"],["alpha","Float32"],["step","Float32"],["sizeDynamicsEnabled","Uint8"],["alphaDynamicsEnabled","Uint8"],["usePreserveAlpha","Uint8"],["tipId","Int8"],["flow","Float32"],["flowDynamicsEnabled","Uint8"]],e.writeUint8(this.toolList.length),e.writeUint8(this.getToolSize());for(t of this.toolList)for(a of i)e["write"+a[1]](t[a[0]])}writeMeta(e){e.writeUint16(this.getMetaSize()),e.writeUint32(Math.ceil(this.startTimeStamp/1e3)),e.writeUint32(Math.ceil(this.endTimeStamp/1e3)),e.writeUint16(this.canvasWidth),e.writeUint16(this.canvasHeight),e.writeUint8(this.bgColor[0]),e.writeUint8(this.bgColor[1]),e.writeUint8(this.bgColor[2]),e.writeUint8(this.toolColor[0]),e.writeUint8(this.toolColor[1]),e.writeUint8(this.toolColor[2]),e.writeUint8(this.toolId)}writeEventStack(e){var t;e.writeUint32(this.events.length);for(t of this.events)t.pack(e)}writeHeader(e,t){e.writeUint8(84),e.writeUint8(71),e.writeUint8(75),e.writeUint8(+this.compressed),e.writeUint32(t),e.writeUint8(this.tegakiVersion[0]),e.writeUint8(this.tegakiVersion[1]),e.writeUint8(this.tegakiVersion[2]),e.writeUint8(this.formatVersion)}compressData(e){return UZIP.deflateRaw(new Uint8Array(e.buf),{level:9})}toUint8Array(){var e,t,a,i,s,r;return this.startTimeStamp&&this.endTimeStamp?(e=this.getHeaderSize(),t=this.getMetaSize()+this.getToolListSize()+this.getEventStackSize(),a=new ArrayBuffer(t),i=new TegakiBinWriter(a),this.writeMeta(i),this.writeToolList(i),this.writeEventStack(i),s=this.compressData(i),i=new TegakiBinWriter(new ArrayBuffer(e+s.length)),this.writeHeader(i,t),(r=new Uint8Array(i.buf)).set(s,e),r):null}toBlob(){var e=this.toUint8Array();return e?new Blob([e.buffer],{type:this.mimeType}):null}}class TegakiReplayViewer{constructor(){this.formatVersion=1,this.compressed=!0,this.tegakiVersion=[0,0,0],this.dataSize=0,this.canvasWidth=0,this.canvasHeight=0,this.bgColor=[0,0,0],this.toolColor=[0,0,0],this.toolId=1,this.toolMap={},this.startTimeStamp=0,this.endTimeStamp=0,this.loaded=!1,this.playing=!1,this.gapless=!0,this.autoPaused=!1,this.destroyed=!1,this.speedIndex=1,this.speedList=[.5,1,2,5,10,25],this.speed=this.speedList[this.speedIndex],this.maxEventsPerFrame=50,this.maxEventCount=864e4,this.events=[],this.preludePos=0,this.currentPos=0,this.conclusionPos=0,this.duration=0,this.playTimeStart=0,this.playTimeCurrent=0,this.eventIndex=0,this.maxCanvasWH=8192,this.maxGapTime=3e3,this.uiAccTime=0,this.onFrameThis=this.onFrame.bind(this)}destroy(){this.destroyed=!0,this.pause(),this.events=null}speedUp(){this.speedIndex+1=0&&(this.speed=this.speedList[--this.speedIndex])}toggleGapless(){this.gapless=!this.gapless}getCurrentPos(){return this.currentPos}getDuration(){return this.duration}loadFromURL(e){fetch(e).then(e=>this.onResponseReady(e))["catch"](e=>this.onLoadError(e))}onResponseReady(e){e.ok?e.arrayBuffer().then(e=>this.onResponseBodyReady(e))["catch"](e=>this.onLoadError(e)):this.onLoadError(e.statusText)}onResponseBodyReady(e){this.loadFromBuffer(e),Tegaki.onReplayLoaded()}onLoadError(e){TegakiUI.printMsg(TegakiStrings.errorLoadReplay+e,0)}autoPause(){this.autoPaused=!0,this.pause()}pause(e){window.cancelAnimationFrame(this.onFrameThis),this.playing=!1,e&&(this.currentPos=0,this.eventIndex=0),Tegaki.onReplayTimeChanged(),Tegaki.onReplayPlayPauseChanged()}rewind(){this.autoPaused=!1,this.pause(!0),Tegaki.onReplayReset()}play(){this.playTimeStart=performance.now(),this.playTimeCurrent=this.playTimeStart,this.playing=!0,this.autoPaused=!1,this.uiAccTime=0,Tegaki.onReplayPlayPauseChanged(),window.requestAnimationFrame(this.onFrameThis)}togglePlayPause(){this.playing?this.pause():this.play()}onFrame(e){var t=e-this.playTimeCurrent;this.playing&&(this.playTimeCurrent=e,this.step(t),this.uiAccTime+=t,this.uiAccTime>1e3&&(Tegaki.onReplayTimeChanged(),this.uiAccTime=0),this.currentPosthis.maxGapTime&&(this.currentPos=t.timeStamp-this.preludePos,a=t.timeStamp),i=0;this.eventIndex=this.maxEventsPerFrame){this.currentPos=t.timeStamp-this.preludePos;break}t.dispatch(),++this.eventIndex,++i}}getEventIdMap(){var e,t,a;e={};for(t in TegakiEvents)e[(a=TegakiEvents[t])[0]]=a[1];return e}readToolMap(e){var t,a,i,s,r,n,o;for(this.toolMap={},n=[["id","Uint8"],["size","Uint8"],["alpha","Float32"],["step","Float32"],["sizeDynamicsEnabled","Uint8"],["alphaDynamicsEnabled","Uint8"],["usePreserveAlpha","Uint8"],["tipId","Int8"],["flow","Float32"],["flowDynamicsEnabled","Uint8"]],a=e.readUint8(),i=e.readUint8(),t=0;t=o)break;s[r[0]]=e["read"+r[1]]()}this.toolMap[s.id]=s,e.pos=o}}readHeader(e){if("TGK"!==String.fromCharCode(e.readUint8(),e.readUint8(),e.readUint8()))throw"invalid header";this.compressed=1===e.readUint8(),this.dataSize=e.readUint32(),this.tegakiVersion[0]=e.readUint8(),this.tegakiVersion[1]=e.readUint8(),this.tegakiVersion[2]=e.readUint8(),this.formatVersion=e.readUint8()}decompressData(e){return UZIP.inflateRaw(new Uint8Array(e.buf,e.pos),new Uint8Array(this.dataSize))}readMeta(e){var t,a;if(a=e.readUint16(),t=e.pos+a-2,this.startTimeStamp=1e3*e.readUint32(),this.endTimeStamp=1e3*e.readUint32(),this.canvasWidth=e.readUint16(),this.canvasHeight=e.readUint16(),this.canvasWidth>this.maxCanvasWH||this.canvasHeight>this.maxCanvasWH)throw"canvas too large";this.bgColor[0]=e.readUint8(),this.bgColor[1]=e.readUint8(),this.bgColor[2]=e.readUint8(),this.toolColor[0]=e.readUint8(),this.toolColor[1]=e.readUint8(),this.toolColor[2]=e.readUint8(),this.toolId=e.readUint8(),e.pos=t}readEventStack(e){var t,a,i,s,r;if(r=this.getEventIdMap(),(a=e.readUint32())<1||a>this.maxEventCount)throw"invalid event count";for(t=0;t0&&(TegakiUI.statusTimeout=setTimeout(TegakiUI.clearMsg,5e3))},clearMsg:function(){TegakiUI.statusTimeout&&(clearTimeout(TegakiUI.statusTimeout),TegakiUI.statusTimeout=0),$T.id("tegaki-status-output").textContent=""},buildUI:function(){var e,t,a,i,s,r;return(e=$T.el("div")).id="tegaki",(a=$T.el("div")).id="tegaki-menu-cnt",Tegaki.replayMode?(a.appendChild(TegakiUI.buildViewerMenuBar()),a.appendChild(TegakiUI.buildReplayControls())):a.appendChild(TegakiUI.buildMenuBar()),a.appendChild(TegakiUI.buildToolModeBar()),e.appendChild(a),e.appendChild(TegakiUI.buildDummyFilePicker()),(t=$T.el("div")).id="tegaki-tools-cnt",t.appendChild(TegakiUI.buildToolsMenu()),e.appendChild(t),[r,s]=TegakiUI.buildCanvasCnt(),e.appendChild(r),(i=$T.el("div")).id="tegaki-ctrl-cnt",i.appendChild(TegakiUI.buildZoomCtrlGroup()),i.appendChild(TegakiUI.buildColorCtrlGroup(Tegaki.toolColor)),i.appendChild(TegakiUI.buildSizeCtrlGroup()),i.appendChild(TegakiUI.buildAlphaCtrlGroup()),i.appendChild(TegakiUI.buildFlowCtrlGroup()),i.appendChild(TegakiUI.buildLayersCtrlGroup()),e.appendChild(i),e.appendChild(TegakiUI.buildStatusCnt()),[e,r,s]},buildDummyFilePicker:function(){var e=$T.el("input");return e.type="file",e.id="tegaki-filepicker",e.className="tegaki-hidden",e.accept="image/png, image/jpeg",$T.on(e,"change",Tegaki.onOpenFileSelected),e},buildMenuBar:function(){var e,t;return(e=$T.el("div")).id="tegaki-menu-bar",(t=$T.el("span")).className="tegaki-mb-btn",t.textContent=TegakiStrings.newCanvas,$T.on(t,"click",Tegaki.onNewClick),e.appendChild(t),(t=$T.el("span")).className="tegaki-mb-btn",t.textContent=TegakiStrings.open,$T.on(t,"click",Tegaki.onOpenClick),e.appendChild(t),(t=$T.el("span")).className="tegaki-mb-btn",t.textContent=TegakiStrings["export"],$T.on(t,"click",Tegaki.onExportClick),e.appendChild(t),(t=$T.el("span")).id="tegaki-undo-btn",t.className="tegaki-mb-btn",t.textContent=TegakiStrings.undo,t.title=TegakiKeybinds.getCaption("undo"),$T.on(t,"click",Tegaki.onUndoClick),e.appendChild(t),(t=$T.el("span")).id="tegaki-redo-btn",t.className="tegaki-mb-btn",t.textContent=TegakiStrings.redo,t.title=TegakiKeybinds.getCaption("redo"),$T.on(t,"click",Tegaki.onRedoClick),e.appendChild(t),(t=$T.el("span")).className="tegaki-mb-btn",t.textContent=TegakiStrings.close,$T.on(t,"click",Tegaki.onCancelClick),e.appendChild(t),(t=$T.el("span")).id="tegaki-finish-btn",t.className="tegaki-mb-btn",t.textContent=TegakiStrings.finish,$T.on(t,"click",Tegaki.onDoneClick),e.appendChild(t),e},buildViewerMenuBar:function(){var e,t;return(e=$T.el("div")).id="tegaki-menu-bar",(t=$T.el("span")).id="tegaki-finish-btn",t.className="tegaki-mb-btn",t.textContent=TegakiStrings.close,$T.on(t,"click",Tegaki.onCloseViewerClick),e.appendChild(t),e},buildToolModeBar:function(){var e,t,a,i;return(e=$T.el("div")).id="tegaki-toolmode-bar",Tegaki.tool||e.classList.add("tegaki-hidden"),(t=$T.el("span")).id="tegaki-tool-mode-dynamics",t.className="tegaki-toolmode-grp",(a=$T.el("span")).className="tegaki-toolmode-lbl",a.textContent=TegakiStrings.pressure,t.appendChild(a),(a=$T.el("span")).id="tegaki-tool-mode-dynamics-ctrl",a.className="tegaki-toolmode-ctrl",(i=$T.el("span")).id="tegaki-tool-mode-dynamics-size",i.className="tegaki-sw-btn",i.textContent=TegakiStrings.size,$T.on(i,"mousedown",Tegaki.onToolPressureSizeClick),a.appendChild(i),(i=$T.el("span")).id="tegaki-tool-mode-dynamics-alpha",i.className="tegaki-sw-btn",i.textContent=TegakiStrings.alpha,$T.on(i,"mousedown",Tegaki.onToolPressureAlphaClick),a.appendChild(i),(i=$T.el("span")).id="tegaki-tool-mode-dynamics-flow",i.className="tegaki-sw-btn",i.textContent=TegakiStrings.flow,$T.on(i,"mousedown",Tegaki.onToolPressureFlowClick),a.appendChild(i),t.appendChild(a),e.appendChild(t),(t=$T.el("span")).id="tegaki-tool-mode-mask",t.className="tegaki-toolmode-grp",(a=$T.el("span")).id="tegaki-toolmode-ctrl-tip",a.className="tegaki-toolmode-ctrl",(i=$T.el("span")).id="tegaki-tool-mode-mask-alpha",i.className="tegaki-sw-btn",i.textContent=TegakiStrings.preserveAlpha,$T.on(i,"mousedown",Tegaki.onToolPreserveAlphaClick),a.appendChild(i),t.appendChild(a),e.appendChild(t),(t=$T.el("span")).id="tegaki-tool-mode-tip",t.className="tegaki-toolmode-grp",(a=$T.el("span")).className="tegaki-toolmode-lbl",a.textContent=TegakiStrings.tip,t.appendChild(a),(a=$T.el("span")).id="tegaki-tool-mode-tip-ctrl",a.className="tegaki-toolmode-ctrl",t.appendChild(a),e.appendChild(t),e},buildToolsMenu:function(){var e,t,a,i;(e=$T.el("div")).id="tegaki-tools-grid";for(i in Tegaki.tools)(t=$T.el("span")).setAttribute("data-tool",i),a=TegakiStrings[i],Tegaki.tools[i].keybind&&(a+=" ("+Tegaki.tools[i].keybind.toUpperCase()+")"),t.setAttribute("title",a),t.id="tegaki-tool-btn-"+i,t.className="tegaki-tool-btn tegaki-icon tegaki-"+i,$T.on(t,"click",Tegaki.onToolClick),e.appendChild(t);return e},buildCanvasCnt:function(){var e,t,a;return(e=$T.el("div")).id="tegaki-canvas-cnt",(t=$T.el("div")).id="tegaki-layers-wrap",(a=$T.el("div")).id="tegaki-layers",t.appendChild(a),e.appendChild(t),[e,a]},buildCtrlGroup:function(e,t){var a,i;return(a=$T.el("div")).className="tegaki-ctrlgrp",e&&(a.id="tegaki-ctrlgrp-"+e),t!==undefined&&((i=$T.el("div")).className="tegaki-ctrlgrp-title",i.textContent=t,a.appendChild(i)),a},buildLayersCtrlGroup:function(){var e,t,a,i;return t=this.buildCtrlGroup("layers",TegakiStrings.layers),(a=$T.el("div")).id="tegaki-layers-opts",(i=$T.el("div")).id="tegaki-layer-alpha-cell",(e=$T.el("span")).className="tegaki-label-xs tegaki-lbl-c tegaki-drag-lbl",e.textContent=TegakiStrings.alpha,$T.on(e,"pointerdown",Tegaki.onLayerAlphaDragStart),i.appendChild(e),(e=$T.el("input")).id="tegaki-layer-alpha-opt",e.className="tegaki-stealth-input tegaki-range-lbl-xs",e.setAttribute("maxlength",3),$T.on(e,"input",Tegaki.onLayerAlphaChange),i.appendChild(e),a.appendChild(i),t.appendChild(a),(e=$T.el("div")).id="tegaki-layers-grid",t.appendChild(e),(a=$T.el("div")).id="tegaki-layers-ctrl",(e=$T.el("span")).title=TegakiStrings.addLayer,e.className="tegaki-ui-btn tegaki-icon tegaki-plus",$T.on(e,"click",Tegaki.onLayerAddClick),a.appendChild(e),(e=$T.el("span")).title=TegakiStrings.delLayers,e.className="tegaki-ui-btn tegaki-icon tegaki-minus",$T.on(e,"click",Tegaki.onLayerDeleteClick),a.appendChild(e),(e=$T.el("span")).id="tegaki-layer-merge",e.title=TegakiStrings.mergeLayers,e.className="tegaki-ui-btn tegaki-icon tegaki-level-down",$T.on(e,"click",Tegaki.onMergeLayersClick),a.appendChild(e),(e=$T.el("span")).id="tegaki-layer-up",e.title=TegakiStrings.moveLayerUp,e.setAttribute("data-up","1"),e.className="tegaki-ui-btn tegaki-icon tegaki-up-open",$T.on(e,"click",Tegaki.onMoveLayerClick),a.appendChild(e),(e=$T.el("span")).id="tegaki-layer-down",e.title=TegakiStrings.moveLayerDown,e.className="tegaki-ui-btn tegaki-icon tegaki-down-open",$T.on(e,"click",Tegaki.onMoveLayerClick),a.appendChild(e),t.appendChild(a),t},buildSizeCtrlGroup:function(){var e,t,a;return t=this.buildCtrlGroup("size",TegakiStrings.size),(a=$T.el("div")).className="tegaki-ctrlrow",(e=$T.el("input")).id="tegaki-size",e.className="tegaki-ctrl-range",e.min=1,e.max=Tegaki.maxSize,e.type="range",e.title=TegakiKeybinds.getCaption("toolSize"),$T.on(e,"input",Tegaki.onToolSizeChange),a.appendChild(e),(e=$T.el("input")).id="tegaki-size-lbl",e.setAttribute("maxlength",3),e.className="tegaki-stealth-input tegaki-range-lbl",$T.on(e,"input",Tegaki.onToolSizeChange),a.appendChild(e),t.appendChild(a),t},buildAlphaCtrlGroup:function(){var e,t,a;return t=this.buildCtrlGroup("alpha",TegakiStrings.alpha),(a=$T.el("div")).className="tegaki-ctrlrow",(e=$T.el("input")).id="tegaki-alpha",e.className="tegaki-ctrl-range",e.min=0,e.max=100,e.step=1,e.type="range",$T.on(e,"input",Tegaki.onToolAlphaChange),a.appendChild(e),(e=$T.el("input")).id="tegaki-alpha-lbl",e.setAttribute("maxlength",3),e.className="tegaki-stealth-input tegaki-range-lbl",$T.on(e,"input",Tegaki.onToolAlphaChange),a.appendChild(e),t.appendChild(a),t},buildFlowCtrlGroup:function(){var e,t,a;return t=this.buildCtrlGroup("flow",TegakiStrings.flow),(a=$T.el("div")).className="tegaki-ctrlrow",(e=$T.el("input")).id="tegaki-flow",e.className="tegaki-ctrl-range",e.min=0,e.max=100,e.step=1,e.type="range",$T.on(e,"input",Tegaki.onToolFlowChange),a.appendChild(e),(e=$T.el("input")).id="tegaki-flow-lbl",e.setAttribute("maxlength",3),e.className="tegaki-stealth-input tegaki-range-lbl",$T.on(e,"input",Tegaki.onToolFlowChange),a.appendChild(e),t.appendChild(a),t},buildZoomCtrlGroup:function(){var e,t,a;return a=this.buildCtrlGroup("zoom",TegakiStrings.zoom),(t=$T.el("div")).className="tegaki-ui-btn tegaki-icon tegaki-plus",t.id="tegaki-zoomin-btn",t.setAttribute("data-in",1),$T.on(t,"click",Tegaki.onZoomChange),a.appendChild(t),(t=$T.el("div")).className="tegaki-ui-btn tegaki-icon tegaki-minus",t.id="tegaki-zoomout-btn",t.setAttribute("data-out",1),$T.on(t,"click",Tegaki.onZoomChange),a.appendChild(t),(e=$T.el("div")).id="tegaki-zoom-lbl",a.appendChild(e),a},buildColorCtrlGroup:function(e){var t,a,i,s,r,n,o,l,g;for(n=/ Edge\//i.test(window.navigator.userAgent),s=this.buildCtrlGroup("color",TegakiStrings.color),(a=$T.el("div")).id="tegaki-color-ctrl",(t=$T.el("div")).id="tegaki-color",n&&t.classList.add("tegaki-hidden"),t.style.backgroundColor=e,$T.on(t,"mousedown",Tegaki.onMainColorClick),a.appendChild(t),(t=$T.el("div")).id="tegaki-palette-switcher",(i=$T.el("span")).id="tegaki-palette-prev-btn",i.title=TegakiStrings.switchPalette,i.setAttribute("data-prev","1"),i.className="tegaki-ui-btn tegaki-icon tegaki-left-open tegaki-disabled",$T.on(i,"click",Tegaki.onSwitchPaletteClick),t.appendChild(i),(i=$T.el("span")).id="tegaki-palette-next-btn",i.title=TegakiStrings.switchPalette,i.className="tegaki-ui-btn tegaki-icon tegaki-right-open",$T.on(i,"click",Tegaki.onSwitchPaletteClick),t.appendChild(i),a.appendChild(t),s.appendChild(a),(a=$T.el("div")).id="tegaki-color-grids",o=0;o0&&(g+=" tegaki-hidden"),t.className=g;for(r of l)(i=$T.el("div")).title=TegakiStrings.paletteSlotReplace,i.className="tegaki-color-btn",i.setAttribute("data-color",r),i.style.backgroundColor=r,$T.on(i,"mousedown",Tegaki.onPaletteColorClick),t.appendChild(i);a.appendChild(t)}return s.appendChild(a),(t=$T.el("input")).id="tegaki-colorpicker",!n&&t.classList.add("tegaki-invis"),t.value=r,t.type="color",$T.on(t,"change",Tegaki.onColorPicked),s.appendChild(t),s},buildStatusCnt:function(){var e,t;return(e=$T.el("div")).id="tegaki-status-cnt",Tegaki.saveReplay&&((t=$T.el("div")).id="tegaki-status-replay",t.textContent="\u2b24",t.setAttribute("title",TegakiStrings.recordingEnabled),e.appendChild(t)),(t=$T.el("div")).id="tegaki-status-output",e.appendChild(t),(t=$T.el("div")).id="tegaki-status-version",t.textContent="tegaki.js v"+Tegaki.VERSION,e.appendChild(t),e},buildReplayControls:function(){var e,t,a;return(e=$T.el("div")).id="tegaki-replay-controls",e.className="tegaki-hidden",(t=$T.el("span")).id="tegaki-replay-gapless-btn",t.className="tegaki-ui-cb-w",$T.on(t,"click",Tegaki.onReplayGaplessClick),(a=$T.el("span")).id="tegaki-replay-gapless-cb",a.className="tegaki-ui-cb",t.appendChild(a),(a=$T.el("span")).className="tegaki-menu-lbl",a.textContent=TegakiStrings.gapless,t.appendChild(a),e.appendChild(t),(t=$T.el("span")).id="tegaki-replay-play-btn",t.className="tegaki-ui-btn tegaki-icon tegaki-play",t.setAttribute("title",TegakiStrings.play),$T.on(t,"click",Tegaki.onReplayPlayPauseClick),e.appendChild(t),(t=$T.el("span")).className="tegaki-ui-btn tegaki-icon tegaki-to-start",t.setAttribute("title",TegakiStrings.rewind),$T.on(t,"click",Tegaki.onReplayRewindClick),e.appendChild(t),(t=$T.el("span")).id="tegaki-replay-slower-btn",t.className="tegaki-ui-btn tegaki-icon tegaki-fast-bw",t.setAttribute("title",TegakiStrings.slower),$T.on(t,"click",Tegaki.onReplaySlowDownClick),e.appendChild(t),(a=$T.el("span")).id="tegaki-replay-speed-lbl",a.className="tegaki-menu-lbl",a.textContent="1.0",e.appendChild(a),(t=$T.el("span")).id="tegaki-replay-faster-btn",t.className="tegaki-ui-btn tegaki-icon tegaki-fast-fw",t.setAttribute("title",TegakiStrings.faster),$T.on(t,"click",Tegaki.onReplaySpeedUpClick),e.appendChild(t),(a=$T.el("span")).id="tegaki-replay-now-lbl",a.className="tegaki-menu-lbl",a.textContent="00:00",e.appendChild(a),(a=$T.el("span")).id="tegaki-replay-end-lbl",a.className="tegaki-menu-lbl",a.textContent="00:00",e.appendChild(a),e},buildLayerGridCell:function(e){var t,a,i;return(t=$T.el("div")).id="tegaki-layers-cell-"+e.id,t.className="tegaki-layers-cell",t.setAttribute("data-id",e.id),t.draggable=!0,t.setAttribute("data-id",e.id),$T.on(t,"pointerdown",TegakiUI.onLayerSelectorPtrDown),$T.on(t,"pointerup",Tegaki.onLayerSelectorClick),$T.on(t,"dragstart",TegakiUI.onLayerDragStart),$T.on(t,"dragover",TegakiUI.onLayerDragOver),$T.on(t,"drop",TegakiUI.onLayerDragDrop),$T.on(t,"dragend",TegakiUI.onLayerDragEnd),$T.on(t,"dragleave",TegakiUI.onLayerDragLeave),$T.on(t,"dragexit",TegakiUI.onLayerDragLeave),(i=$T.el("div")).className="tegaki-layers-cell-v",(a=$T.el("span")).id="tegaki-layers-cb-v-"+e.id,a.className="tegaki-ui-cb",a.setAttribute("data-id",e.id),a.title=TegakiStrings.toggleVisibility,$T.on(a,"click",Tegaki.onLayerToggleVisibilityClick),e.visible&&(a.className+=" tegaki-ui-cb-a"),i.appendChild(a),t.appendChild(i),(i=$T.el("div")).className="tegaki-layers-cell-p",(a=$T.el("canvas")).id="tegaki-layers-p-canvas-"+e.id,a.className="tegaki-alpha-bg-xs",[a.width,a.height]=TegakiUI.getLayerPreviewSize(),i.appendChild(a),t.appendChild(i),(i=$T.el("div")).className="tegaki-layers-cell-n",(a=$T.el("div")).id="tegaki-layer-name-"+e.id,a.className="tegaki-ellipsis",a.setAttribute("data-id",e.id),a.textContent=e.name,$T.on(a,"dblclick",Tegaki.onLayerNameChangeClick),i.appendChild(a),t.appendChild(i),t},onLayerSelectorPtrDown:function(e){"mouse"===e.pointerType?this.hasAttribute("data-nodrag")&&(this.removeAttribute("data-nodrag"),$T.on(this,"dragstart",TegakiUI.onLayerDragStart)):this.hasAttribute("data-nodrag")||(this.setAttribute("data-nodrag",1),$T.off(this,"dragstart",TegakiUI.onLayerDragStart))},onLayerDragStart:function(e){var t,a;e.ctrlKey||(TegakiUI.draggedNode=null,$T.id("tegaki-layers-grid").children[1]?(a=+e.target.getAttribute("data-id"),(t=$T.el("div")).className="tegaki-invis",e.dataTransfer.setDragImage(t,0,0),e.dataTransfer.setData("text/plain",a),e.dataTransfer.effectAllowed="move",TegakiUI.draggedNode=e.target,TegakiUI.updateLayersGridDragExt(!0)):e.preventDefault())},onLayerDragOver:function(e){e.preventDefault(),e.dataTransfer.dropEffect="move",TegakiUI.updateLayersGridDragEffect(e.target,+TegakiUI.draggedNode.getAttribute("data-id"))},onLayerDragLeave:function(){TegakiUI.updateLayersGridDragEffect()},onLayerDragEnd:function(){TegakiUI.draggedNode=null,TegakiUI.updateLayersGridDragExt(!1),TegakiUI.updateLayersGridDragEffect()},onLayerDragDrop:function(e){var t,a,i;e.preventDefault(),TegakiUI.draggedNode=null,[t]=TegakiUI.layersGridFindDropTgt(e.target),a=+e.dataTransfer.getData("text/plain"),TegakiUI.updateLayersGridDragEffect(e.target.parentNode),TegakiUI.updateLayersGridDragExt(!1),TegakiUI.layersGridCanDrop(t,a)&&(i=t?TegakiLayers.getLayerPosById(t):Tegaki.layers.length,TegakiLayers.selectedLayersHas(a)||Tegaki.setActiveLayer(a),Tegaki.moveSelectedLayers(i))},updateLayersGridDragExt:function(e){var t,a;(t=$T.id("tegaki-layers-grid")).children[1]&&(e?((a=$T.el("div")).id="tegaki-layers-cell-dx",a.draggable=!0,$T.on(a,"dragover",TegakiUI.onLayerDragOver),$T.on(a,"drop",TegakiUI.onLayerDragDrop),t.parentNode.insertBefore(a,t)):(a=$T.id("tegaki-layers-cell-dx"))&&a.parentNode.removeChild(a))},updateLayersGridDragEffect:function(e,t){var a,i,s;i=$T.cls("tegaki-layers-cell-d",$T.id("tegaki-ctrlgrp-layers"));for(a of i)a.classList.remove("tegaki-layers-cell-d");e&&t&&([s,e]=TegakiUI.layersGridFindDropTgt(e),TegakiUI.layersGridCanDrop(s,t)&&(e||(e=$T.id("tegaki-layers-grid")),e.classList.add("tegaki-layers-cell-d")))},layersGridFindDropTgt:function(e){var t,a;for(t=+e.getAttribute("data-id"),a=$T.id("tegaki-ctrlgrp-layers");!e.draggable&&e!==a;)t=+(e=e.parentNode).getAttribute("data-id");return e!==a&&e.draggable?[t,e]:[0,null]},layersGridCanDrop:function(e,t){var a;if(e===t)return!1;if((a=$T.id("tegaki-layers-cell-"+t)).previousElementSibling){if(+a.previousElementSibling.getAttribute("data-id")===e)return!1}else if(!e)return!1;return!0},setReplayMode:function(e){Tegaki.bg.classList[e?"add":"remove"]("tegaki-replay-mode")},onToolChanged:function(){$T.id("tegaki-toolmode-bar").classList.remove("tegaki-hidden"),TegakiUI.updateToolSize(),TegakiUI.updateToolAlpha(),TegakiUI.updateToolFlow(),TegakiUI.updateToolModes()},updateLayerAlphaOpt:function(){$T.id("tegaki-layer-alpha-opt").value=Math.round(100*Tegaki.activeLayer.alpha)},updateLayerName:function(e){var t;(t=$T.id("tegaki-layer-name-"+e.id))&&(t.textContent=e.name)},updateLayerPreview:function(e){var t,a;(t=$T.id("tegaki-layers-p-canvas-"+e.id))&&((a=TegakiUI.getLayerPreviewCtx(e))||((a=t.getContext("2d")).imageSmoothingEnabled=!1,TegakiUI.setLayerPreviewCtx(e,a)),$T.clearCtx(a),a.drawImage(e.canvas,0,0,t.width,t.height))},updateLayerPreviewSize:function(e){var t,a,i;i=TegakiUI.getLayerPreviewSize();for(a of Tegaki.layers)(t=$T.id("tegaki-layers-p-canvas-"+a.id))&&([t.width,t.height]=i,e&&TegakiUI.updateLayerPreview(a))},getLayerPreviewCtx:function(e){TegakiUI.layerPreviewCtxCache.get(e)},setLayerPreviewCtx:function(e,t){TegakiUI.layerPreviewCtxCache.set(e,t)},deleteLayerPreviewCtx:function(e){TegakiUI.layerPreviewCtxCache["delete"](e)},updateLayersGridClear:function(){$T.id("tegaki-layers-grid").innerHTML=""},updateLayersGrid:function(){var e,t,a,i;a=$T.frag();for(e of Tegaki.layers)t=TegakiUI.buildLayerGridCell(e),a.insertBefore(t,a.firstElementChild);TegakiUI.updateLayersGridClear(),i.appendChild(a)},updateLayersGridActive:function(e){var t;(t=$T.cls("tegaki-layers-cell-a",$T.id("tegaki-layers-grid"))[0])&&t.classList.remove("tegaki-layers-cell-a"),(t=$T.id("tegaki-layers-cell-"+e))&&t.classList.add("tegaki-layers-cell-a"),TegakiUI.updateLayerAlphaOpt()},updateLayersGridAdd:function(e,t){var a,i,s;a=TegakiUI.buildLayerGridCell(e),i=$T.id("tegaki-layers-grid"),s=t?$T.id("tegaki-layers-cell-"+t):null,i.insertBefore(a,s)},updateLayersGridRemove:function(e){var t;(t=$T.id("tegaki-layers-cell-"+e))&&t.parentNode.removeChild(t)},updayeLayersGridOrder:function(){var e,t,a;t=$T.id("tegaki-layers-grid");for(e of Tegaki.layers)a=$T.id("tegaki-layers-cell-"+e.id),t.insertBefore(a,t.firstElementChild)},updateLayersGridVisibility:function(e,t){var a;(a=$T.id("tegaki-layers-cb-v-"+e))&&(t?a.classList.add("tegaki-ui-cb-a"):a.classList.remove("tegaki-ui-cb-a"))},updateLayersGridSelectedClear:function(){var e,t;for(e of Tegaki.layers)(t=$T.id("tegaki-layers-cell-"+e.id))&&t.classList.remove("tegaki-layers-cell-s")},updateLayersGridSelectedSet:function(e,t){var a;(a=$T.id("tegaki-layers-cell-"+e))&&(t?a.classList.add("tegaki-layers-cell-s"):a.classList.remove("tegaki-layers-cell-s"))},updateToolSize:function(){var e=$T.id("tegaki-ctrlgrp-size");Tegaki.tool.useSize?(e.classList.remove("tegaki-hidden"),$T.id("tegaki-size-lbl").value=Tegaki.tool.size,$T.id("tegaki-size").value=Tegaki.tool.size):e.classList.add("tegaki-hidden")},updateToolAlpha:function(){var e,t=$T.id("tegaki-ctrlgrp-alpha");Tegaki.tool.useAlpha?(t.classList.remove("tegaki-hidden"),e=Math.round(100*Tegaki.tool.alpha),$T.id("tegaki-alpha-lbl").value=e,$T.id("tegaki-alpha").value=e):t.classList.add("tegaki-hidden")},updateToolFlow:function(){var e,t=$T.id("tegaki-ctrlgrp-flow");Tegaki.tool.useFlow?(t.classList.remove("tegaki-hidden"),e=Math.round(100*Tegaki.tool.flow),$T.id("tegaki-flow-lbl").value=e,$T.id("tegaki-flow").value=e):t.classList.add("tegaki-hidden")},updateToolDynamics:function(){var e,t;e=$T.id("tegaki-tool-mode-dynamics"),Tegaki.tool.usesDynamics()?(t=$T.id("tegaki-tool-mode-dynamics-size"),Tegaki.tool.useSizeDynamics?(Tegaki.tool.sizeDynamicsEnabled?t.classList.add("tegaki-sw-btn-a"):t.classList.remove("tegaki-sw-btn-a"),t.classList.remove("tegaki-hidden")):t.classList.add("tegaki-hidden"),t=$T.id("tegaki-tool-mode-dynamics-alpha"),Tegaki.tool.useAlphaDynamics?(Tegaki.tool.alphaDynamicsEnabled?t.classList.add("tegaki-sw-btn-a"):t.classList.remove("tegaki-sw-btn-a"),t.classList.remove("tegaki-hidden")):t.classList.add("tegaki-hidden"),t=$T.id("tegaki-tool-mode-dynamics-flow"),Tegaki.tool.useFlowDynamics?(Tegaki.tool.flowDynamicsEnabled?t.classList.add("tegaki-sw-btn-a"):t.classList.remove("tegaki-sw-btn-a"),t.classList.remove("tegaki-hidden")):t.classList.add("tegaki-hidden"),e.classList.remove("tegaki-hidden")):e.classList.add("tegaki-hidden")},updateToolShape:function(){var e,t,a,i,s;if(t=$T.id("tegaki-tool-mode-tip"),Tegaki.tool.tipList){for(s=Tegaki.tool.tipList,(a=$T.id("tegaki-tool-mode-tip-ctrl")).innerHTML="",e=0;e=Tegaki.zoomFactorList.length?$T.id("tegaki-zoomin-btn").classList.add("tegaki-disabled"):$T.id("tegaki-zoomin-btn").classList.remove("tegaki-disabled"),Tegaki.zoomLevel+Tegaki.zoomBaseLevel<=0?$T.id("tegaki-zoomout-btn").classList.add("tegaki-disabled"):$T.id("tegaki-zoomout-btn").classList.remove("tegaki-disabled")},updateColorPalette:function(){var e,t,a;a=Tegaki.colorPaletteId,t=$T.cls("tegaki-color-grid",$T.id("tegaki-color-grids"));for(e of t)+e.getAttribute("data-id")===a?e.classList.remove("tegaki-hidden"):e.classList.add("tegaki-hidden");e=$T.id("tegaki-palette-prev-btn"),0===a?e.classList.add("tegaki-disabled"):e.classList.remove("tegaki-disabled"),e=$T.id("tegaki-palette-next-btn"),a===TegakiColorPalettes.length-1?e.classList.add("tegaki-disabled"):e.classList.remove("tegaki-disabled")},updateReplayTime:function(e){var t,a,i=Tegaki.replayViewer;(t=i.getCurrentPos())>(a=i.getDuration())&&(t=a),$T.id("tegaki-replay-now-lbl").textContent=$T.msToHms(t),e&&($T.id("tegaki-replay-end-lbl").textContent=$T.msToHms(a))},updateReplayControls:function(){TegakiUI.updateReplayGapless(),TegakiUI.updateReplayPlayPause(),TegakiUI.updateReplaySpeed()},updateReplayGapless:function(){var e,t=Tegaki.replayViewer;e=$T.id("tegaki-replay-gapless-cb"),t.gapless?e.classList.add("tegaki-ui-cb-a"):e.classList.remove("tegaki-ui-cb-a")},updateReplayPlayPause:function(){var e,t=Tegaki.replayViewer;e=$T.id("tegaki-replay-play-btn"),t.playing?(e.classList.remove("tegaki-play"),e.classList.add("tegaki-pause"),e.setAttribute("title",TegakiStrings.pause)):(e.classList.add("tegaki-play"),e.classList.remove("tegaki-pause"),e.setAttribute("title",TegakiStrings.play),t.getCurrentPos()>8&255},readUint:function(e,t){return 16777216*e[t+3]+(e[t+2]<<16|e[t+1]<<8|e[t])},writeUint:function(e,t,a){e[t]=255&a,e[t+1]=a>>8&255,e[t+2]=a>>16&255,e[t+3]=a>>24&255},readASCII:function(e,t,a){for(var i="",s=0;s>6,e[t+s+1]=128|n>>0&63,s+=2;else if(0==(4294901760&n))e[t+s]=224|n>>12,e[t+s+1]=128|n>>6&63,e[t+s+2]=128|n>>0&63,s+=3;else{if(0!=(4292870144&n))throw"e";e[t+s]=240|n>>18,e[t+s+1]=128|n>>12&63,e[t+s+2]=128|n>>6&63,e[t+s+3]=128|n>>0&63,s+=4}}return s},sizeUTF8:function(e){for(var t=e.length,a=0,i=0;i>>3}var h=r.lits,p=r.strt,k=r.prev,T=0,u=0,y=0,f=0,v=0,m=0;c>2&&(p[m=UZIP.F._hash(e,0)]=0);for(l=0;l14e3||u>26697)&&c-l>100&&(d>>16,w=65535&L;if(0!=L){w=65535&L;var S=n(C=L>>>16,r.of0);r.lhst[257+S]++;var I=n(w,r.df0);r.dhst[I]++,f+=r.exb[S]+r.dxb[I],h[T]=C<<23|l-d,h[T+1]=w<<16|S<<8|I,T+=2,d=l+C}else r.lhst[e[l]]++;u++}}for(y==l&&0!=e.length||(d>>3},UZIP.F._bestMatch=function(e,t,a,i,s,r){var n=32767&t,o=a[n],l=n-o+32768&32767;if(o==n||i!=UZIP.F._hash(e,t-l))return 0;for(var g=0,d=0,c=Math.min(32767,t);l<=c&&0!=--r&&o!=n;){if(0==g||e[t+g]==e[t+g-l]){var h=UZIP.F._howLong(e,t,l);if(h>g){if(d=l,(g=h)>=s)break;l+2p&&(p=u,o=T)}}}l+=(n=o)-(o=a[n])+32768&32767}return g<<16|d},UZIP.F._howLong=function(e,t,a){if(e[t]!=e[t-a]||e[t+1]!=e[t+1-a]||e[t+2]!=e[t+2-a])return 0;var i=t,s=Math.min(e.length,t+258);for(t+=3;t>>23,z=A+(8388607&E);A>16,M=x>>8&255,R=255&x;m(o,l=UZIP.F._writeLit(257+M,I,o,l),$-f.of0[M]),l+=f.exb[M],v(o,l=UZIP.F._writeLit(R,U,o,l),F-f.df0[R]),l+=f.dxb[R],A+=$}}l=UZIP.F._writeLit(256,I,o,l)}return l},UZIP.F._copyExact=function(e,t,a,i,s){var r=s>>>3;return i[r]=a,i[r+1]=a>>>8,i[r+2]=255-i[r],i[r+3]=255-i[r+1],r+=4,i.set(new Uint8Array(e.buffer,t,a),r),s+(a+4<<3)},UZIP.F.getTrees=function(){for(var e=UZIP.F.U,t=UZIP.F._hufTree(e.lhst,e.ltree,15),a=UZIP.F._hufTree(e.dhst,e.dtree,15),i=[],s=UZIP.F._lenCodes(e.ltree,i),r=[],n=UZIP.F._lenCodes(e.dtree,r),o=0;o4&&0==e.itree[1+(e.ordr[g-1]<<1)];)g--;return[t,a,l,s,n,g,i,r]},UZIP.F.getSecond=function(e){for(var t=[],a=0;a>1)+",");return t},UZIP.F.contSize=function(e,t){for(var a=0,i=0;i15&&(UZIP.F._putsE(a,i,n,o),i+=o)}return i},UZIP.F._lenCodes=function(e,t){for(var a=e.length;2!=a&&0==e[a-1];)a-=2;for(var i=0;i>>1,138))<11?t.push(17,g-3):t.push(18,g-11),i+=2*g-2}else if(s==o&&r==s&&n==s){for(l=i+5;l+2>>1,6);t.push(16,g-3),i+=2*g-2}else t.push(s,0)}return a>>>1},UZIP.F._hufTree=function(e,t,a){var i=[],s=e.length,r=t.length,n=0;for(n=0;na&&(UZIP.F.restrictDepth(l,a,T),T=a),n=0;nt;i++){var n=e[i].d;e[i].d=t,r+=s-(1<>>=a-t;r>0;){(n=e[i].d)=0;i--)e[i].d==t&&r<0&&(e[i].d--,r++)},UZIP.F._goodIndex=function(e,t){var a=0;return t[16|a]<=e&&(a|=16),t[8|a]<=e&&(a|=8),t[4|a]<=e&&(a|=4),t[2|a]<=e&&(a|=2),t[1|a]<=e&&(a|=1),a},UZIP.F._writeLit=function(e,t,a,i){return UZIP.F._putsF(a,i,t[e<<1]),i+t[1+(e<<1)]},UZIP.F.inflate=function(e,t){var a=Uint8Array;if(3==e[0]&&0==e[1])return t||new a(0);var i=UZIP.F,s=i._bitsF,r=i._bitsE,n=i._decodeTiny,o=i.makeCodes,l=i.codes2map,g=i._get17,d=i.U,c=null==t;c&&(t=new a(e.length>>>2<<3));for(var h,p,k=0,T=0,u=0,y=0,f=0,v=0,m=0,b=0,L=0;0==k;)if(k=s(e,L,1),T=s(e,L+1,2),L+=3,0!=T){if(c&&(t=UZIP.F._check(t,b+(1<<17))),1==T&&(h=d.flmap,p=d.fdmap,v=511,m=31),2==T){u=r(e,L,5)+257,y=r(e,L+5,5)+1,f=r(e,L+10,4)+4;L+=14;for(var C=0;C<38;C+=2)d.itree[C]=0,d.itree[C+1]=0;var w=1;for(C=0;Cw&&(w=S)}L+=3*f,o(d.itree,w),l(d.itree,w,d.imap),h=d.lmap,p=d.dmap,L=n(d.imap,(1<>>4;if(A>>>8==0)t[b++]=A;else{if(256==A)break;var D=b+A-254;if(A>264){var E=d.ldef[A-257];D=b+(E>>>3)+r(e,L,7&E),L+=7&E}var $=p[g(e,L)&m];L+=15&$;var z=$>>>4,x=d.ddef[z],F=(x>>>4)+s(e,L,15&x);for(L+=15&x,c&&(t=UZIP.F._check(t,b+(1<<17)));b>>3),R=e[M-4]|e[M-3]<<8;c&&(t=UZIP.F._check(t,b+R)),t.set(new a(e.buffer,e.byteOffset+M,R),b),L=M+R<<3,b+=R}return t.length==b?t:t.slice(0,b)},UZIP.F._check=function(e,t){var a=e.length;if(t<=a)return e;var i=new Uint8Array(Math.max(a<<1,t));return i.set(e,0),i},UZIP.F._decodeTiny=function(e,t,a,i,s,r){for(var n=UZIP.F._bitsE,o=UZIP.F._get17,l=0;l>>4;if(d<=15)r[l]=d,l++;else{var c=0,h=0;16==d?(h=3+n(i,s,2),s+=2,c=r[l-1]):17==d?(h=3+n(i,s,3),s+=3):18==d&&(h=11+n(i,s,7),s+=7);for(var p=l+h;l>>1;rs&&(s=o),r++}for(;r>1,o=e[r+1],l=n<<4|o,g=t-o,d=e[r]<>>15-t]=l,d++}},UZIP.F.revCodes=function(e,t){for(var a=UZIP.F.U.rev15,i=15-t,s=0;s>>i}},UZIP.F._putsE=function(e,t,a){a<<=7&t;var i=t>>>3;e[i]|=a,e[i+1]|=a>>>8},UZIP.F._putsF=function(e,t,a){a<<=7&t;var i=t>>>3;e[i]|=a,e[i+1]|=a>>>8,e[i+2]|=a>>>16},UZIP.F._bitsE=function(e,t,a){return(e[t>>>3]|e[1+(t>>>3)]<<8)>>>(7&t)&(1<>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16)>>>(7&t)&(1<>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16)>>>(7&t)},UZIP.F._get25=function(e,t){return(e[t>>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16|e[3+(t>>>3)]<<24)>>>(7&t)},UZIP.F.U=function(){var e=Uint16Array,t=Uint32Array;return{next_code:new e(16),bl_count:new e(16),ordr:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],of0:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],exb:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],ldef:new e(32),df0:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],dxb:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],ddef:new t(32),flmap:new e(512),fltree:[],fdmap:new e(32),fdtree:[],lmap:new e(32768),ltree:[],ttree:[],dmap:new e(32768),dtree:[],imap:new e(512),itree:[],rev15:new e(32768),lhst:new t(286),dhst:new t(30),ihst:new t(19),lits:new t(15e3),strt:new e(65536),prev:new e(32768)}}(),function(){function e(e,t,a){for(;0!=t--;)e.push(0,a)}for(var t=UZIP.F.U,a=32768,i=0;i>>1|(1431655765&s)<<1))>>>2|(858993459&s)<<2))>>>4|(252645135&s)<<4))>>>8|(16711935&s)<<8,t.rev15[i]=(s>>>16|s<<16)>>>17}for(i=0;i<32;i++)t.ldef[i]=t.of0[i]<<3|t.exb[i],t.ddef[i]=t.df0[i]<<4|t.dxb[i];e(t.fltree,144,8),e(t.fltree,112,9),e(t.fltree,24,7),e(t.fltree,8,8),UZIP.F.makeCodes(t.fltree,9),UZIP.F.codes2map(t.fltree,9,t.flmap),UZIP.F.revCodes(t.fltree,9),e(t.fdtree,32,5),UZIP.F.makeCodes(t.fdtree,5),UZIP.F.codes2map(t.fdtree,5,t.fdmap),UZIP.F.revCodes(t.fdtree,5),e(t.itree,19,0),e(t.ltree,286,0),e(t.dtree,30,0),e(t.ttree,320,0)}(); \ No newline at end of file diff --git a/json-test.php b/json-test.php new file mode 100644 index 0000000..381f85d --- /dev/null +++ b/json-test.php @@ -0,0 +1,725 @@ + $val ) { + $carr[] = $key; + } + + sort( $carr, SORT_NATURAL ); + + $count = count( $carr ) + 1; + $replycount = $op['replycount']; + + $build = ''; + $json = array(); + $extra = array(); + $imagecount = $op['imgreplycount']; + $tail_images = 0; + + $extra['replies'] = $replycount; + $extra['images'] = $imagecount; + + // Not inside a thread + if ($replies !== false) { + if ($replycount - $replies > 0) { + for ($i = $replycount - $replies; $i < $replycount; $i++) { + if ($log[$carr[$i]]['fsize'] && !$log[$carr[$i]]['filedeleted']) { + $tail_images++; + } + } + + $extra['omitted_posts'] = $replycount - $replies; + $extra['omitted_images'] = $imagecount - $tail_images; + } + } + // Inside a thread + else if (SHOW_THREAD_UNIQUES) { + if ($thread_unique_ips) { + $unique_ips = (int)$thread_unique_ips; + } + else { + $unique_ips = get_unique_ip_count($threadid); + } + + if ($unique_ips) { + $extra['unique_ips'] = $unique_ips; + } + } + + //if( $replycount >= MAX_RES && !$op['permaage'] ) $extra['bumplimit'] = 1; + //if( $imagecount >= MAX_IMGRES && !$op['sticky'] ) $extra['imagelimit'] = 1; + + $i = 1; + + if ($op['semantic_url'] !== '') { + $extra['semantic_url'] = $op['semantic_url']; + } + + if ($replies !== false) { + $i = $count - $replies; + + if ($i < 1) { + $i = 1; + } + } + + if ($for_catalogue) { + $json = generate_post_json( $op, $threadid, $extra ); // we do op before replies + if ($replycount > 0) { + $json['last_replies'] = array(); + for( ; $i < $count; $i++ ) { + if( isset( $log[$carr[$i - 1]] ) ) { + $var = $log[$carr[$i - 1]]; + $json['last_replies'][] = generate_post_json( $var, $threadid ); + } + } + if (META_BOARD) { + $capcode_replies = generate_capcode_replies($op['children']); + + if ($capcode_replies) { + $json['capcode_replies'] = $capcode_replies; + } + } + } + + $json['last_modified'] = (int)$log[$threadid]['last_modified']; + + return $json; + } + else if ($tail) { + $json[] = generate_op_tail_json($op, $extra); + + $lim = $count - $tail; + + $tail_id = 0; + + for (; $i < $count; $i++ ) { + if (isset($log[$carr[$i - 1]])) { + $var = $log[$carr[$i - 1]]; + if ($i < $lim) { + $tail_id = (int)$var['no']; + } + else { + $json[] = generate_post_json($var, $threadid); + } + } + } + + $json[0]['tail_size'] = $tail; + $json[0]['tail_id'] = $tail_id; + + $temp = array('posts' => $json); + } + else { + $json[] = generate_post_json( $op, $threadid, $extra ); // we do op before replies + + $tailSize = get_json_tail_size($threadid); + + if ($tailSize) { + $json[0]['tail_size'] = $tailSize; + } + + for( ; $i < $count; $i++ ) { + if( isset( $log[$carr[$i - 1]] ) ) { + $var = $log[$carr[$i - 1]]; + $json[] = generate_post_json( $var, $threadid ); + } + } + + if (META_BOARD) { + $capcode_replies = generate_capcode_replies($op['children']); + + if ($capcode_replies) { + $json[0]['capcode_replies'] = $capcode_replies; + } + } + + $temp = array('posts' => $json); + } + + if( $return ) return $temp; + + unset( $json ); + + if (!$tail) { + $filename = RES_DIR . $threadid . '.json'; + } + else { + $filename = RES_DIR . $threadid . '-tail.json'; + } + $page = json_encode($temp); + print_page( $filename, $page, 0, 0 ); + + return true; +} + +function generate_capcode_replies($replies) { + global $log; + + $capcode_replies = array( + 'admin' => array(), + 'developer' => array(), + 'mod' => array() + ); + + $has_capcode_replies = false; + + foreach ($replies as $no => $val) { + if (!isset($log[$no])) { + continue; + } + $json_post = $log[$no]; + if ($json_post['capcode'] === 'none') { + continue; + } + if ($json_post['capcode'] === 'admin_highlight') { + $json_capcode = 'admin'; + } + else { + $json_capcode = $json_post['capcode']; + } + $capcode_replies[$json_capcode][] = (int)$no; + $has_capcode_replies = true; + } + + if ($has_capcode_replies) { + $ret = array(); + foreach ($capcode_replies as $key => $value) { + if (empty($value)) { + continue; + } + $ret[$key] = $value; + } + return $ret; + } + else { + return null; + } +} + +function post_json_force_type( &$post ) +{ + static $post_intern = array( + 'no' => 'integer', + 'resto' => 'integer', + 'sticky' => 'integer', + 'closed' => 'integer', + 'archived' => 'integer', + 'now' => 'string', + 'time' => 'integer', + 'name' => 'string', + 'trip' => 'string', + 'id' => 'string', + 'capcode' => 'string', + 'country' => 'string', + 'country_name' => 'string', + 'sub' => 'string', + 'com' => 'string', + 'tim' => 'integer', + 'filename' => 'string', + 'ext' => 'string', + 'fsize' => 'integer', + 'md5' => 'string', + 'w' => 'integer', + 'h' => 'integer', + 'tn_w' => 'integer', + 'tn_h' => 'integer', + 'filedeleted' => 'integer', + 'spoiler' => 'integer', + 'custom_spoiler' => 'integer', + 'omitted_posts' => 'integer', + 'omitted_images' => 'integer', + 'replies' => 'integer', + 'images' => 'integer', + 'bumplimit' => 'integer', + 'imagelimit' => 'integer', + 'last_modified' => 'integer', + 'archived_on' => 'integer', + 'since4pass' => 'integer', + 'm_img' => 'integer' + ); + + + foreach( $post as $key => $val) { + if( isset( $post_intern[$key] ) ) { + settype( $post[$key], $post_intern[$key] ); + } + } +} + +function generate_op_tail_json($op, $extra) { + $ary = array(); + + $ary['no'] = (int)$op['no']; + + $ary['bumplimit'] = (int)$op['bumplimit']; + $ary['imagelimit'] = (int)$op['imagelimit']; + + if ($op['sticky']) { + $ary['sticky'] = 1; + if ($op['undead']) { + $ary['sticky_cap'] = STICKY_CAP; + } + } + + if ($op['closed']) { + $ary['closed'] = 1; + } + + if ($op['archived']) { + $ary['archived'] = 1; + } + + $ary['replies'] = (int)$extra['replies']; + $ary['images'] = (int)$extra['images']; + + if (isset($extra['unique_ips'])) { + $ary['unique_ips'] = (int)$extra['unique_ips']; + } + + if (SPOILERS) { + $ary['custom_spoiler'] = (int)SPOILER_NUM; + } + + return $ary; +} + +function generate_post_json( $var, $threadid, $extra = array(), $banskip = false ) +{ + $COUNTRY_FLAG_ARR = array( + 'sp', + 'int', + ); + + $FORCED_ANON_ARR = array( + 'b', + 'soc' + ); + + $META_BOARD_ARR = array( + 'q' + ); + + if (UPLOAD_BOARD) { + $FLASH_TAGS = array( + 0 => 'Hentai', + 1 => 'Japanese', + 2 => 'Anime', + 3 => 'Game', + 4 => 'Other', + 5 => 'Loop', + 6 => 'Porn', + ); + } + + $SHOW_COUNTRY_FLAGS = $banskip ? in_array( $var['board'], $COUNTRY_FLAG_ARR ) : SHOW_COUNTRY_FLAGS; + $ENABLE_BOARD_FLAGS = $banskip ? false : ENABLE_BOARD_FLAGS; + $META_BOARD = $banskip ? in_array( $var['board'], $META_BOARD_ARR ) : META_BOARD; + $FORCED_ANON = $banskip ? in_array( $var['board'], $FORCED_ANON_ARR ) : FORCED_ANON; + + if ($ENABLE_BOARD_FLAGS) { + $board_flags_array = get_board_flags_array(); + } + + $country = $var['country']; + + if ($ENABLE_BOARD_FLAGS && isset($board_flags_array[$var['board_flag']])) { + $board_flag = $var['board_flag']; + } + else { + $board_flag = ''; + } + + if( $banskip && $var['ext'] ) { + $salt = file_get_contents( '/www/keys/legacy.salt' ); + $no = $var['no']; + + $hash = sha1( $var['board'] . $no . $salt ); + $var['thumb'] = $hash; + } + + $intern_host = $var['host']; + + $unset = array( + 'permasage', + 'host', + 'pwd', + 'children', + 'imgreplycount', + 'replycount', + 'last_modified', + 'root', + '4pass_id' + ); + + if ($banskip) { + $unset[] = 'protected'; + $unset[] = 'ua'; + } + + $nunset = $unset; + + $var['tim'] = (int)$var['tim']; + + if( $var['filedeleted'] == 1 || !$var['ext'] ) { + $arr = array( + 'tim', + 'w', + 'h', + 'tn_w', + 'tn_h', + 'filename', + 'ext', + 'md5', + 'fsize', + 'tmd5' + ); + + foreach( $arr as $key ) { + unset( $var[$key] ); + } + } + else { + // FIXME + $var['filename'] = mb_convert_encoding($var['filename'], 'UTF-8', 'UTF-8'); + } + + // FIXME + $var['com'] = mb_convert_encoding($var['com'], 'UTF-8', 'UTF-8'); + + if( !$var['filedeleted'] ) $nunset[] = 'filedeleted'; + + // trim it up + foreach( $nunset as $key ) { + unset( $var[$key] ); + } + + $is_archived = $var['archived']; + + if( $var['resto'] ) { + unset( $var['sticky'] ); + unset( $var['closed'] ); + unset( $var['archived'] ); + } + else { + if (!$var['archived']) { + unset($var['archived']); + unset($var['archived_on']); + } + + if (!$var['closed']) { + unset($var['closed']); + } + + if (!$var['sticky']) { + unset($var['sticky']); + } + else { + if ($var['undead']) { + $var['sticky_cap'] = (int)STICKY_CAP; + } + unset( $var['bumplimit'], $var['imagelimit'] ); + } + + if ($var['permaage']) { + unset( $var['bumplimit'] ); + } + } + + if (!$var['m_img']) { + unset($var['m_img']); + } + + if (!$var['since4pass']) { + unset($var['since4pass']); + } + // April 2024 + else if ($var['since4pass'] >= 10000) { + $_stock = april_2024_get_stock_from_s4p($var['since4pass']); + + if ($_stock) { + $var['xa24'] = $_stock; + } + + unset($var['since4pass']); + } + + unset($var['permaage'], $var['undead']); + + // clean up names + if( strpos( $var['name'], '
    ' ) !== false ) { + $name = explode( ' ', $var['name'] ); + $var['name'] = $name[0]; + $var['trip'] = $name[1]; + + if( $var['trip'] && !$var['name'] ) unset( $var['name'] ); + } + + if( !$banskip && SPOILERS && !$var['resto'] ) $var['custom_spoiler'] = (int)SPOILER_NUM; + + $var['spoiler'] = 0; + if( strpos( $var['sub'], 'SPOILER<>' ) === 0 ) { + $var['sub'] = substr( $var['sub'], 9 ); + if( !$var['sub'] ) $var['sub'] = ''; + $var['spoiler'] = 1; + } else { + unset( $var['spoiler'] ); + } + + if( $var['sub'] && !$var['resto'] && UPLOAD_BOARD ) { + if( preg_match( '/^(\d+)\|/', $var['sub'], $tag_matches ) ) { + $var['tag'] = $FLASH_TAGS[(int)$tag_matches[1]]; + $var['sub'] = preg_replace( '/^(\d+)\|/', '', $var['sub'] ); + } + } + + if ( !$banskip ) { + if( !$var['id'] || $is_archived ) { + unset( $var['id'] ); + } elseif( $var['id'] && $var['no'] == $threadid && $var['capcode'] === 'none') { + $var['id'] = generate_uid( $var['no'], $var['time'], $intern_host ); + } + } + + if ($var['capcode'] == 'none') { + if ($ENABLE_BOARD_FLAGS && $board_flag) { + unset($var['country']); + $var['flag_name'] = board_flag_code_to_name($board_flag); + } + else if ($SHOW_COUNTRY_FLAGS) { + unset( $var['board_flag'] ); + $var['country_name'] = country_code_to_name($country); + } + else { + unset($var['country']); + unset($var['country_name']); + unset($var['board_flag']); + } + } + else { + unset($var['country']); + unset($var['country_name']); + unset($var['board_flag']); + } + + if( ( $FORCED_ANON || $META_BOARD ) && ( $var['capcode'] != 'admin' && $var['capcode'] != 'admin_hl' ) ) { + unset( $var['trip'] ); + $var['name'] = 'Anonymous'; + } + + if( $var['capcode'] == 'none' ) unset( $var['capcode'] ); + if( !$banskip ) $var['com'] = auto_link( $var['com'], $threadid ); + if( !$banskip && isset( $var['md5'] ) ) $var['md5'] = base64_encode( pack( 'H*', $var['md5'] ) ); + + if( $var['com'] == '' ) unset( $var['com'] ); + if(isset($var['email'])) unset( $var['email'] ); + if( $var['sub'] == '' ) unset( $var['sub'] ); + + if( !empty( $extra ) ) { + foreach( $extra as $key => $val ) { + $var[$key] = $val; + } + } + + post_json_force_type( $var ); + + /// XXX: CHANGE TO TRIM WHITESPACE + return $var; +} + +function generate_index_json( $return = false ) +{ + + global $log, $index_rbl; + log_cache( 0, 0 ); // generate all threads + + $threads = $log['THREADS']; + + $threadcount = count( $threads ); + + // figure out how many replies to print + if (defined('REPLIES_SHOWN')) { + $replies_shown = REPLIES_SHOWN; + } + else { + $replies_shown = 5; + } + + // Loop through every page + for( $page = 0; $page < $threadcount; $page += DEF_PAGES ) { + $file_page_num = $page / DEF_PAGES + 1; + + if (PAGE_MAX && $file_page_num > PAGE_MAX) { + break; + } + + $thread = $page; + $json = array(); + + if( floor( $page / DEF_PAGES ) > $index_rbl ) return; + + for( $i = $thread; $i < $thread + DEF_PAGES; $i++ ) { + list( $_unused, $threadid ) = each( $threads ); + + if( !$threadid ) break; + + if ($log[$threadid]['sticky'] == 1) { + $replies = min(1, $replies_shown); + } + else { + $replies = $replies_shown; + } + + $json[] = generate_thread_json( $threadid, true, $replies ); + } + + if( empty( $json ) ) return true; // we've reached the point of no return + + $temp = json_encode( array('threads' => $json) ); + + $filename = INDEX_DIR . ($page / DEF_PAGES + 1) . '.json'; + print_page( $filename, $temp, 0, 0 ); + } + + return true; +} + +// ↓ STICK IT TO THE MAN +function generate_board_catalogue() +{ + //$json = generate_index_json( true ); + + global $log, $index_rbl; + log_cache( 0, 0 ); // generate all threads + + $threads = $log['THREADS']; + $threadcount = count( $threads ); + $curpage = 0; + + $json = array(); + + if (defined('REPLIES_SHOWN')) { + $replies_shown = REPLIES_SHOWN; + } + else { + $replies_shown = 5; + } + + for( $page = 0; $page < $threadcount; $page += DEF_PAGES ) { + // Loop through each page... + + $thispage = $page; + + for( $i = $thispage; $i < $thispage + DEF_PAGES; $i++ ) { + list( $_unused, $threadid ) = each( $threads ); + + if( !$threadid ) break; + + if ($log[$threadid]['sticky'] == 1) { + $replies = min(1, $replies_shown); + } + else { + $replies = $replies_shown; + } + + $json[$curpage][] = generate_thread_json( $threadid, false, $replies, true ); + + } + + $curpage++; + } + + $build = array(); + + foreach( $json as $page => $threads ) { + $thread = array( + 'page' => $page + 1, + 'threads' => $threads + ); + $build[] = $thread; + } + + $page = json_encode($build); + print_page( INDEX_DIR . 'catalog.json', $page, 0, 0 ); +} + +function generate_board_threads_json() +{ + global $log; + log_cache( 0, 0 ); + + $threads = $log['THREADS']; + $threadcount = count( $threads ); + $curpage = 0; + $json = array(); + + for( $page = 0; $page < $threadcount; $page += DEF_PAGES ) { + $thispage = $page; + + for( $i = $thispage; $i < $thispage + DEF_PAGES; $i++ ) { + list( $_unused, $threadid ) = each( $threads ); + + if( !$threadid ) break; + + $arr = array( + 'no' => $threadid, + 'last_modified' => $log[$threadid]['last_modified'], + 'replies' => $log[$threadid]['replycount'] + ); + + post_json_force_type($arr); + + $json[$curpage][] = $arr; + + } + + $curpage++; + } + + foreach( $json as $page => $threads ) { + + $build[] = array( + 'page' => $page + 1, + 'threads' => $threads + ); + } + + $page = json_encode( $build ); + print_page( INDEX_DIR . 'threads.json', $page, 0, 0 ); +} + +function generate_board_archived_json() { + $query = "SELECT no FROM `" . BOARD_DIR . "` WHERE archived = 1 AND resto = 0 ORDER BY no ASC"; + + $res = mysql_board_call($query); + + if (!$res) { + return false; + } + + $threads = array(); + + while ($tid = mysql_fetch_row($res)[0]) { + $threads[] = $tid; + } + + $page = '[' . implode(',', $threads) . ']'; + + print_page(INDEX_DIR . 'archive.json', $page, 0, 0); +} diff --git a/json.php b/json.php new file mode 100644 index 0000000..61b9612 --- /dev/null +++ b/json.php @@ -0,0 +1,719 @@ + $val ) { + $carr[] = $key; + } + + sort( $carr, SORT_NATURAL ); + + $count = count( $carr ) + 1; + $replycount = $op['replycount']; + + $build = ''; + $json = array(); + $extra = array(); + $imagecount = $op['imgreplycount']; + $tail_images = 0; + + $extra['replies'] = $replycount; + $extra['images'] = $imagecount; + + // Not inside a thread + if ($replies !== false) { + if ($replycount - $replies > 0) { + for ($i = $replycount - $replies; $i < $replycount; $i++) { + if ($log[$carr[$i]]['fsize'] && !$log[$carr[$i]]['filedeleted']) { + $tail_images++; + } + } + + $extra['omitted_posts'] = $replycount - $replies; + $extra['omitted_images'] = $imagecount - $tail_images; + } + } + // Inside a thread + else if (SHOW_THREAD_UNIQUES) { + if ($thread_unique_ips) { + $unique_ips = (int)$thread_unique_ips; + } + else { + $unique_ips = get_unique_ip_count($threadid); + } + + if ($unique_ips) { + $extra['unique_ips'] = $unique_ips; + } + } + + //if( $replycount >= MAX_RES && !$op['permaage'] ) $extra['bumplimit'] = 1; + //if( $imagecount >= MAX_IMGRES && !$op['sticky'] ) $extra['imagelimit'] = 1; + + $i = 1; + + if ($op['semantic_url'] !== '') { + $extra['semantic_url'] = $op['semantic_url']; + } + + if ($replies !== false) { + $i = $count - $replies; + + if ($i < 1) { + $i = 1; + } + } + + if ($for_catalogue) { + $json = generate_post_json( $op, $threadid, $extra ); // we do op before replies + if ($replycount > 0) { + $json['last_replies'] = array(); + for( ; $i < $count; $i++ ) { + if( isset( $log[$carr[$i - 1]] ) ) { + $var = $log[$carr[$i - 1]]; + $json['last_replies'][] = generate_post_json( $var, $threadid ); + } + } + if (META_BOARD) { + $capcode_replies = generate_capcode_replies($op['children']); + + if ($capcode_replies) { + $json['capcode_replies'] = $capcode_replies; + } + } + } + + $json['last_modified'] = (int)$log[$threadid]['last_modified']; + + return $json; + } + else if ($tail) { + $json[] = generate_op_tail_json($op, $extra); + + $lim = $count - $tail; + + $tail_id = 0; + + for (; $i < $count; $i++ ) { + if (isset($log[$carr[$i - 1]])) { + $var = $log[$carr[$i - 1]]; + if ($i < $lim) { + $tail_id = (int)$var['no']; + } + else { + $json[] = generate_post_json($var, $threadid); + } + } + } + + $json[0]['tail_size'] = $tail; + $json[0]['tail_id'] = $tail_id; + + $temp = array('posts' => $json); + } + else { + $json[] = generate_post_json( $op, $threadid, $extra ); // we do op before replies + + $tailSize = get_json_tail_size($threadid); + + if ($tailSize) { + $json[0]['tail_size'] = $tailSize; + } + + for( ; $i < $count; $i++ ) { + if( isset( $log[$carr[$i - 1]] ) ) { + $var = $log[$carr[$i - 1]]; + $json[] = generate_post_json( $var, $threadid ); + } + } + + if (META_BOARD) { + $capcode_replies = generate_capcode_replies($op['children']); + + if ($capcode_replies) { + $json[0]['capcode_replies'] = $capcode_replies; + } + } + + $temp = array('posts' => $json); + } + + if( $return ) return $temp; + + unset( $json ); + + if (!$tail) { + $filename = RES_DIR . $threadid . '.json'; + } + else { + $filename = RES_DIR . $threadid . '-tail.json'; + } + $page = json_encode($temp); + print_page( $filename, $page, 0, 0 ); + + return true; +} + +function generate_capcode_replies($replies) { + global $log; + + $capcode_replies = array( + 'admin' => array(), + 'developer' => array(), + 'mod' => array() + ); + + $has_capcode_replies = false; + + foreach ($replies as $no => $val) { + if (!isset($log[$no])) { + continue; + } + $json_post = $log[$no]; + if ($json_post['capcode'] === 'none') { + continue; + } + if ($json_post['capcode'] === 'admin_highlight') { + $json_capcode = 'admin'; + } + else { + $json_capcode = $json_post['capcode']; + } + $capcode_replies[$json_capcode][] = (int)$no; + $has_capcode_replies = true; + } + + if ($has_capcode_replies) { + $ret = array(); + foreach ($capcode_replies as $key => $value) { + if (empty($value)) { + continue; + } + $ret[$key] = $value; + } + return $ret; + } + else { + return null; + } +} + +function post_json_force_type( &$post ) +{ + static $post_intern = array( + 'no' => 'integer', + 'resto' => 'integer', + 'sticky' => 'integer', + 'closed' => 'integer', + 'archived' => 'integer', + 'now' => 'string', + 'time' => 'integer', + 'name' => 'string', + 'trip' => 'string', + 'id' => 'string', + 'capcode' => 'string', + 'country' => 'string', + 'country_name' => 'string', + 'sub' => 'string', + 'com' => 'string', + 'tim' => 'integer', + 'filename' => 'string', + 'ext' => 'string', + 'fsize' => 'integer', + 'md5' => 'string', + 'w' => 'integer', + 'h' => 'integer', + 'tn_w' => 'integer', + 'tn_h' => 'integer', + 'filedeleted' => 'integer', + 'spoiler' => 'integer', + 'custom_spoiler' => 'integer', + 'omitted_posts' => 'integer', + 'omitted_images' => 'integer', + 'replies' => 'integer', + 'images' => 'integer', + 'bumplimit' => 'integer', + 'imagelimit' => 'integer', + 'last_modified' => 'integer', + 'archived_on' => 'integer', + 'since4pass' => 'integer', + 'm_img' => 'integer' + ); + + + foreach( $post as $key => $val) { + if( isset( $post_intern[$key] ) ) { + settype( $post[$key], $post_intern[$key] ); + } + } +} + +function generate_op_tail_json($op, $extra) { + $ary = array(); + + $ary['no'] = (int)$op['no']; + + $ary['bumplimit'] = (int)$op['bumplimit']; + $ary['imagelimit'] = (int)$op['imagelimit']; + + if ($op['sticky']) { + $ary['sticky'] = 1; + if ($op['undead']) { + $ary['sticky_cap'] = STICKY_CAP; + } + } + + if ($op['closed']) { + $ary['closed'] = 1; + } + + if ($op['archived']) { + $ary['archived'] = 1; + } + + $ary['replies'] = (int)$extra['replies']; + $ary['images'] = (int)$extra['images']; + + if (isset($extra['unique_ips'])) { + $ary['unique_ips'] = (int)$extra['unique_ips']; + } + + if (SPOILERS) { + $ary['custom_spoiler'] = (int)SPOILER_NUM; + } + + return $ary; +} + +function generate_post_json( $var, $threadid, $extra = array(), $banskip = false ) +{ + $COUNTRY_FLAG_ARR = array( + 'sp', + 'int', + ); + + $FORCED_ANON_ARR = array( + 'b', + 'soc' + ); + + $META_BOARD_ARR = array( + 'q' + ); + + if (UPLOAD_BOARD) { + $FLASH_TAGS = array( + 0 => 'Hentai', + 1 => 'Japanese', + 2 => 'Anime', + 3 => 'Game', + 4 => 'Other', + 5 => 'Loop', + 6 => 'Porn', + ); + } + + $SHOW_COUNTRY_FLAGS = $banskip ? in_array( $var['board'], $COUNTRY_FLAG_ARR ) : SHOW_COUNTRY_FLAGS; + $ENABLE_BOARD_FLAGS = $banskip ? false : ENABLE_BOARD_FLAGS; + $META_BOARD = $banskip ? in_array( $var['board'], $META_BOARD_ARR ) : META_BOARD; + $FORCED_ANON = $banskip ? in_array( $var['board'], $FORCED_ANON_ARR ) : FORCED_ANON; + + if ($ENABLE_BOARD_FLAGS) { + $board_flags_array = get_board_flags_array(); + } + + $country = $var['country']; + + if ($ENABLE_BOARD_FLAGS && isset($board_flags_array[$var['board_flag']])) { + $board_flag = $var['board_flag']; + } + else { + $board_flag = ''; + } + + if( $banskip && $var['ext'] ) { + $salt = file_get_contents( '/www/keys/legacy.salt' ); + $no = $var['no']; + + $hash = sha1( $var['board'] . $no . $salt ); + $var['thumb'] = $hash; + } + + $intern_host = $var['host']; + + $unset = array( + 'permasage', + 'host', + 'pwd', + 'children', + 'imgreplycount', + 'replycount', + 'last_modified', + 'root', + '4pass_id' + ); + + if ($banskip) { + $unset[] = 'protected'; + $unset[] = 'ua'; + } + + $nunset = $unset; + + $var['tim'] = (int)$var['tim']; + + if( $var['filedeleted'] == 1 || !$var['ext'] ) { + $arr = array( + 'tim', + 'w', + 'h', + 'tn_w', + 'tn_h', + 'filename', + 'ext', + 'md5', + 'fsize', + 'tmd5' + ); + + foreach( $arr as $key ) { + unset( $var[$key] ); + } + } + else { + // FIXME + $var['filename'] = mb_convert_encoding($var['filename'], 'UTF-8', 'UTF-8'); + } + + // FIXME + $var['com'] = mb_convert_encoding($var['com'], 'UTF-8', 'UTF-8'); + + if( !$var['filedeleted'] ) $nunset[] = 'filedeleted'; + + // trim it up + foreach( $nunset as $key ) { + unset( $var[$key] ); + } + + $is_archived = $var['archived']; + + if( $var['resto'] ) { + unset( $var['sticky'] ); + unset( $var['closed'] ); + unset( $var['archived'] ); + } + else { + if (!$var['archived']) { + unset($var['archived']); + unset($var['archived_on']); + } + + if (!$var['closed']) { + unset($var['closed']); + } + + if (!$var['sticky']) { + unset($var['sticky']); + } + else { + if ($var['undead']) { + $var['sticky_cap'] = (int)STICKY_CAP; + } + unset( $var['bumplimit'], $var['imagelimit'] ); + } + + if ($var['permaage']) { + unset( $var['bumplimit'] ); + } + } + + if (!$var['m_img']) { + unset($var['m_img']); + } + + if (!$var['since4pass']) { + unset($var['since4pass']); + } + // April 2024 + else if ($var['since4pass'] >= 10000) { + unset($var['since4pass']); + } + + unset($var['permaage'], $var['undead']); + + // clean up names + if( strpos( $var['name'], ' ' ) !== false ) { + $name = explode( ' ', $var['name'] ); + $var['name'] = $name[0]; + $var['trip'] = $name[1]; + + if( $var['trip'] && !$var['name'] ) unset( $var['name'] ); + } + + if( !$banskip && SPOILERS && !$var['resto'] ) $var['custom_spoiler'] = (int)SPOILER_NUM; + + $var['spoiler'] = 0; + if( strpos( $var['sub'], 'SPOILER<>' ) === 0 ) { + $var['sub'] = substr( $var['sub'], 9 ); + if( !$var['sub'] ) $var['sub'] = ''; + $var['spoiler'] = 1; + } else { + unset( $var['spoiler'] ); + } + + if( $var['sub'] && !$var['resto'] && UPLOAD_BOARD ) { + if( preg_match( '/^(\d+)\|/', $var['sub'], $tag_matches ) ) { + $var['tag'] = $FLASH_TAGS[(int)$tag_matches[1]]; + $var['sub'] = preg_replace( '/^(\d+)\|/', '', $var['sub'] ); + } + } + + if ( !$banskip ) { + if( !$var['id'] || $is_archived ) { + unset( $var['id'] ); + } elseif( $var['id'] && $var['no'] == $threadid && $var['capcode'] === 'none') { + $var['id'] = generate_uid( $var['no'], $var['time'], $intern_host ); + } + } + + if ($var['capcode'] == 'none') { + if ($ENABLE_BOARD_FLAGS && $board_flag) { + unset($var['country']); + $var['flag_name'] = board_flag_code_to_name($board_flag); + } + else if ($SHOW_COUNTRY_FLAGS) { + unset( $var['board_flag'] ); + $var['country_name'] = country_code_to_name($country); + } + else { + unset($var['country']); + unset($var['country_name']); + unset($var['board_flag']); + } + } + else { + unset($var['country']); + unset($var['country_name']); + unset($var['board_flag']); + } + + if( ( $FORCED_ANON || $META_BOARD ) && ( $var['capcode'] != 'admin' && $var['capcode'] != 'admin_hl' ) ) { + unset( $var['trip'] ); + $var['name'] = 'Anonymous'; + } + + if( $var['capcode'] == 'none' ) unset( $var['capcode'] ); + if( !$banskip ) $var['com'] = auto_link( $var['com'], $threadid ); + if( !$banskip && isset( $var['md5'] ) ) $var['md5'] = base64_encode( pack( 'H*', $var['md5'] ) ); + + if( $var['com'] == '' ) unset( $var['com'] ); + if(isset($var['email'])) unset( $var['email'] ); + if( $var['sub'] == '' ) unset( $var['sub'] ); + + if( !empty( $extra ) ) { + foreach( $extra as $key => $val ) { + $var[$key] = $val; + } + } + + post_json_force_type( $var ); + + /// XXX: CHANGE TO TRIM WHITESPACE + return $var; +} + +function generate_index_json( $return = false ) +{ + + global $log, $index_rbl; + log_cache( 0, 0 ); // generate all threads + + $threads = $log['THREADS']; + + $threadcount = count( $threads ); + + // figure out how many replies to print + if (defined('REPLIES_SHOWN')) { + $replies_shown = REPLIES_SHOWN; + } + else { + $replies_shown = 5; + } + + // Loop through every page + for( $page = 0; $page < $threadcount; $page += DEF_PAGES ) { + $file_page_num = $page / DEF_PAGES + 1; + + if (PAGE_MAX && $file_page_num > PAGE_MAX) { + break; + } + + $thread = $page; + $json = array(); + + if( floor( $page / DEF_PAGES ) > $index_rbl ) return; + + for( $i = $thread; $i < $thread + DEF_PAGES; $i++ ) { + list( $_unused, $threadid ) = each( $threads ); + + if( !$threadid ) break; + + if ($log[$threadid]['sticky'] == 1) { + $replies = min(1, $replies_shown); + } + else { + $replies = $replies_shown; + } + + $json[] = generate_thread_json( $threadid, true, $replies ); + } + + if( empty( $json ) ) return true; // we've reached the point of no return + + $temp = json_encode( array('threads' => $json) ); + + $filename = INDEX_DIR . ($page / DEF_PAGES + 1) . '.json'; + print_page( $filename, $temp, 0, 0 ); + } + + return true; +} + +// ↓ STICK IT TO THE MAN +function generate_board_catalogue() +{ + //$json = generate_index_json( true ); + + global $log, $index_rbl; + log_cache( 0, 0 ); // generate all threads + + $threads = $log['THREADS']; + $threadcount = count( $threads ); + $curpage = 0; + + $json = array(); + + if (defined('REPLIES_SHOWN')) { + $replies_shown = REPLIES_SHOWN; + } + else { + $replies_shown = 5; + } + + for( $page = 0; $page < $threadcount; $page += DEF_PAGES ) { + // Loop through each page... + + $thispage = $page; + + for( $i = $thispage; $i < $thispage + DEF_PAGES; $i++ ) { + list( $_unused, $threadid ) = each( $threads ); + + if( !$threadid ) break; + + if ($log[$threadid]['sticky'] == 1) { + $replies = min(1, $replies_shown); + } + else { + $replies = $replies_shown; + } + + $json[$curpage][] = generate_thread_json( $threadid, false, $replies, true ); + + } + + $curpage++; + } + + $build = array(); + + foreach( $json as $page => $threads ) { + $thread = array( + 'page' => $page + 1, + 'threads' => $threads + ); + $build[] = $thread; + } + + $page = json_encode($build); + print_page( INDEX_DIR . 'catalog.json', $page, 0, 0 ); +} + +function generate_board_threads_json() +{ + global $log; + log_cache( 0, 0 ); + + $threads = $log['THREADS']; + $threadcount = count( $threads ); + $curpage = 0; + $json = array(); + + for( $page = 0; $page < $threadcount; $page += DEF_PAGES ) { + $thispage = $page; + + for( $i = $thispage; $i < $thispage + DEF_PAGES; $i++ ) { + list( $_unused, $threadid ) = each( $threads ); + + if( !$threadid ) break; + + $arr = array( + 'no' => $threadid, + 'last_modified' => $log[$threadid]['last_modified'], + 'replies' => $log[$threadid]['replycount'] + ); + + post_json_force_type($arr); + + $json[$curpage][] = $arr; + + } + + $curpage++; + } + + foreach( $json as $page => $threads ) { + + $build[] = array( + 'page' => $page + 1, + 'threads' => $threads + ); + } + + $page = json_encode( $build ); + print_page( INDEX_DIR . 'threads.json', $page, 0, 0 ); +} + +function generate_board_archived_json() { + $query = "SELECT no FROM `" . BOARD_DIR . "` WHERE archived = 1 AND resto = 0 ORDER BY no ASC"; + + $res = mysql_board_call($query); + + if (!$res) { + return false; + } + + $threads = array(); + + while ($tid = mysql_fetch_row($res)[0]) { + $threads[] = $tid; + } + + $page = '[' . implode(',', $threads) . ']'; + + print_page(INDEX_DIR . 'archive.json', $page, 0, 0); +} diff --git a/latest.php b/latest.php new file mode 100644 index 0000000..a31e9c1 --- /dev/null +++ b/latest.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/lib/GoogleAuthenticator.php b/lib/GoogleAuthenticator.php new file mode 100644 index 0000000..5e70eed --- /dev/null +++ b/lib/GoogleAuthenticator.php @@ -0,0 +1,201 @@ +_getBase32LookupTable(); + unset($validChars[32]); + + $secret = ''; + for ($i = 0; $i < $secretLength; $i++) { + $secret .= $validChars[array_rand($validChars)]; + } + return $secret; + } + + /** + * Calculate the code, with given secret and point in time + * + * @param string $secret + * @param int|null $timeSlice + * @return string + */ + public function getCode($secret, $timeSlice = null) + { + if ($timeSlice === null) { + $timeSlice = floor(time() / 30); + } + + $secretkey = $this->_base32Decode($secret); + + // Pack time into binary string + $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); + // Hash it with users secret key + $hm = hash_hmac('SHA1', $time, $secretkey, true); + // Use last nipple of result as index/offset + $offset = ord(substr($hm, -1)) & 0x0F; + // grab 4 bytes of the result + $hashpart = substr($hm, $offset, 4); + + // Unpak binary value + $value = unpack('N', $hashpart); + $value = $value[1]; + // Only 32 bits + $value = $value & 0x7FFFFFFF; + + $modulo = pow(10, $this->_codeLength); + return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); + } + + /** + * Get QR-Code URL for image, from google charts + * + * @param string $name + * @param string $secret + * @return string + */ + public function getQRCodeGoogleUrl($name, $secret) { + $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.''); + return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.''; + } + + /** + * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now + * + * @param string $secret + * @param string $code + * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) + * @return bool + */ + public function verifyCode($secret, $code, $discrepancy = 1) + { + $currentTimeSlice = floor(time() / 30); + + for ($i = -$discrepancy; $i <= $discrepancy; $i++) { + $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); + if ($calculatedCode == $code ) { + return true; + } + } + + return false; + } + + /** + * Set the code length, should be >=6 + * + * @param int $length + * @return PHPGangsta_GoogleAuthenticator + */ + public function setCodeLength($length) + { + $this->_codeLength = $length; + return $this; + } + + /** + * Helper class to decode base32 + * + * @param $secret + * @return bool|string + */ + protected function _base32Decode($secret) + { + if (empty($secret)) return ''; + + $base32chars = $this->_getBase32LookupTable(); + $base32charsFlipped = array_flip($base32chars); + + $paddingCharCount = substr_count($secret, $base32chars[32]); + $allowedValues = array(6, 4, 3, 1, 0); + if (!in_array($paddingCharCount, $allowedValues)) return false; + for ($i = 0; $i < 4; $i++){ + if ($paddingCharCount == $allowedValues[$i] && + substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false; + } + $secret = str_replace('=','', $secret); + $secret = str_split($secret); + $binaryString = ""; + for ($i = 0; $i < count($secret); $i = $i+8) { + $x = ""; + if (!in_array($secret[$i], $base32chars)) return false; + for ($j = 0; $j < 8; $j++) { + $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); + } + $eightBits = str_split($x, 8); + for ($z = 0; $z < count($eightBits); $z++) { + $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:""; + } + } + return $binaryString; + } + + /** + * Helper class to encode base32 + * + * @param string $secret + * @param bool $padding + * @return string + */ + protected function _base32Encode($secret, $padding = true) + { + if (empty($secret)) return ''; + + $base32chars = $this->_getBase32LookupTable(); + + $secret = str_split($secret); + $binaryString = ""; + for ($i = 0; $i < count($secret); $i++) { + $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT); + } + $fiveBitBinaryArray = str_split($binaryString, 5); + $base32 = ""; + $i = 0; + while ($i < count($fiveBitBinaryArray)) { + $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)]; + $i++; + } + if ($padding && ($x = strlen($binaryString) % 40) != 0) { + if ($x == 8) $base32 .= str_repeat($base32chars[32], 6); + elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4); + elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3); + elseif ($x == 32) $base32 .= $base32chars[32]; + } + return $base32; + } + + /** + * Get array with all 32 characters for decoding from/encoding to base32 + * + * @return array + */ + protected function _getBase32LookupTable() + { + return array( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 + 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 + '=' // padding char + ); + } +} diff --git a/lib/admin-test.php b/lib/admin-test.php new file mode 100644 index 0000000..8412c37 --- /dev/null +++ b/lib/admin-test.php @@ -0,0 +1,612 @@ +postCount(); + + // New - below 1 hour and 1 post + if (!$userpwd->isUserKnown(60, 1) || $post_count < 1) { + $known_status = 1; + } + // Recent - above 1h and 1 post / below 3h and 6 posts + else if (!$userpwd->isUserKnown(4320) || $post_count < 6) { + $known_status = 2; + } + // Regular - above 3 days and 5 posts / below 7 days and 21 posts + else if (!$userpwd->isUserKnown(10080) || $post_count < 21) { + $known_status = 3; + } + + if ($userpwd->verifiedLevel()) { + $verified_level = 1; + } + } + + $data = [ $browser_id, $req_sig, $known_status, $verified_level ]; + $data = implode(':', $data); + + return $data; +} + +function _grep_notjanitor( $a ) +{ + return ( $a != 'janitor' ); +} + +function get_random_string( $len = 16 ) +{ + $str = mt_rand( 1000000, 9999999 ); + $str = hash( 'sha256', $str ); + + return substr( $str, -$len ); +} + +function derefer_url($url) { + return 'https://www.4chan.org/derefer?url=' . rawurlencode($url); +} + +function access_check() +{ + global $access; + + $user = $_COOKIE['4chan_auser']; + $pass = $_COOKIE['apass']; + + if( !$user || !$pass ) return; + + $query = mysql_global_call( "SELECT allow,password_expired,level,flags,username,password,signed_agreement FROM mod_users WHERE username='%s' LIMIT 1", $user ); + + if (!mysql_num_rows($query)) { + return ''; + } + + list($allow, $expired, $level, $flags, $username, $password, $signed_agreement) = mysql_fetch_row($query); + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $hashed_admin_password = hash('sha256', $username . $password . $admin_salt); + + if ($hashed_admin_password !== $pass) { + return ''; + } + + if( $expired ) { + die( 'Your password has expired; check IRC for instructions on changing it.' ); + } + + if ($signed_agreement == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php') { + die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.'); + } + + if( $allow ) { + if( $level == 'janitor' ) { + $a = $access['janitor']; + $a['board'] = array_filter( explode( ',', $allow ), '_grep_notjanitor' ); + if( in_array( "all", $a['board'] ) ) + unset( $a['board'] ); + + return $a; + } elseif( $level == 'manager' ) { + return $access['manager']; + } elseif( $level == 'admin' ) { + return $access['admin']; + } elseif( $level == 'mod' ) { + if (is_array($access['mod'])) { + $flags = explode(',', $flags); + $access['mod']['is_developer'] = in_array('developer', $flags); + } + return $access['mod']; + } else { + die( 'oh no you are not a right user!' ); + } + } else { + return ''; + } +} + +//based on team pages' valid(), need to merge with above! +//this sets different globals and respects deny +function access_check2( $func = 0 ) +{ + global $is_admin, $user, $pass; + $is_admin = 0; + $user = ""; + $pass = ""; + if( isset( $_COOKIE['4chan_auser'] ) && isset( $_COOKIE['4chan_apass'] ) ) { + $user = $_COOKIE['4chan_auser']; + $pass = $_COOKIE['4chan_apass']; + } + if( isset( $user ) && $user && $pass ) { + $result = mysql_global_call( "SELECT allow,deny,password_expired FROM " . SQLLOGMOD . " WHERE username='%s' and password='%s' limit 1", $user, $pass ); + if( mysql_num_rows( $result ) != 0 ) { + list( $allowed, $denied, $expired ) = mysql_fetch_array( $result ); + if( $expired ) { + die( 'Your password has expired; check IRC for instructions on changing it.' ); + } + if( $func == "unban" ) { + $deny_arr = explode( ",", $denied ); + if( in_array( "unban", $deny_arr ) ) die( "You do not have access to unban users." ); + } + $allow_arr = explode( ",", $allowed ); + if( in_array( "admin", $allow_arr ) || in_array( "manager", $allow_arr ) ) $is_admin = 1; + } else { + die( "Please login via admin panel first. (admin user not found)" ); + } + if( $user && !$pass ) { + die( "Please login via admin panel first. (no pass specified)" ); + } elseif( !$user && $pass ) { + die( "Please login via admin panel first. (no user specified)" ); + } + } else { + die( "Please login via admin panel first." ); + } +} + +function form_post_values( $names ) +{ + $a = array(); + + foreach( $names as $n ) { + $v = $_REQUEST[$n]; + if( $v ) $a[$n] = $v; + } + + return $a; +} + +//rebuild the bans for board $boards +function rebuild_bans( $boards ) +{ + // run in background + $cmd = "nohup /usr/local/bin/suid_run_global bin/rebuildbans $boards >/dev/null 2>&1 &"; +// print "
    Rebuilding bans in $boards
    "; + exec( $cmd ); +} + +//add list of bans to the file for $boards +function append_bans( $boards, $bans ) +{ + $str = is_array( $bans ) ? implode( ",", $bans ) : $bans; + $cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $boards $str >/dev/null 2>&1 &"; +// print "
    Added new bans to $boards
    "; + exec( $cmd ); +} + +// IPs that can't be banned because they're known good proxy servers +// e.g. cloudflare, singapore +function whitelisted_ip( $ip = 0 ) +{ + list( $ips ) = post_filter_get( "ipwhitelist" ); + if( $ip === 0 ) $ip = $_SERVER["REMOTE_ADDR"]; + + return find_ipxff_in( ip2long( $ip ), 0, $ips ); +} + +// add a global ban (indefinite for now) +// returns true if it was new (not already inserted) +function add_ban( $ip, $reason, $days = -1, $zonly = false, $origname = 'Anonymous', &$error, $no = 0, $pass = '', $no_reverse = false ) +{ + global $user; + if( ip2long( $ip ) === false ) { + $error = "invalid IP address"; + + return false; + } + if( whitelisted_ip( $ip ) ) { + $error = "IP is whitelisted"; + + return false; + } + + // FIXME add unique index to banned_users instead + $prev = mysql_global_call( "SELECT COUNT(*)>0 FROM " . SQLLOGBAN . " WHERE active=1 AND global=1 AND host='%s'", $ip ); + list( $nprev ) = mysql_fetch_array( $prev ); + if( $nprev > 0 ) return false; + + if ($no_reverse) { + $rev = $ip; + } + else { + $rev = gethostbyaddr( $ip ); + } + + $tripcode = ''; + + $name_bits = explode('
    !', $origname); + + if ($name_bits[1]) { + $tripcode = preg_replace('/<[^>]+>/', '', $name_bits[1]); + } + + $origname = str_replace( ' !', ' #', $origname ); + $origname = preg_replace( '/<[^>]+>/', '', $origname ); // remove all remaining html crap + + $board = defined( 'BOARD_DIR' ) ? BOARD_DIR : ""; + + if( $days == -1 ) + $length = "00000000000000"; + else + $length = date( "Ymd", time() + $days * ( 24 * 60 * 60 ) ) . '000000'; + + echo "Banned $ip (" . htmlspecialchars( $rev ) . ")
    \n"; + + if (!isset($user)) { + $banned_by = $_COOKIE['4chan_auser']; + } + else { + $banned_by = $user; + } + + mysql_global_do( "INSERT INTO " . SQLLOGBAN . " (global,board,host,reverse,reason,admin,zonly,length,name,tripcode,4pass_id,post_num,admin_ip) values (%d,'%s','%s','%s','%s','%s',%d,'%s','%s','%s','%s',%d,'%s')", !$zonly, $board, $ip, $rev, "$reason", $banned_by, $zonly, $length, $origname, $tripcode, $pass, $no, $_SERVER['REMOTE_ADDR'] ); + + return true; +} + +function is_real_board( $board ) +{ + // no board + if( $board === "-" || $board === '' ) return true; + + $res = mysql_global_call( "select count(*) from boardlist where dir='%s'", $board ); + $row = mysql_fetch_row( $res ); + + return ( $row[0] > 0 ); +} + +function remote_delete_things( $board, $nos, $tool = null ) +{ + // see reports/actions.php, action_delete() + $url = "https://sys.int/$board/"; + + if( $board != 'f' ) // XXX dumb. :( XXX + $url .= 'imgboard.php'; + else + $url .= 'up.php'; + + // Build the appropriate POST and cookie... + $post = array(); + $post['mode'] = 'usrdel'; + $post['onlyimgdel'] = ''; // never delete only img + + if ($tool) { + $post['tool'] = $tool; + } + + // note multiple post number deletions + foreach( $nos as $no ) + $post[$no] = 'delete'; + + $post['remote_addr'] = $_SERVER['REMOTE_ADDR']; + + rpc_start_request($url, $post, $_COOKIE, true); + + return ""; +} + +function clear_cookies() +{ + if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) { + setcookie( "4chan_auser", "", time() - 3600, "/", ".4chan.org", true ); + setcookie( "4chan_apass", "", time() - 3600, "/", ".4chan.org", true ); + setcookie( "4chan_aflags", "", time() - 3600, "/", ".4chan.org", true ); + + } elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) { + setcookie( "4chan_auser", "", time() - 24 * 3600, "/", ".4channel.org", true ); + setcookie( "4chan_apass", "", time() - 24 * 3600, "/", ".4channel.org", true ); + } else { + setcookie( "4chan_auser", "", time() - 24 * 3600, "/", true ); + setcookie( "4chan_apass", "", time() - 24 * 3600, "/", true ); + setcookie( "4chan_aflags", "", time() - 24 * 3600, "/", true ); + } + + setcookie( 'extra_path', '', 1, '/', '.4chan.org' ); +} + +// record and autoban failed logins. assumes admin or imgboard.php as caller +function admin_login_fail() +{ + $ip = ip2long( $_SERVER["REMOTE_ADDR"] ); + clear_cookies(); + + mysql_global_call( "insert into user_actions (ip,board,action,time) values (%d,'%s','fail_login',now())", $ip, BOARD_DIR ); + + $query = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='fail_login' and time >= subdate(now(), interval 1 hour)", LOGIN_FAIL_HOURLY, $ip ); + if( mysql_result( $query, 0, 0 ) ) { + auto_ban_poster( "", -1, 1, "failed to login to /" . BOARD_DIR . "/admin.php " . LOGIN_FAIL_HOURLY . " times", "Repeated admin login failures." ); + } + + error( S_WRONGPASS ); +} + +// delete all posts everywhere by the poster's IP +// for autobans +function del_all_posts( $ip = false ) +{ + $q = mysql_global_call( "select sql_cache dir from boardlist" ); + $boards = mysql_column_array( $q ); + + $host = $ip ? $ip : $_SERVER['REMOTE_ADDR']; + + foreach( $boards as $b ) { + $q = mysql_board_call( "select no from `%s` where host='%s'", $b, $host ); + $posts = mysql_column_array( $q ); + if( !count( $posts ) ) continue; + remote_delete_things( $b, $posts ); + } +} + +function auto_ban_poster($nametrip, $banlength, $global, $reason, $pubreason = '', $is_filter = false, $pwd = null, $pass_id = null) { + if (!$nametrip) { + $nametrip = S_ANONAME; + } + + if (strpos($nametrip, '
    !') !== false) { + $nameparts = explode(' !', $nametrip); + $nametrip = "{$nameparts[0]} #{$nameparts[1]}"; + } + + $host = $_SERVER['REMOTE_ADDR']; + $reverse = mysql_real_escape_string(gethostbyaddr($host)); + + $nametrip = mysql_real_escape_string($nametrip); + $global = ($global ? 1 : 0); + $board = defined( 'BOARD_DIR' ) ? BOARD_DIR : ''; + $reason = mysql_real_escape_string($reason); + $pubreason = mysql_real_escape_string($pubreason); + + if ($pubreason) { + $pubreason .= "<>"; + } + + if ($pass_id) { + $pass_id = mysql_real_escape_string($pass_id); + } + else { + $pass_id = ''; + } + + if ($pwd) { + $pwd = mysql_real_escape_string($pwd); + } + else { + $pwd = ''; + } + + // check for whitelisted ban + if( whitelisted_ip() ) return; + + //if they're already banned on this board, don't insert again + //since this is just a spam post + //i don't think it matters if the active ban is global=0 and this one is global=1 + /* + if ($banlength == -1) { + $existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 AND global = 1 AND length = 0"); + } + else { + $existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 and (board='$board' or global=1)"); + } + $existingban = mysql_result( $existingq, 0, 0 ); + if( $existingban > 0 ) { + delete_uploaded_files(); + die(); + } + */ + /* + if( $banlength == 0 ) { // warning + // check for recent warnings to punish spammers + $autowarnq = mysql_global_call( "SELECT COUNT(*) FROM " . SQLLOGBAN . " WHERE host='$host' AND admin='Auto-ban' AND now > DATE_SUB(NOW(),INTERVAL 3 DAY) AND reason like '%$reason'" ); + $autowarncount = mysql_result( $autowarnq, 0, 0 ); + if( $autowarncount > 3 ) { + $banlength = 14; + } + } + */ + + if ($banlength == -1) { // permanent + $length = '0000' . '00' . '00'; // YYYY/MM/DD + } + else { + $banlength = (int)$banlength; + + if ($banlength < 0) { + $banlength = 0; + } + + $length = date('Ymd', time() + $banlength * (24 * 60 * 60)); + } + + $length .= "00" . "00" . "00"; // H:M:S + + $sql = "INSERT INTO " . SQLLOGBAN . " (board,global,name,host,reason,length,admin,reverse,post_time,4pass_id,password) VALUES('$board','$global','$nametrip','$host','{$pubreason}Auto-ban: $reason','$length','Auto-ban','$reverse',NOW(),'$pass_id','$pwd')"; + + $res = mysql_global_call($sql); + + if (!$res) { + die(S_SQLFAIL); + } + + //append_bans( $global ? "global" : $board, array($host) ); + + //$child = stripos($pubreason, 'child') !== false || stripos($reason, 'child') !== false; + + //if ($global && $child && !$is_filter) { + // del_all_posts(); + //} +} + +function cloudflare_purge_url_old($file,$secondary = false) +{ + global $purges; + + if (!defined('CLOUDFLARE_API_TOKEN')) { + internal_error_log('cf', "tried purging but token isn't set"); + return null; + } + + $post = array( + "tkn" => CLOUDFLARE_API_TOKEN, + "email" => CLOUDFLARE_EMAIL, + "a" => "zone_file_purge", + "z" => $secondary ? CLOUDFLARE_ZONE_2 : CLOUDFLARE_ZONE, + "url" => $file + ); + + //quick_log_to("/www/perhost/cf-purge.log", print_r($post, true)); + + $ch = rpc_start_request("https://www.cloudflare.com/api_json.html", $post, array(), false); + return $ch; +} + +function write_to_event_log($event, $ip, $args = []) { + $sql = << 0) { + $json_post['file'] = $post["filename"].$post["ext"]; + $json_post['md5'] = $post["md5"]; + } + + $json_post = json_encode($json_post, JSON_PARTIAL_OUTPUT_ON_ERROR); + + return write_to_event_log($event, $ip, [ + 'board' => $board, + 'thread_id' => $post['resto'] ? $post['resto'] : $post['no'], + 'post_id' => $post['no'], + 'arg_str' => $username, + 'pwd' => $pwd, + 'meta' => $json_post + ]); +} + +function cloudflare_purge_url($files, $zone2 = false) { + // 4cdn = ca66ca34d08802412ae32ee20b7e98af (zone2) + // 4chan = 363d1b9b6be563ffd5143c8cfcc29d52 + + $url = 'https://api.cloudflare.com/client/v4/zones/' + . ($zone2 ? 'ca66ca34d08802412ae32ee20b7e98af' : '363d1b9b6be563ffd5143c8cfcc29d52') + . '/purge_cache'; + + $opts = array( + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_HTTPHEADER => array( + 'Authorization: Bearer iTf0pQMTvn0zSHAN9vg5S1m_tiwmPKYDjepq8za9', + 'Content-Type: application/json' + ) + ); + + // Multiple files + if (is_array($files)) { + // Batching + if (count($files) > 30) { + $files = array_chunk($files, 30); + + foreach ($files as $batch) { + $opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($batch, JSON_UNESCAPED_SLASHES) . '}'; + //print_r($opts[CURLOPT_POSTFIELDS]); + rpc_start_request_with_options($url, $opts); + } + } + else { + $opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($files, JSON_UNESCAPED_SLASHES) . '}'; + //print_r($opts[CURLOPT_POSTFIELDS]); + rpc_start_request_with_options($url, $opts); + } + } + // Single file + else { + $opts[CURLOPT_POSTFIELDS] = '{"files":["' . $files . '"]}'; + //print_r($opts[CURLOPT_POSTFIELDS]); + rpc_start_request_with_options($url, $opts); + } +} + +function cloudflare_purge_by_basename($board, $basename) { + preg_match("/([0-9]+)[sm]?\\.([a-z]{3,4})/", $basename, $m); + $tim = $m[1]; + $ext = $m[2]; + + cloudflare_purge_url("https://i.4cdn.org/$board/$tim.$ext", true); + cloudflare_purge_url("https://i.4cdn.org/$board/${tim}s.jpg", true); + cloudflare_purge_url("https://i.4cdn.org/$board/${tim}m.jpg", true); +} diff --git a/lib/admin.php b/lib/admin.php new file mode 100644 index 0000000..c147164 --- /dev/null +++ b/lib/admin.php @@ -0,0 +1,603 @@ +isUserKnown(60, 1)) { // 1h + $known_status = 1; // New user + } + else if (!$userpwd->isUserKnown(1440)) { // 24h + $known_status = 2; // Not yet trusted + } + + if ($userpwd->verifiedLevel()) { + $verified_level = 1; + } + } + + $data = [ $browser_id, $req_sig, $known_status, $verified_level ]; + $data = implode(':', $data); + + return $data; +} + +function _grep_notjanitor( $a ) +{ + return ( $a != 'janitor' ); +} + +function get_random_string( $len = 16 ) +{ + $str = mt_rand( 1000000, 9999999 ); + $str = hash( 'sha256', $str ); + + return substr( $str, -$len ); +} + +function derefer_url($url) { + return 'https://www.4chan.org/derefer?url=' . rawurlencode($url); +} + +function access_check() +{ + global $access; + + $user = $_COOKIE['4chan_auser']; + $pass = $_COOKIE['apass']; + + if( !$user || !$pass ) return; + + $query = mysql_global_call( "SELECT allow,password_expired,level,flags,username,password,signed_agreement FROM mod_users WHERE username='%s' LIMIT 1", $user ); + + if (!mysql_num_rows($query)) { + return ''; + } + + list($allow, $expired, $level, $flags, $username, $password, $signed_agreement) = mysql_fetch_row($query); + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $hashed_admin_password = hash('sha256', $username . $password . $admin_salt); + + if ($hashed_admin_password !== $pass) { + return ''; + } + + if( $expired ) { + die( 'Your password has expired; check IRC for instructions on changing it.' ); + } + + if ($signed_agreement == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php') { + die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.'); + } + + if( $allow ) { + if( $level == 'janitor' ) { + $a = $access['janitor']; + $a['board'] = array_filter( explode( ',', $allow ), '_grep_notjanitor' ); + if( in_array( "all", $a['board'] ) ) + unset( $a['board'] ); + + return $a; + } elseif( $level == 'manager' ) { + return $access['manager']; + } elseif( $level == 'admin' ) { + return $access['admin']; + } elseif( $level == 'mod' ) { + if (is_array($access['mod'])) { + $flags = explode(',', $flags); + $access['mod']['is_developer'] = in_array('developer', $flags); + } + return $access['mod']; + } else { + die( 'oh no you are not a right user!' ); + } + } else { + return ''; + } +} + +//based on team pages' valid(), need to merge with above! +//this sets different globals and respects deny +function access_check2( $func = 0 ) +{ + global $is_admin, $user, $pass; + $is_admin = 0; + $user = ""; + $pass = ""; + if( isset( $_COOKIE['4chan_auser'] ) && isset( $_COOKIE['4chan_apass'] ) ) { + $user = $_COOKIE['4chan_auser']; + $pass = $_COOKIE['4chan_apass']; + } + if( isset( $user ) && $user && $pass ) { + $result = mysql_global_call( "SELECT allow,deny,password_expired FROM " . SQLLOGMOD . " WHERE username='%s' and password='%s' limit 1", $user, $pass ); + if( mysql_num_rows( $result ) != 0 ) { + list( $allowed, $denied, $expired ) = mysql_fetch_array( $result ); + if( $expired ) { + die( 'Your password has expired; check IRC for instructions on changing it.' ); + } + if( $func == "unban" ) { + $deny_arr = explode( ",", $denied ); + if( in_array( "unban", $deny_arr ) ) die( "You do not have access to unban users." ); + } + $allow_arr = explode( ",", $allowed ); + if( in_array( "admin", $allow_arr ) || in_array( "manager", $allow_arr ) ) $is_admin = 1; + } else { + die( "Please login via admin panel first. (admin user not found)" ); + } + if( $user && !$pass ) { + die( "Please login via admin panel first. (no pass specified)" ); + } elseif( !$user && $pass ) { + die( "Please login via admin panel first. (no user specified)" ); + } + } else { + die( "Please login via admin panel first." ); + } +} + +function form_post_values( $names ) +{ + $a = array(); + + foreach( $names as $n ) { + $v = $_REQUEST[$n]; + if( $v ) $a[$n] = $v; + } + + return $a; +} + +//rebuild the bans for board $boards +function rebuild_bans( $boards ) +{ + // run in background + $cmd = "nohup /usr/local/bin/suid_run_global bin/rebuildbans $boards >/dev/null 2>&1 &"; +// print "
    Rebuilding bans in $boards
    "; + exec( $cmd ); +} + +//add list of bans to the file for $boards +function append_bans( $boards, $bans ) +{ + $str = is_array( $bans ) ? implode( ",", $bans ) : $bans; + $cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $boards $str >/dev/null 2>&1 &"; +// print "
    Added new bans to $boards
    "; + exec( $cmd ); +} + +// IPs that can't be banned because they're known good proxy servers +// e.g. cloudflare, singapore +function whitelisted_ip( $ip = 0 ) +{ + list( $ips ) = post_filter_get( "ipwhitelist" ); + if( $ip === 0 ) $ip = $_SERVER["REMOTE_ADDR"]; + + return find_ipxff_in( ip2long( $ip ), 0, $ips ); +} + +// add a global ban (indefinite for now) +// returns true if it was new (not already inserted) +function add_ban( $ip, $reason, $days = -1, $zonly = false, $origname = 'Anonymous', &$error, $no = 0, $pass = '', $no_reverse = false ) +{ + global $user; + if( ip2long( $ip ) === false ) { + $error = "invalid IP address"; + + return false; + } + if( whitelisted_ip( $ip ) ) { + $error = "IP is whitelisted"; + + return false; + } + + // FIXME add unique index to banned_users instead + $prev = mysql_global_call( "SELECT COUNT(*)>0 FROM " . SQLLOGBAN . " WHERE active=1 AND global=1 AND host='%s'", $ip ); + list( $nprev ) = mysql_fetch_array( $prev ); + if( $nprev > 0 ) return false; + + if ($no_reverse) { + $rev = $ip; + } + else { + $rev = gethostbyaddr( $ip ); + } + + $tripcode = ''; + + $name_bits = explode('
    !', $origname); + + if ($name_bits[1]) { + $tripcode = preg_replace('/<[^>]+>/', '', $name_bits[1]); + } + + $origname = str_replace( ' !', ' #', $origname ); + $origname = preg_replace( '/<[^>]+>/', '', $origname ); // remove all remaining html crap + + $board = defined( 'BOARD_DIR' ) ? BOARD_DIR : ""; + + if( $days == -1 ) + $length = "00000000000000"; + else + $length = date( "Ymd", time() + $days * ( 24 * 60 * 60 ) ) . '000000'; + + echo "Banned $ip (" . htmlspecialchars( $rev ) . ")
    \n"; + + if (!isset($user)) { + $banned_by = $_COOKIE['4chan_auser']; + } + else { + $banned_by = $user; + } + + mysql_global_do( "INSERT INTO " . SQLLOGBAN . " (global,board,host,reverse,reason,admin,zonly,length,name,tripcode,4pass_id,post_num,admin_ip) values (%d,'%s','%s','%s','%s','%s',%d,'%s','%s','%s','%s',%d,'%s')", !$zonly, $board, $ip, $rev, "$reason", $banned_by, $zonly, $length, $origname, $tripcode, $pass, $no, $_SERVER['REMOTE_ADDR'] ); + + return true; +} + +function is_real_board( $board ) +{ + // no board + if( $board === "-" || $board === '' ) return true; + + $res = mysql_global_call( "select count(*) from boardlist where dir='%s'", $board ); + $row = mysql_fetch_row( $res ); + + return ( $row[0] > 0 ); +} + +function remote_delete_things( $board, $nos, $tool = null ) +{ + // see reports/actions.php, action_delete() + $url = "https://sys.int/$board/"; + + if( $board != 'f' ) // XXX dumb. :( XXX + $url .= 'imgboard.php'; + else + $url .= 'up.php'; + + // Build the appropriate POST and cookie... + $post = array(); + $post['mode'] = 'usrdel'; + $post['onlyimgdel'] = ''; // never delete only img + + if ($tool) { + $post['tool'] = $tool; + } + + // note multiple post number deletions + foreach( $nos as $no ) + $post[$no] = 'delete'; + + $post['remote_addr'] = $_SERVER['REMOTE_ADDR']; + + rpc_start_request($url, $post, $_COOKIE, true); + + return ""; +} + +function clear_cookies() +{ + if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) { + setcookie( "4chan_auser", "", time() - 3600, "/", ".4chan.org", true ); + setcookie( "4chan_apass", "", time() - 3600, "/", ".4chan.org", true ); + setcookie( "4chan_aflags", "", time() - 3600, "/", ".4chan.org", true ); + + } elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) { + setcookie( "4chan_auser", "", time() - 24 * 3600, "/", ".4channel.org", true ); + setcookie( "4chan_apass", "", time() - 24 * 3600, "/", ".4channel.org", true ); + } else { + setcookie( "4chan_auser", "", time() - 24 * 3600, "/", true ); + setcookie( "4chan_apass", "", time() - 24 * 3600, "/", true ); + setcookie( "4chan_aflags", "", time() - 24 * 3600, "/", true ); + } + + setcookie( 'extra_path', '', 1, '/', '.4chan.org' ); +} + +// record and autoban failed logins. assumes admin or imgboard.php as caller +function admin_login_fail() +{ + $ip = ip2long( $_SERVER["REMOTE_ADDR"] ); + clear_cookies(); + + mysql_global_call( "insert into user_actions (ip,board,action,time) values (%d,'%s','fail_login',now())", $ip, BOARD_DIR ); + + $query = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='fail_login' and time >= subdate(now(), interval 1 hour)", LOGIN_FAIL_HOURLY, $ip ); + if( mysql_result( $query, 0, 0 ) ) { + auto_ban_poster( "", -1, 1, "failed to login to /" . BOARD_DIR . "/admin.php " . LOGIN_FAIL_HOURLY . " times", "Repeated admin login failures." ); + } + + error( S_WRONGPASS ); +} + +// delete all posts everywhere by the poster's IP +// for autobans +function del_all_posts( $ip = false ) +{ + $q = mysql_global_call( "select sql_cache dir from boardlist" ); + $boards = mysql_column_array( $q ); + + $host = $ip ? $ip : $_SERVER['REMOTE_ADDR']; + + foreach( $boards as $b ) { + $q = mysql_board_call( "select no from `%s` where host='%s'", $b, $host ); + $posts = mysql_column_array( $q ); + if( !count( $posts ) ) continue; + remote_delete_things( $b, $posts ); + } +} + +function auto_ban_poster($nametrip, $banlength, $global, $reason, $pubreason = '', $is_filter = false, $pwd = null, $pass_id = null) { + if (!$nametrip) { + $nametrip = S_ANONAME; + } + + if (strpos($nametrip, '
    !') !== false) { + $nameparts = explode(' !', $nametrip); + $nametrip = "{$nameparts[0]} #{$nameparts[1]}"; + } + + $host = $_SERVER['REMOTE_ADDR']; + $reverse = mysql_real_escape_string(gethostbyaddr($host)); + + $nametrip = mysql_real_escape_string($nametrip); + $global = ($global ? 1 : 0); + $board = defined( 'BOARD_DIR' ) ? BOARD_DIR : ''; + $reason = mysql_real_escape_string($reason); + $pubreason = mysql_real_escape_string($pubreason); + + if ($pubreason) { + $pubreason .= "<>"; + } + + if ($pass_id) { + $pass_id = mysql_real_escape_string($pass_id); + } + else { + $pass_id = ''; + } + + if ($pwd) { + $pwd = mysql_real_escape_string($pwd); + } + else { + $pwd = ''; + } + + // check for whitelisted ban + if( whitelisted_ip() ) return; + + //if they're already banned on this board, don't insert again + //since this is just a spam post + //i don't think it matters if the active ban is global=0 and this one is global=1 + /* + if ($banlength == -1) { + $existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 AND global = 1 AND length = 0"); + } + else { + $existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 and (board='$board' or global=1)"); + } + $existingban = mysql_result( $existingq, 0, 0 ); + if( $existingban > 0 ) { + delete_uploaded_files(); + die(); + } + */ + /* + if( $banlength == 0 ) { // warning + // check for recent warnings to punish spammers + $autowarnq = mysql_global_call( "SELECT COUNT(*) FROM " . SQLLOGBAN . " WHERE host='$host' AND admin='Auto-ban' AND now > DATE_SUB(NOW(),INTERVAL 3 DAY) AND reason like '%$reason'" ); + $autowarncount = mysql_result( $autowarnq, 0, 0 ); + if( $autowarncount > 3 ) { + $banlength = 14; + } + } + */ + + if ($banlength == -1) { // permanent + $length = '0000' . '00' . '00'; // YYYY/MM/DD + } + else { + $banlength = (int)$banlength; + + if ($banlength < 0) { + $banlength = 0; + } + + $length = date('Ymd', time() + $banlength * (24 * 60 * 60)); + } + + $length .= "00" . "00" . "00"; // H:M:S + + $sql = "INSERT INTO " . SQLLOGBAN . " (board,global,name,host,reason,length,admin,reverse,post_time,4pass_id,password) VALUES('$board','$global','$nametrip','$host','{$pubreason}Auto-ban: $reason','$length','Auto-ban','$reverse',NOW(),'$pass_id','$pwd')"; + + $res = mysql_global_call($sql); + + if (!$res) { + die(S_SQLFAIL); + } + + //append_bans( $global ? "global" : $board, array($host) ); + + //$child = stripos($pubreason, 'child') !== false || stripos($reason, 'child') !== false; + + //if ($global && $child && !$is_filter) { + // del_all_posts(); + //} +} + +function cloudflare_purge_url_old($file,$secondary = false) +{ + global $purges; + + if (!defined('CLOUDFLARE_API_TOKEN')) { + internal_error_log('cf', "tried purging but token isn't set"); + return null; + } + + $post = array( + "tkn" => CLOUDFLARE_API_TOKEN, + "email" => CLOUDFLARE_EMAIL, + "a" => "zone_file_purge", + "z" => $secondary ? CLOUDFLARE_ZONE_2 : CLOUDFLARE_ZONE, + "url" => $file + ); + + //quick_log_to("/www/perhost/cf-purge.log", print_r($post, true)); + + $ch = rpc_start_request("https://www.cloudflare.com/api_json.html", $post, array(), false); + return $ch; +} + +function write_to_event_log($event, $ip, $args = []) { + $sql = << 0) { + $json_post['file'] = $post["filename"].$post["ext"]; + $json_post['md5'] = $post["md5"]; + } + + $json_post = json_encode($json_post, JSON_PARTIAL_OUTPUT_ON_ERROR); + + return write_to_event_log($event, $ip, [ + 'board' => $board, + 'thread_id' => $post['resto'] ? $post['resto'] : $post['no'], + 'post_id' => $post['no'], + 'arg_str' => $username, + 'pwd' => $pwd, + 'meta' => $json_post + ]); +} + +function cloudflare_purge_url($files, $zone2 = false) { + // 4cdn = ca66ca34d08802412ae32ee20b7e98af (zone2) + // 4chan = 363d1b9b6be563ffd5143c8cfcc29d52 + + $url = 'https://api.cloudflare.com/client/v4/zones/' + . ($zone2 ? 'ca66ca34d08802412ae32ee20b7e98af' : '363d1b9b6be563ffd5143c8cfcc29d52') + . '/purge_cache'; + + $opts = array( + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_HTTPHEADER => array( + 'Authorization: Bearer iTf0pQMTvn0zSHAN9vg5S1m_tiwmPKYDjepq8za9', + 'Content-Type: application/json' + ) + ); + + // Multiple files + if (is_array($files)) { + // Batching + if (count($files) > 30) { + $files = array_chunk($files, 30); + + foreach ($files as $batch) { + $opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($batch, JSON_UNESCAPED_SLASHES) . '}'; + //print_r($opts[CURLOPT_POSTFIELDS]); + rpc_start_request_with_options($url, $opts); + } + } + else { + $opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($files, JSON_UNESCAPED_SLASHES) . '}'; + //print_r($opts[CURLOPT_POSTFIELDS]); + rpc_start_request_with_options($url, $opts); + } + } + // Single file + else { + $opts[CURLOPT_POSTFIELDS] = '{"files":["' . $files . '"]}'; + //print_r($opts[CURLOPT_POSTFIELDS]); + rpc_start_request_with_options($url, $opts); + } +} + +function cloudflare_purge_by_basename($board, $basename) { + preg_match("/([0-9]+)[sm]?\\.([a-z]{3,4})/", $basename, $m); + $tim = $m[1]; + $ext = $m[2]; + + cloudflare_purge_url("https://i.4cdn.org/$board/$tim.$ext", true); + cloudflare_purge_url("https://i.4cdn.org/$board/${tim}s.jpg", true); + cloudflare_purge_url("https://i.4cdn.org/$board/${tim}m.jpg", true); +} diff --git a/lib/ads-test.php b/lib/ads-test.php new file mode 100644 index 0000000..85c6de2 --- /dev/null +++ b/lib/ads-test.php @@ -0,0 +1,155 @@ +",$ads[$n]); + + if (!$url) return ""; + + $text = "".htmlspecialchars($desc).""; + + $n++; + + if ($n == $num_ads) + $n = 0; + + $text_ads_n[$file] = $n; + + return $text; +} + +//duplicate of rid.php +//dir - absolute path from web root to dir inc. trailing slash +//urlroot - what to append the name to to get the url +function rid_in_directory($dir,$urlroot) { + global $document_root; + $realdir = "/www/global/imgtop/dontblockthis/".$dir; + $ft = "$realdir/files.txt"; + $names = file_array_cached($ft); + + if (!$names) { + $arr = scandir($realdir); + + foreach ($arr as $fi) { + if (preg_match("/\.(jpg|gif|png)$/", $fi)) { + $names[] = $fi; + } + } + + file_put_contents($ft, join($names, "\n")); + } + + return $urlroot.$names[rand(0, count($names)-1)]; +} + +// Takes a dir and a filename and uses file() to parse it +// then returns a random value. +function rand_from_flatfile( $dir, $filename ) +{ + $file = $dir . $filename; + $names = file_array_cached( $file ); + + + return $names[ rand( 0, count($names)-1 ) ]; +} + +function form_ads(&$dat) { + $error = false; // unused, errors have ads too + $dat .= "
    "; + /*if(!$error && FIXED_AD == 1) { + $dat.=''; + }*/ + if(FIXED_LEFT_AD == 1) { + if(defined('FIXED_LEFT_TXT') && FIXED_LEFT_TXT) { + $dat.= ad_text_for(FIXED_LEFT_TXT); + } + else if(defined('FIXED_LEFT_TABLE')) { + list($ldimg,$ldhref) = rid(FIXED_LEFT_TABLE,1); + $dat.=''; + } + } + if(FIXED_RIGHT_AD == 1) { + if(defined('FIXED_RIGHT_TXT') && FIXED_RIGHT_TXT) { + $dat.= ad_text_for(FIXED_RIGHT_TXT); + } + else if(defined('FIXED_RIGHT_TABLE')) { + list($ldimg,$ldhref) = rid(FIXED_RIGHT_TABLE,1); + $dat.=''; + } + } + $dat .= "
    "; +} + +function ad_text_for($path) { + $txt = @file_get_contents_cached($path); + + if (!$txt) return $txt; + + return preg_replace_callback("@RANDOM@", "rand", $txt); +} + +function global_msg_txt() { + static $globalmsgtxt, $globalmsgdate; + + if (!$globalmsgdate) { + if (file_exists(GLOBAL_MSG_FILE)) { + $globalmsgtxt = file_get_contents(GLOBAL_MSG_FILE); + $globalmsgdate = filemtime(GLOBAL_MSG_FILE); + + if ($globalmsgtxt) { + $globalmsgtxt = str_replace('{{4CHAN_DOMAIN}}', MAIN_DOMAIN, $globalmsgtxt); + } + } + else { + $globalmsgtxt = null; + $globalmsgdate = 1; + } + } + + return array($globalmsgtxt, $globalmsgdate); +} + +?> diff --git a/lib/ads.php b/lib/ads.php new file mode 100644 index 0000000..298e95d --- /dev/null +++ b/lib/ads.php @@ -0,0 +1,145 @@ +",$ads[$n]); + + if (!$url) return ""; + + $text = "".htmlspecialchars($desc).""; + + $n++; + + if ($n == $num_ads) + $n = 0; + + $text_ads_n[$file] = $n; + + return $text; +} + +//duplicate of rid.php +//dir - absolute path from web root to dir inc. trailing slash +//urlroot - what to append the name to to get the url +function rid_in_directory($dir,$urlroot) { + global $document_root; + $realdir = "/www/global/imgtop/dontblockthis/".$dir; + $ft = "$realdir/files.txt"; + $names = file_array_cached($ft); + + if (!$names) { + $arr = scandir($realdir); + + foreach ($arr as $fi) { + if (preg_match("/\.(jpg|gif|png)$/", $fi)) { + $names[] = $fi; + } + } + + file_put_contents($ft, join($names, "\n")); + } + + return $urlroot.$names[rand(0, count($names)-1)]; +} + +// Takes a dir and a filename and uses file() to parse it +// then returns a random value. +function rand_from_flatfile( $dir, $filename ) +{ + $file = $dir . $filename; + $names = file_array_cached( $file ); + + + return $names[ rand( 0, count($names)-1 ) ]; +} + +function form_ads(&$dat) { + $error = false; // unused, errors have ads too + $dat .= "
    "; + /*if(!$error && FIXED_AD == 1) { + $dat.=''; + }*/ + if(FIXED_LEFT_AD == 1) { + if(defined('FIXED_LEFT_TXT') && FIXED_LEFT_TXT) { + $dat.= ad_text_for(FIXED_LEFT_TXT); + } + else if(defined('FIXED_LEFT_TABLE')) { + list($ldimg,$ldhref) = rid(FIXED_LEFT_TABLE,1); + $dat.=''; + } + } + if(FIXED_RIGHT_AD == 1) { + if(defined('FIXED_RIGHT_TXT') && FIXED_RIGHT_TXT) { + $dat.= ad_text_for(FIXED_RIGHT_TXT); + } + else if(defined('FIXED_RIGHT_TABLE')) { + list($ldimg,$ldhref) = rid(FIXED_RIGHT_TABLE,1); + $dat.=''; + } + } + $dat .= "
    "; +} + +function ad_text_for($path) { + $txt = @file_get_contents_cached($path); + + if (!$txt) return $txt; + + return preg_replace_callback("@RANDOM@", "rand", $txt); +} + +function global_msg_txt() { + static $globalmsgtxt, $globalmsgdate; + + if (!$globalmsgdate) { + $globalmsgtxt = @file_get_contents(GLOBAL_MSG_FILE); + $globalmsgdate = @filemtime(GLOBAL_MSG_FILE); + } + + return array($globalmsgtxt, $globalmsgdate); +} + +?> diff --git a/lib/archives.php b/lib/archives.php new file mode 100644 index 0000000..72980f2 --- /dev/null +++ b/lib/archives.php @@ -0,0 +1,171 @@ +/' . $board . '/' . $resno . ''; + } + + if ($url_only) { + return $url; + } + + $url = rawurlencode($url); + + return '/' . $board . '/' . $resno . ''; +} diff --git a/lib/auth-test.php b/lib/auth-test.php new file mode 100644 index 0000000..7709302 --- /dev/null +++ b/lib/auth-test.php @@ -0,0 +1,594 @@ + false, + 'flags' => false, + 'allow' => false, + 'deny' => false, + 'guest' => true, +); + +$levelorder = array( + 1 => 'janitor', + 10 => 'mod', + 20 => 'manager', + 50 => 'admin' +); + +$levelorderf = array( + 'janitor' => 1, + 'mod' => 10, + 'manager' => 20, + 'admin' => 50 +); + +if (!defined('SQLLOGMOD')) { + define("SQLLOGMOD", "mod_users"); + define('PASS_TIMEOUT', 1800); + define('LOGIN_FAIL_HOURLY', 5); +} + +function csrf_tag() { + if (isset($_COOKIE['_tkn'])) { + return ''; + } + else { + return ''; + } +} + +function csrf_attr() { + if (isset($_COOKIE['_tkn'])) { + return 'data-tkn="' . htmlspecialchars($_COOKIE['_tkn'], ENT_QUOTES) . '"'; + } + else { + return ''; + } +} + +function auth_encrypt($data) { + $key = file_get_contents('/www/keys/2015_enc.key'); + + if (!$key) { + return false; + } + + $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND); + + $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv); + + if ($encrypted === false) { + return false; + } + + return $iv . $encrypted; +} + +function auth_decrypt($data) { + $key = file_get_contents('/www/keys/2015_enc.key'); + + if (!$key) { + return false; + } + + $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv_dec = substr($data, 0, $iv_size); + + $data = substr($data, $iv_size); + + $data = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv_dec); + + if ($data === false) { + return false; + } + + return rtrim($data, "\0"); +} + +function verify_one_time_pwd($username, $otp) { + if (!$otp) { + return false; + } + + $query = "SELECT auth_secret FROM mod_users WHERE username = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $username); + + if (!$res) { + return false; + } + + $enc_secret = mysql_fetch_row($res)[0]; + + if (!$enc_secret) { + return false; + } + + require_once 'lib/GoogleAuthenticator.php'; + + $ga = new PHPGangsta_GoogleAuthenticator(); + + $dec_secret = auth_decrypt($enc_secret); + + if ($dec_secret === false) { + return false; + } + + if ($ga->verifyCode($dec_secret, $otp, 2)) { + return true; + } + + return false; +} + +/** + * Returns a hash containing implicit levels for the current authed level + * ex: will return array('janitor' => true, 'mod' => true) + * if the current level is 'mod' + */ +function get_level_map($level = null) { + global $auth, $levelorderf; + + $map = array(); + + if (!$level) { + $level = $auth['level']; + } + + if (!$level) { + return $map; + } + + $level_value = (int)$levelorderf[$level]; + + foreach ($levelorderf as $k => $v) { + if ($v <= $level_value) { + $map[$k] = true; + } + } + + return $map; +} + +function has_level( $level = 'mod', $board = false ) +{ + if( is_local_auth() ) return YES; + + global $auth, $levelorder, $levelorderf; + static $ourlevel = -1; + + + //if( !$board && defined( 'BOARD_DIR' ) ) $board = BOARD_DIR; + //if( !access_board($board) ) return false; + if( $ourlevel < 0 ) $ourlevel = $levelorderf[$auth['level']]; + + if (!isset($levelorderf[$level])) { + return false; + } + + if( $levelorderf[$level] <= $ourlevel ) return true; + + return false; +} + +function has_flag( $flag, $board = false ) +{ + if( is_local_auth() ) return YES; + + global $auth; + if( $auth['guest'] ) return false; + + if( !access_board( $board ) ) return false; + if( in_array( $flag, $auth['flags'] ) ) return true; + + return false; +} + +function access_board( $board ) +{ + if( is_local_auth() ) return YES; + + global $auth; + + if( $auth['guest'] ) return false; + + $can_do = false; + + // See if we have access to this board or all + if( in_array( 'all', $auth['allow'] ) || in_array( $board, $auth['allow'] ) ) $can_do = true; + + // Are we denied on this board? + if( $board && in_array( $board, $auth['deny'] ) ) $can_do = false; + + // If we're not using a board, are we denied for no-board stuff? + if( !$board && in_array( 'noboard', $auth['deny'] ) ) $can_do = false; + + return $can_do; +} + +function is_user() +{ + if( is_local_auth() ) return YES; + + global $auth; + if( $auth['guest'] ) return false; + if( $auth['level'] ) return true; + + return false; +} + +function auth_user($skip_agreement = false) { + global $auth; + + $user = $_COOKIE['4chan_auser']; + $pass = $_COOKIE['apass']; + + if( !$user || !$pass ) return false; + + $query = mysql_global_call("SELECT * FROM `%s` WHERE `username` = '%s' LIMIT 1", SQLLOGMOD, $user); + + if (!mysql_num_rows($query)) { + return false; + } + + $fetch = mysql_fetch_assoc($query); + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $hashed_admin_password = hash('sha256', $fetch['username'] . $fetch['password'] . $admin_salt); + + if ($hashed_admin_password !== $pass) { + return false; + } + + if ($fetch['password_expired'] == 1) { + die('Your password has expired; check IRC for instructions on changing it.'); + } + + if (!$skip_agreement) { + if ($fetch['signed_agreement'] == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php' && basename($_SERVER['SELF_PATH']) !== 'agreement_genkey.php') { + die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.'); + } + } + + $auth['level'] = $fetch['level']; + $auth['flags'] = explode( ',', $fetch['flags'] ); + $auth['allow'] = explode( ',', $fetch['allow'] ); + $auth['deny'] = explode( ',', $fetch['deny'] ); + $auth['guest'] = false; + + $flags = array(); + + if( has_level( 'admin' ) ) { + $flags['forcedanonname'] = 2; + } + + if( has_level( 'manager' ) || has_flag( 'html' ) ) { + $flags['html'] = 1; + } + + $flags = array_flip( $flags ); + $flags = implode( ',', $flags ); + + $ips_array = json_decode($fetch['ips'], true); + + if (json_last_error() !== JSON_ERROR_NONE) { + die('Database Error (1-0)'); + } + + $ips_array[$_SERVER['REMOTE_ADDR']] = $_SERVER['REQUEST_TIME']; + + if (count($ips_array) > 512) { + asort($ips_array); + array_shift($ips_array); + } + + $ips_array = json_encode($ips_array); + + if (json_last_error() !== JSON_ERROR_NONE) { + die('Database Error (1-1)'); + } + + if (mb_strlen($_SERVER['HTTP_USER_AGENT']) > 128) { + $ua = mb_substr($_SERVER['HTTP_USER_AGENT'], 0, 128); + } + else { + $ua = $_SERVER['HTTP_USER_AGENT']; + } + + mysql_global_call("UPDATE `%s` SET ips = '$ips_array', last_ua = '%s' WHERE id = %d LIMIT 1", SQLLOGMOD, $ua, $fetch['id']); + + return true; +} +// OLD auth +/* +function auth_user( $login = false ) +{ + global $auth; + + if( $login ) { + $user = $_POST['userlogin']; + $pass = $_POST['passlogin']; + } else { + $user = $_COOKIE['4chan_auser']; + $pass = $_COOKIE['4chan_apass']; + } + + if( !$user || !$pass ) return false; + + $query = mysql_global_call( "SELECT * FROM `%s` WHERE `username` = '%s' LIMIT 1", SQLLOGMOD, $user ); + if( !mysql_num_rows( $query ) ) return false; + $fetch = mysql_fetch_assoc( $query ); + + if( $fetch['password_expired'] == 1 ) { + die( 'Your password has expired; check IRC for instructions on changing it.' ); + } + + if ($login) { + if( !password_verify($pass, $fetch['password'])) return false; + + $pass = $fetch['password']; + } else { + if ($pass != $fetch['password']) return false; + } + + $auth['level'] = $fetch['level']; + $auth['flags'] = explode( ',', $fetch['flags'] ); + $auth['allow'] = explode( ',', $fetch['allow'] ); + $auth['deny'] = explode( ',', $fetch['deny'] ); + $auth['guest'] = false; + + $flags = array(); + + if( has_level( 'admin' ) && $user == 'moot' ) { + $flags['forcedanonname'] = 2; + } + + if( has_level( 'manager' ) || has_flag( 'html' ) ) { + $flags['html'] = 1; + } + + $flags = array_flip( $flags ); + $flags = implode( ',', $flags ); + + $ips_array = json_decode($fetch['ips'], true); + + if (json_last_error() !== JSON_ERROR_NONE) { + die('Database Error (1-0)'); + } + + $ips_array[$_SERVER['REMOTE_ADDR']] = $_SERVER['REQUEST_TIME']; + $ips_array = json_encode($ips_array); + + if (json_last_error() !== JSON_ERROR_NONE) { + die('Database Error (1-1)'); + } + + if ($login) { + $login_query = ", last_login = now()"; + } + else { + if (!isset($_COOKIE['apass'])) { + return false; + } + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $hashed_admin_cookie = $_COOKIE['apass']; + $hashed_admin_password = hash('sha256', $fetch['username'] . $fetch['password'] . $admin_salt); + + if ($hashed_admin_password !== $hashed_admin_cookie) { + return false; + } + + $login_query = ''; + } + + mysql_global_do("UPDATE `%s` SET ips = '$ips_array' $login_query WHERE id = %d", SQLLOGMOD, $fetch['id']); + + if( !isset( $_COOKIE['4chan_auser'] ) || !isset( $_COOKIE['4chan_apass'] ) ) { + if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) { + setcookie( "4chan_auser", $user, time() + 30 * 24 * 3600, "/", ".4chan.org", true, true ); + setcookie( "4chan_apass", $pass, time() + 30 * 24 * 3600, "/", ".4chan.org", true, true ); + setcookie( "4chan_aflags", $flags, time() + 30 * 24 * 3600, "/", ".4chan.org", true ); + + $jspath = $auth['level'] == 'janitor' ? JANITOR_JS_PATH : ADMIN_JS_PATH; + if( !isset( $_COOKIE['extra_path'] ) || !in_array( $_COOKIE['extra_path'], array(JANITOR_JS_PATH, ADMIN_JS_PATH) ) ) { + setcookie( 'extra_path', $jspath, time() + ( 30 * 24 * 3600 ), '/', '.4chan.org' ); + } + } elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) { + setcookie( "4chan_auser", $user, time() + 30 * 24 * 3600, "/", ".4channel.org", true, true ); + setcookie( "4chan_apass", $pass, time() + 30 * 24 * 3600, "/", ".4channel.org", true, true ); + } else { + die( 'Not 4chan.org' ); + } + + } + + return true; +} +*/ +function is_local_auth() +{ + if (!isset($_SERVER['REMOTE_ADDR'])) { + return true; + } + + // local rpc can do anything + $longip = ip2long( $_SERVER['REMOTE_ADDR'] ); + + if( + cidrtest( $longip, "10.0.0.0/24" ) || + cidrtest( $longip, "204.152.204.0/24" ) || + cidrtest( $longip, "127.0.0.0/24" ) + ) { + return YES; + } + + return false; +} + +function can_delete( $resno ) +{ + if( !has_level( 'janitor' ) ) return false; + if( has_level( 'janitor' ) && access_board( BOARD_DIR ) ) return true; + //if( !access_board(BOARD_DIR) ) return false; + + $query = mysql_global_do( "SELECT COUNT(*) from reports WHERE board='%s' AND no=%d AND cat=2", BOARD_DIR, $resno ); + $illegal_count = mysql_result( $query, 0, 0 ); + mysql_free_result( $query ); + + return $illegal_count >= 3; +} + +function start_auth_captcha($use_alt_captcha = false) +{ + if (valid_captcha_bypass() !== true) { + if ($use_alt_captcha) { + start_recaptcha_verify_alt(); + } + else { + start_recaptcha_verify(); + } + } +} + +function clear_pass_cookies() { + setcookie('pass_id', null, 1, '/', 'sys.4chan.org', true, true); + setcookie('pass_id', null, 1, '/', '.4chan.org', true, true); + setcookie('pass_enabled', null, 1, '/', '.4chan.org'); +} + +function valid_captcha_bypass() +{ + global $captcha_bypass, $passid, $rangeban_bypass; + + $captcha_bypass = false; + $rangeban_bypass = false; + + $passid = ''; + + if (is_local_auth() || has_level('janitor')) { + $captcha_bypass = true; + $rangeban_bypass = true; + return true; + } + + if (CAPTCHA != 1) { + $captcha_bypass = true; + } + + $time = $_SERVER['REQUEST_TIME']; + $host = $_SERVER['REMOTE_ADDR']; + + // check for 4chan pass + $pass_cookie = isset( $_COOKIE['pass_id'] ) ? $_COOKIE['pass_id'] : ''; + + if (strlen($pass_cookie) == 10) { + setcookie('pass_id', '0', 1, '/', '.4chan.org', true, true); + setcookie('pass_enabled', '0', 1, '/', '.4chan.org'); + error(S_PASSFORMATCHANGED); + } + + if ($pass_cookie) { + $pass_parts = explode('.', $pass_cookie); + + $pass_user = $pass_parts[0]; + $pass_session = $pass_parts[1]; + + if (!$pass_user || !$pass_session) { + error(S_INVALIDPASS); + } + + // The column is case insensitive but all passes should be uppercase to avoid ban bypassing exploits. + $pass_user = strtoupper($pass_user); + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $passq = mysql_global_call("SELECT user_hash, session_id, last_ip, last_used, last_country, status, pending_id, UNIX_TIMESTAMP(expiration_date) as expiration_date FROM pass_users WHERE pin != '' AND user_hash = '%s'", $pass_user); + + if( !$passq ) error( S_INVALIDPASS ); + + $res = mysql_fetch_assoc($passq); + + if (!$res || !$res['session_id']) { + clear_pass_cookies(); + error(S_INVALIDPASS); + } + + $hashed_pass_session = substr(hash('sha256', $res['session_id'] . $admin_salt), 0, 32); + + if ($hashed_pass_session !== $pass_session) { + clear_pass_cookies(); + error(S_INVALIDPASS); + } + + if ((int)$res['expiration_date'] <= $time) { + clear_pass_cookies(); + error(sprintf(S_PASSEXPIRED, $res['pending_id'])); + } + + if ($res['status'] != 0) { + clear_pass_cookies(); + error(S_PASSDISABLED); + } + + $lastused = strtotime( $res['last_used'] ); + $lastip_mask = ip2long( $res['last_ip'] ) & ( ~255 ); + $ip_mask = ip2long( $host ) & ( ~255 ); + + if( $lastip_mask !== 0 && ( $time - $lastused ) < PASS_TIMEOUT && $lastip_mask != $ip_mask ) { + + // old strict code, above is to match last octet + //if( ( $time - $lastused ) < PASS_TIMEOUT && $res['last_ip'] != $host && $res['last_ip'] != '0.0.0.0' ) { + clear_pass_cookies(); + error( S_PASSINUSE ); + } + + $update_country = ''; + + if ($res['last_ip'] !== $host) { + $geo_data = GeoIP2::get_country($host); + + if ($geo_data && isset($geo_data['country_code'])) { + $country_code = $geo_data['country_code']; + } + else { + $country_code = 'XX'; + } + + $update_country = ", last_country = '" . mysql_real_escape_string($country_code) . "'"; + } + + $passid = $pass_user; + + $captcha_bypass = true; + $rangeban_bypass = true; + + mysql_global_call( "UPDATE pass_users SET last_used = NOW(), last_ip = '%s' $update_country WHERE user_hash = '%s' AND status = 0 LIMIT 1", $host, $res['user_hash'], $host ); + } + + return $captcha_bypass; +} + +// some code paths might think current admin name is 4chan_auser cookie +// when that's not set (e.g. local requests), assert out here +function validate_admin_cookies() +{ + if (!$_COOKIE['4chan_auser']) { + error('Internal error (internal request missing name)'); + } +} diff --git a/lib/auth.php b/lib/auth.php new file mode 100644 index 0000000..7709302 --- /dev/null +++ b/lib/auth.php @@ -0,0 +1,594 @@ + false, + 'flags' => false, + 'allow' => false, + 'deny' => false, + 'guest' => true, +); + +$levelorder = array( + 1 => 'janitor', + 10 => 'mod', + 20 => 'manager', + 50 => 'admin' +); + +$levelorderf = array( + 'janitor' => 1, + 'mod' => 10, + 'manager' => 20, + 'admin' => 50 +); + +if (!defined('SQLLOGMOD')) { + define("SQLLOGMOD", "mod_users"); + define('PASS_TIMEOUT', 1800); + define('LOGIN_FAIL_HOURLY', 5); +} + +function csrf_tag() { + if (isset($_COOKIE['_tkn'])) { + return ''; + } + else { + return ''; + } +} + +function csrf_attr() { + if (isset($_COOKIE['_tkn'])) { + return 'data-tkn="' . htmlspecialchars($_COOKIE['_tkn'], ENT_QUOTES) . '"'; + } + else { + return ''; + } +} + +function auth_encrypt($data) { + $key = file_get_contents('/www/keys/2015_enc.key'); + + if (!$key) { + return false; + } + + $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND); + + $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv); + + if ($encrypted === false) { + return false; + } + + return $iv . $encrypted; +} + +function auth_decrypt($data) { + $key = file_get_contents('/www/keys/2015_enc.key'); + + if (!$key) { + return false; + } + + $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv_dec = substr($data, 0, $iv_size); + + $data = substr($data, $iv_size); + + $data = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv_dec); + + if ($data === false) { + return false; + } + + return rtrim($data, "\0"); +} + +function verify_one_time_pwd($username, $otp) { + if (!$otp) { + return false; + } + + $query = "SELECT auth_secret FROM mod_users WHERE username = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $username); + + if (!$res) { + return false; + } + + $enc_secret = mysql_fetch_row($res)[0]; + + if (!$enc_secret) { + return false; + } + + require_once 'lib/GoogleAuthenticator.php'; + + $ga = new PHPGangsta_GoogleAuthenticator(); + + $dec_secret = auth_decrypt($enc_secret); + + if ($dec_secret === false) { + return false; + } + + if ($ga->verifyCode($dec_secret, $otp, 2)) { + return true; + } + + return false; +} + +/** + * Returns a hash containing implicit levels for the current authed level + * ex: will return array('janitor' => true, 'mod' => true) + * if the current level is 'mod' + */ +function get_level_map($level = null) { + global $auth, $levelorderf; + + $map = array(); + + if (!$level) { + $level = $auth['level']; + } + + if (!$level) { + return $map; + } + + $level_value = (int)$levelorderf[$level]; + + foreach ($levelorderf as $k => $v) { + if ($v <= $level_value) { + $map[$k] = true; + } + } + + return $map; +} + +function has_level( $level = 'mod', $board = false ) +{ + if( is_local_auth() ) return YES; + + global $auth, $levelorder, $levelorderf; + static $ourlevel = -1; + + + //if( !$board && defined( 'BOARD_DIR' ) ) $board = BOARD_DIR; + //if( !access_board($board) ) return false; + if( $ourlevel < 0 ) $ourlevel = $levelorderf[$auth['level']]; + + if (!isset($levelorderf[$level])) { + return false; + } + + if( $levelorderf[$level] <= $ourlevel ) return true; + + return false; +} + +function has_flag( $flag, $board = false ) +{ + if( is_local_auth() ) return YES; + + global $auth; + if( $auth['guest'] ) return false; + + if( !access_board( $board ) ) return false; + if( in_array( $flag, $auth['flags'] ) ) return true; + + return false; +} + +function access_board( $board ) +{ + if( is_local_auth() ) return YES; + + global $auth; + + if( $auth['guest'] ) return false; + + $can_do = false; + + // See if we have access to this board or all + if( in_array( 'all', $auth['allow'] ) || in_array( $board, $auth['allow'] ) ) $can_do = true; + + // Are we denied on this board? + if( $board && in_array( $board, $auth['deny'] ) ) $can_do = false; + + // If we're not using a board, are we denied for no-board stuff? + if( !$board && in_array( 'noboard', $auth['deny'] ) ) $can_do = false; + + return $can_do; +} + +function is_user() +{ + if( is_local_auth() ) return YES; + + global $auth; + if( $auth['guest'] ) return false; + if( $auth['level'] ) return true; + + return false; +} + +function auth_user($skip_agreement = false) { + global $auth; + + $user = $_COOKIE['4chan_auser']; + $pass = $_COOKIE['apass']; + + if( !$user || !$pass ) return false; + + $query = mysql_global_call("SELECT * FROM `%s` WHERE `username` = '%s' LIMIT 1", SQLLOGMOD, $user); + + if (!mysql_num_rows($query)) { + return false; + } + + $fetch = mysql_fetch_assoc($query); + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $hashed_admin_password = hash('sha256', $fetch['username'] . $fetch['password'] . $admin_salt); + + if ($hashed_admin_password !== $pass) { + return false; + } + + if ($fetch['password_expired'] == 1) { + die('Your password has expired; check IRC for instructions on changing it.'); + } + + if (!$skip_agreement) { + if ($fetch['signed_agreement'] == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php' && basename($_SERVER['SELF_PATH']) !== 'agreement_genkey.php') { + die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.'); + } + } + + $auth['level'] = $fetch['level']; + $auth['flags'] = explode( ',', $fetch['flags'] ); + $auth['allow'] = explode( ',', $fetch['allow'] ); + $auth['deny'] = explode( ',', $fetch['deny'] ); + $auth['guest'] = false; + + $flags = array(); + + if( has_level( 'admin' ) ) { + $flags['forcedanonname'] = 2; + } + + if( has_level( 'manager' ) || has_flag( 'html' ) ) { + $flags['html'] = 1; + } + + $flags = array_flip( $flags ); + $flags = implode( ',', $flags ); + + $ips_array = json_decode($fetch['ips'], true); + + if (json_last_error() !== JSON_ERROR_NONE) { + die('Database Error (1-0)'); + } + + $ips_array[$_SERVER['REMOTE_ADDR']] = $_SERVER['REQUEST_TIME']; + + if (count($ips_array) > 512) { + asort($ips_array); + array_shift($ips_array); + } + + $ips_array = json_encode($ips_array); + + if (json_last_error() !== JSON_ERROR_NONE) { + die('Database Error (1-1)'); + } + + if (mb_strlen($_SERVER['HTTP_USER_AGENT']) > 128) { + $ua = mb_substr($_SERVER['HTTP_USER_AGENT'], 0, 128); + } + else { + $ua = $_SERVER['HTTP_USER_AGENT']; + } + + mysql_global_call("UPDATE `%s` SET ips = '$ips_array', last_ua = '%s' WHERE id = %d LIMIT 1", SQLLOGMOD, $ua, $fetch['id']); + + return true; +} +// OLD auth +/* +function auth_user( $login = false ) +{ + global $auth; + + if( $login ) { + $user = $_POST['userlogin']; + $pass = $_POST['passlogin']; + } else { + $user = $_COOKIE['4chan_auser']; + $pass = $_COOKIE['4chan_apass']; + } + + if( !$user || !$pass ) return false; + + $query = mysql_global_call( "SELECT * FROM `%s` WHERE `username` = '%s' LIMIT 1", SQLLOGMOD, $user ); + if( !mysql_num_rows( $query ) ) return false; + $fetch = mysql_fetch_assoc( $query ); + + if( $fetch['password_expired'] == 1 ) { + die( 'Your password has expired; check IRC for instructions on changing it.' ); + } + + if ($login) { + if( !password_verify($pass, $fetch['password'])) return false; + + $pass = $fetch['password']; + } else { + if ($pass != $fetch['password']) return false; + } + + $auth['level'] = $fetch['level']; + $auth['flags'] = explode( ',', $fetch['flags'] ); + $auth['allow'] = explode( ',', $fetch['allow'] ); + $auth['deny'] = explode( ',', $fetch['deny'] ); + $auth['guest'] = false; + + $flags = array(); + + if( has_level( 'admin' ) && $user == 'moot' ) { + $flags['forcedanonname'] = 2; + } + + if( has_level( 'manager' ) || has_flag( 'html' ) ) { + $flags['html'] = 1; + } + + $flags = array_flip( $flags ); + $flags = implode( ',', $flags ); + + $ips_array = json_decode($fetch['ips'], true); + + if (json_last_error() !== JSON_ERROR_NONE) { + die('Database Error (1-0)'); + } + + $ips_array[$_SERVER['REMOTE_ADDR']] = $_SERVER['REQUEST_TIME']; + $ips_array = json_encode($ips_array); + + if (json_last_error() !== JSON_ERROR_NONE) { + die('Database Error (1-1)'); + } + + if ($login) { + $login_query = ", last_login = now()"; + } + else { + if (!isset($_COOKIE['apass'])) { + return false; + } + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $hashed_admin_cookie = $_COOKIE['apass']; + $hashed_admin_password = hash('sha256', $fetch['username'] . $fetch['password'] . $admin_salt); + + if ($hashed_admin_password !== $hashed_admin_cookie) { + return false; + } + + $login_query = ''; + } + + mysql_global_do("UPDATE `%s` SET ips = '$ips_array' $login_query WHERE id = %d", SQLLOGMOD, $fetch['id']); + + if( !isset( $_COOKIE['4chan_auser'] ) || !isset( $_COOKIE['4chan_apass'] ) ) { + if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) { + setcookie( "4chan_auser", $user, time() + 30 * 24 * 3600, "/", ".4chan.org", true, true ); + setcookie( "4chan_apass", $pass, time() + 30 * 24 * 3600, "/", ".4chan.org", true, true ); + setcookie( "4chan_aflags", $flags, time() + 30 * 24 * 3600, "/", ".4chan.org", true ); + + $jspath = $auth['level'] == 'janitor' ? JANITOR_JS_PATH : ADMIN_JS_PATH; + if( !isset( $_COOKIE['extra_path'] ) || !in_array( $_COOKIE['extra_path'], array(JANITOR_JS_PATH, ADMIN_JS_PATH) ) ) { + setcookie( 'extra_path', $jspath, time() + ( 30 * 24 * 3600 ), '/', '.4chan.org' ); + } + } elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) { + setcookie( "4chan_auser", $user, time() + 30 * 24 * 3600, "/", ".4channel.org", true, true ); + setcookie( "4chan_apass", $pass, time() + 30 * 24 * 3600, "/", ".4channel.org", true, true ); + } else { + die( 'Not 4chan.org' ); + } + + } + + return true; +} +*/ +function is_local_auth() +{ + if (!isset($_SERVER['REMOTE_ADDR'])) { + return true; + } + + // local rpc can do anything + $longip = ip2long( $_SERVER['REMOTE_ADDR'] ); + + if( + cidrtest( $longip, "10.0.0.0/24" ) || + cidrtest( $longip, "204.152.204.0/24" ) || + cidrtest( $longip, "127.0.0.0/24" ) + ) { + return YES; + } + + return false; +} + +function can_delete( $resno ) +{ + if( !has_level( 'janitor' ) ) return false; + if( has_level( 'janitor' ) && access_board( BOARD_DIR ) ) return true; + //if( !access_board(BOARD_DIR) ) return false; + + $query = mysql_global_do( "SELECT COUNT(*) from reports WHERE board='%s' AND no=%d AND cat=2", BOARD_DIR, $resno ); + $illegal_count = mysql_result( $query, 0, 0 ); + mysql_free_result( $query ); + + return $illegal_count >= 3; +} + +function start_auth_captcha($use_alt_captcha = false) +{ + if (valid_captcha_bypass() !== true) { + if ($use_alt_captcha) { + start_recaptcha_verify_alt(); + } + else { + start_recaptcha_verify(); + } + } +} + +function clear_pass_cookies() { + setcookie('pass_id', null, 1, '/', 'sys.4chan.org', true, true); + setcookie('pass_id', null, 1, '/', '.4chan.org', true, true); + setcookie('pass_enabled', null, 1, '/', '.4chan.org'); +} + +function valid_captcha_bypass() +{ + global $captcha_bypass, $passid, $rangeban_bypass; + + $captcha_bypass = false; + $rangeban_bypass = false; + + $passid = ''; + + if (is_local_auth() || has_level('janitor')) { + $captcha_bypass = true; + $rangeban_bypass = true; + return true; + } + + if (CAPTCHA != 1) { + $captcha_bypass = true; + } + + $time = $_SERVER['REQUEST_TIME']; + $host = $_SERVER['REMOTE_ADDR']; + + // check for 4chan pass + $pass_cookie = isset( $_COOKIE['pass_id'] ) ? $_COOKIE['pass_id'] : ''; + + if (strlen($pass_cookie) == 10) { + setcookie('pass_id', '0', 1, '/', '.4chan.org', true, true); + setcookie('pass_enabled', '0', 1, '/', '.4chan.org'); + error(S_PASSFORMATCHANGED); + } + + if ($pass_cookie) { + $pass_parts = explode('.', $pass_cookie); + + $pass_user = $pass_parts[0]; + $pass_session = $pass_parts[1]; + + if (!$pass_user || !$pass_session) { + error(S_INVALIDPASS); + } + + // The column is case insensitive but all passes should be uppercase to avoid ban bypassing exploits. + $pass_user = strtoupper($pass_user); + + $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); + + if (!$admin_salt) { + die('Internal Server Error (s0)'); + } + + $passq = mysql_global_call("SELECT user_hash, session_id, last_ip, last_used, last_country, status, pending_id, UNIX_TIMESTAMP(expiration_date) as expiration_date FROM pass_users WHERE pin != '' AND user_hash = '%s'", $pass_user); + + if( !$passq ) error( S_INVALIDPASS ); + + $res = mysql_fetch_assoc($passq); + + if (!$res || !$res['session_id']) { + clear_pass_cookies(); + error(S_INVALIDPASS); + } + + $hashed_pass_session = substr(hash('sha256', $res['session_id'] . $admin_salt), 0, 32); + + if ($hashed_pass_session !== $pass_session) { + clear_pass_cookies(); + error(S_INVALIDPASS); + } + + if ((int)$res['expiration_date'] <= $time) { + clear_pass_cookies(); + error(sprintf(S_PASSEXPIRED, $res['pending_id'])); + } + + if ($res['status'] != 0) { + clear_pass_cookies(); + error(S_PASSDISABLED); + } + + $lastused = strtotime( $res['last_used'] ); + $lastip_mask = ip2long( $res['last_ip'] ) & ( ~255 ); + $ip_mask = ip2long( $host ) & ( ~255 ); + + if( $lastip_mask !== 0 && ( $time - $lastused ) < PASS_TIMEOUT && $lastip_mask != $ip_mask ) { + + // old strict code, above is to match last octet + //if( ( $time - $lastused ) < PASS_TIMEOUT && $res['last_ip'] != $host && $res['last_ip'] != '0.0.0.0' ) { + clear_pass_cookies(); + error( S_PASSINUSE ); + } + + $update_country = ''; + + if ($res['last_ip'] !== $host) { + $geo_data = GeoIP2::get_country($host); + + if ($geo_data && isset($geo_data['country_code'])) { + $country_code = $geo_data['country_code']; + } + else { + $country_code = 'XX'; + } + + $update_country = ", last_country = '" . mysql_real_escape_string($country_code) . "'"; + } + + $passid = $pass_user; + + $captcha_bypass = true; + $rangeban_bypass = true; + + mysql_global_call( "UPDATE pass_users SET last_used = NOW(), last_ip = '%s' $update_country WHERE user_hash = '%s' AND status = 0 LIMIT 1", $host, $res['user_hash'], $host ); + } + + return $captcha_bypass; +} + +// some code paths might think current admin name is 4chan_auser cookie +// when that's not set (e.g. local requests), assert out here +function validate_admin_cookies() +{ + if (!$_COOKIE['4chan_auser']) { + error('Internal error (internal request missing name)'); + } +} diff --git a/lib/board_flags_lgbt.php b/lib/board_flags_lgbt.php new file mode 100644 index 0000000..be52e32 --- /dev/null +++ b/lib/board_flags_lgbt.php @@ -0,0 +1,132 @@ + 'AAP', + 'ACE' => 'Asexual', + 'ACH' => 'Achillean', + 'AFB' => 'AFAB', + 'AGP' => 'AGP', + 'AGR' => 'Agender', + 'ALL' => 'LGBT', + 'ALY' => 'Ally', + 'AMB' => 'AMAB', + 'AND' => 'Androgynous', + 'ARO' => 'Aromantic', + 'BCH' => 'Butch', + 'BI' => 'Bisexual', + 'BOY' => 'Boymoder', + 'BR' => 'Bear', + 'CHR' => 'Chaser', + 'CIS' => 'Cis', + 'DOM' => 'Dom', + 'DRO' => 'Demiromantic', + 'DSX' => 'Demisexual', + 'FBY' => 'Femboy', + 'FFB' => 'FtM Femboy', + 'FR' => 'FtM Repressor', + 'GAY' => 'Gay', + 'GFL' => 'Genderfluid', + 'GQR' => 'Genderqueer', + 'HFB' => 'HRT Femboy', + 'HON' => 'Hon', + 'HST' => 'HSTS', + 'INT' => 'Intersex', + 'LAB' => 'Labrys', + 'LES' => 'Lesbian', + 'MBT' => 'MtF Butch', + 'MR' => 'MtF Repressor', + 'NB' => 'Nonbinary', + 'OG' => 'Original', + 'PAN' => 'Pansexual', + 'PBI' => 'Prison Bi', + 'PG' => 'Prison Gay', + 'PLY' => 'Poly', + 'PNR' => 'Pooner', + 'PRG' => 'Progress', + 'QES' => 'Questioning', + 'QR' => 'Queer', + 'REP' => 'Repressor', + 'SPH' => 'Sapphic', + 'STR' => 'Straight', + 'SUB' => 'Sub', + 'SW' => 'Switch', + 'TF' => 'Transfem', + 'TKH' => 'Twinkhon', + 'TMA' => 'Transmasc', + 'TNK' => 'Twink', + 'TRN' => 'Transgender', + 'UKR' => 'Woke' + ); + + return $board_flags; +} + +// Flag names as they appear in the selection menu +function get_board_flags_selector() { + static $board_flags = array( + 'AAP' => 'AAP', + 'ACE' => 'Asexual', + 'ACH' => 'Achillean', + 'AFB' => 'AFAB', + 'AGP' => 'AGP', + 'AGR' => 'Agender', + 'ALL' => 'LGBT', + 'ALY' => 'Ally', + 'AMB' => 'AMAB', + 'AND' => 'Androgynous', + 'ARO' => 'Aromantic', + 'BCH' => 'Butch', + 'BI' => 'Bisexual', + 'BOY' => 'Boymoder', + 'BR' => 'Bear', + 'CHR' => 'Chaser', + 'CIS' => 'Cis', + 'DOM' => 'Dom', + 'DRO' => 'Demiromantic', + 'DSX' => 'Demisexual', + 'FBY' => 'Femboy', + 'FFB' => 'FtM Femboy', + 'FR' => 'FtM Repressor', + 'GAY' => 'Gay', + 'GFL' => 'Genderfluid', + 'GQR' => 'Genderqueer', + 'HFB' => 'HRT Femboy', + 'HON' => 'Hon', + 'HST' => 'HSTS', + 'INT' => 'Intersex', + 'LAB' => 'Labrys', + 'LES' => 'Lesbian', + 'MBT' => 'MtF Butch', + 'MR' => 'MtF Repressor', + 'NB' => 'Nonbinary', + 'OG' => 'Original', + 'PAN' => 'Pansexual', + 'PBI' => 'Prison Bi', + 'PG' => 'Prison Gay', + 'PLY' => 'Poly', + 'PNR' => 'Pooner', + 'PRG' => 'Progress', + 'QES' => 'Questioning', + 'QR' => 'Queer', + 'REP' => 'Repressor', + 'SPH' => 'Sapphic', + 'STR' => 'Straight', + 'SUB' => 'Sub', + 'SW' => 'Switch', + 'TF' => 'Transfem', + 'TKH' => 'Twinkhon', + 'TMA' => 'Transmasc', + 'TNK' => 'Twink', + 'TRN' => 'Transgender', + 'UKR' => 'Woke' + ); + + return $board_flags; +} + +function board_flag_code_to_name($code) { + $board_flags = get_board_flags_array(); + return isset($board_flags[$code]) ? $board_flags[$code] : 'None'; +} diff --git a/lib/board_flags_mlp.php b/lib/board_flags_mlp.php new file mode 100644 index 0000000..9494fbc --- /dev/null +++ b/lib/board_flags_mlp.php @@ -0,0 +1,102 @@ + '4cc /mlp/', + 'ADA' => 'Adagio Dazzle', + 'AN' => 'Anon', + 'ANF' => 'Anonfilly', + 'APB' => 'Apple Bloom', + 'AJ' => 'Applejack', + 'AB' => 'Aria Blaze', + 'AU' => 'Autumn Blaze', + 'BB' => 'Bon Bon', + 'BM' => 'Big Mac', + 'BP' => 'Berry Punch', + 'BS' => 'Babs Seed', + 'CL' => 'Changeling', + 'CO' => 'Coco Pommel', + 'CG' => 'Cozy Glow', + 'CHE' => 'Cheerilee', + 'CB' => 'Cherry Berry', + 'DAY' => 'Daybreaker', + 'DD' => 'Daring Do', + 'DER' => 'Derpy Hooves', + 'DT' => 'Diamond Tiara', + 'DIS' => 'Discord', + 'EQA' => 'EqG Applejack', + 'EQF' => 'EqG Fluttershy', + 'EQP' => 'EqG Pinkie Pie', + 'EQR' => 'EqG Rainbow Dash', + 'EQT' => 'EqG Trixie', + 'EQI' => 'EqG Twilight Sparkle', + 'EQS' => 'EqG Sunset Shimmer', + 'ERA' => 'EqG Rarity', + 'FAU' => 'Fausticorn', + 'FLE' => 'Fleur de lis', + 'FL' => 'Fluttershy', + 'GI' => 'Gilda', + 'HT' => 'Hitch Trailblazer', + 'IZ' => 'Izzy Moonbow', + 'LI' => 'Limestone', + 'LT' => 'Lord Tirek', + 'LY' => 'Lyra Heartstrings', + 'MA' => 'Marble', + 'MAU' => 'Maud', + 'MIN' => 'Minuette', + 'NI' => 'Nightmare Moon', + 'NUR' => 'Nurse Redheart', + 'OCT' => 'Octavia', + 'PAR' => 'Parasprite', + 'PC' => 'Princess Cadance', + 'PCE' => 'Princess Celestia', + 'PI' => 'Pinkie Pie', + 'PLU' => 'Princess Luna', + 'PM' => 'Pinkamena', + 'PP' => 'Pipp Petals', + 'QC' => 'Queen Chrysalis', + 'RAR' => 'Rarity', + 'RD' => 'Rainbow Dash', + 'RLU' => 'Roseluck', + 'S1L' => 'S1 Luna', + 'SCO' => 'Scootaloo', + 'SHI' => 'Shining Armor', + 'SIL' => 'Silver Spoon', + 'SON' => 'Sonata Dusk', + 'SP' => 'Spike', + 'SPI' => 'Spitfire', + 'SS' => 'Sunny Starscout', + 'STA' => 'Star Dancer', + 'STL' => 'Starlight Glimmer', + 'SPT' => 'Sprout', + 'SUN' => 'Sunburst', + 'SUS' => 'Sunset Shimmer', + 'SWB' => 'Sweetie Belle', + 'TFA' => 'TFH Arizona', + 'TFO' => 'TFH Oleander', + 'TFP' => 'TFH Paprika', + 'TFS' => 'TFH Shanty', + 'TFT' => 'TFH Tianhuo', + 'TFV' => 'TFH Velvet', + 'TP' => 'TFH Pom', + 'TS' => 'Tempest Shadow', + 'TWI' => 'Twilight Sparkle', + 'TX' => 'Trixie', + 'VS' => 'Vinyl Scratch', + 'ZE' => 'Zecora', + 'ZS' => 'Zipp Storm' + ); + + return $board_flags; +} + +// Flag names as they appear in the selection menu +function get_board_flags_selector() { + return get_board_flags_array(); +} + +function board_flag_code_to_name($code) { + $board_flags = get_board_flags_array(); + return isset($board_flags[$code]) ? $board_flags[$code] : 'None'; +} diff --git a/lib/board_flags_pol.php b/lib/board_flags_pol.php new file mode 100644 index 0000000..8973332 --- /dev/null +++ b/lib/board_flags_pol.php @@ -0,0 +1,72 @@ + 'Anarcho-Capitalist', + 'AN' => 'Anarchist', + 'BL' => 'Black Lives Matter', + 'CF' => 'Confederate', + 'CM' => 'Commie', + 'CT' => 'Catalonia', + 'DM' => 'Democrat', + 'EU' => 'European', + 'FC' => 'Fascist', + 'GN' => 'Gadsden', + 'GY' => 'LGBT', + 'JH' => 'Jihadi', + 'KN' => 'Kekistani', + 'MF' => 'Muslim', + 'NB' => 'National Bolshevik', + 'NT' => 'NATO', + 'NZ' => 'Nazi', + 'PC' => 'Hippie', + 'PR' => 'Pirate', + 'RE' => 'Republican', + 'TM' => 'DEUS VULT', + 'MZ' => 'Task Force Z', + 'TR' => 'Tree Hugger', + 'UN' => 'United Nations', + 'WP' => 'White Supremacist' + ); + + return $board_flags; +} + +// Flag names as they appear in the selection menu +function get_board_flags_selector() { + static $board_flags = array( + 'AC' => 'Anarcho-Capitalist', + 'AN' => 'Anarchist', + 'BL' => 'Black Nationalist', + 'CF' => 'Confederate', + 'CM' => 'Communist', + 'CT' => 'Catalonia', + 'DM' => 'Democrat', + 'EU' => 'European', + 'FC' => 'Fascist', + 'GN' => 'Gadsden', + 'GY' => 'Gay', + 'JH' => 'Jihadi', + 'KN' => 'Kekistani', + 'MF' => 'Muslim', + 'NB' => 'National Bolshevik', + 'NT' => 'NATO', + 'NZ' => 'Nazi', + 'PC' => 'Hippie', + 'PR' => 'Pirate', + 'RE' => 'Republican', + 'MZ' => 'Task Force Z', + 'TM' => 'Templar', + 'TR' => 'Tree Hugger', + 'UN' => 'United Nations', + 'WP' => 'White Supremacist' + ); + + return $board_flags; +} + +function board_flag_code_to_name($code) { + $board_flags = get_board_flags_array(); + return isset($board_flags[$code]) ? $board_flags[$code] : 'None'; +} diff --git a/lib/board_flags_test.php b/lib/board_flags_test.php new file mode 100644 index 0000000..4cfdbc7 --- /dev/null +++ b/lib/board_flags_test.php @@ -0,0 +1,26 @@ + 'Flag 1', + 'FL2' => 'Flag 2' + ); + + return $board_flags; +} + +// Flag names as they appear in the selection menu +function get_board_flags_selector() { + static $board_flags = array( + 'FL1' => 'Flag 1', + 'FL2' => 'Flag 2' + ); + + return $board_flags; +} + +function board_flag_code_to_name($code) { + $board_flags = get_board_flags_array(); + return isset($board_flags[$code]) ? $board_flags[$code] : 'None'; +} diff --git a/lib/captcha-test.php b/lib/captcha-test.php new file mode 100644 index 0000000..2c2aa46 --- /dev/null +++ b/lib/captcha-test.php @@ -0,0 +1,686 @@ + 24) { + return false; + } + + if (strlen($_POST['t-challenge']) > 255) { + return false; + } + + // User agent + if (!isset($_SERVER['HTTP_USER_AGENT'])) { + $user_agent = '!'; + } + else { + $user_agent = md5($_SERVER['HTTP_USER_AGENT']); + } + + // Password + $password = '!'; + + if ($userpwd && !$userpwd->isNew()) { + $password = $userpwd->getPwd(); + } + + // --- + + list($uniq_id, $challenge_hash) = explode('.', $_POST['t-challenge']); + + $long_ip = ip2long($ip); + + if (!$uniq_id || !$challenge_hash || !$long_ip) { + return false; + } + + $challenge_key = "ch$long_ip"; + + $params = [$ip, $password, $user_agent, $board, $thread_id]; + + $response = TwisterCaptcha::normalizeReponseStr($_POST['t-response']); + + $is_valid = TwisterCaptcha::verifyChallengeHash($challenge_hash, $uniq_id, $response, $params); + + if ($is_valid) { + $active_uniq_id = $memcached->get($challenge_key); + + // Delete challenge + $memcached->delete($challenge_key); + + if (!$active_uniq_id || $uniq_id !== $active_uniq_id) { + return false; + } + + // Return and decrement the unsolved session count + $us = decrement_twister_captcha_session($memcached, $ip, $unsolved_count !== null); + + if ($unsolved_count !== null) { + $unsolved_count = $us; + } + + return true; + } + + // Delete challenge + $memcached->delete($challenge_key); + + return false; +} + +// Decrements the unsolved count by 2 and returns the old count +function decrement_twister_captcha_session($memcached, $ip, $return_old = true) { + if (!$memcached) { + return false; + } + + $long_ip = ip2long($ip); + + if (!$long_ip) { + return false; + } + + $key = "us$long_ip"; + + if ($return_old) { + $val = $memcached->get($key); + + if ($val === false) { + $val = 0; + } + } + else { + $val = 0; + } + + $memcached->decrement($key, 2); + + return $val; +} + +// FIXME: The IP arg isn't used for now +function set_twister_captcha_credits($memcached, $ip, $userpwd, $current_time) { + if (!$memcached || !$userpwd) { + return false; + } + + $current_time = (int)$current_time; + + if ($current_time <= 0) { + return false; + } + + //$long_ip = ip2long($ip); + + //if (!$long_ip) { + //return false; + //} + + $credits = 0; + + // Config + // Stage 1 should match the check in use_twister_captcha_credit() + // and captcha.php for optimisation purposes + + // Stage 1 + $noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes) + $noop_post_count_1 = 5; // required post count + $noop_credits_1 = 1; // credits given + $noop_duration_1 = 3600; // duration of the credits (1 hour, in seconds) + + // Stage 2 + $noop_known_ttl_2 = 21600; // 15 days + $noop_post_count_2 = 20; + $noop_credits_2 = 2; + $noop_duration_2 = 7200; // 2 hours + + // Stage 3 + $noop_known_ttl_3 = 129600; // 90 days + $noop_post_count_3 = 100; + $noop_credits_3 = 3; + $noop_duration_3 = 10800; // 3 hours + + // --- + + // The IP changed too recently + if ($userpwd->ipLifetime() < 60) { + return false; + } + + // Stage 3 + if ($userpwd->isUserKnown($noop_known_ttl_3) && $userpwd->postCount() >= $noop_post_count_3) { + $credits = $noop_credits_3; + $duration = $noop_duration_3; + } + // Stage 2 + else if ($userpwd->isUserKnown($noop_known_ttl_2) && $userpwd->postCount() >= $noop_post_count_2) { + $credits = $noop_credits_2; + $duration = $noop_duration_2; + } + // Stage 1 + else if ($userpwd->isUserKnown($noop_known_ttl_1) && $userpwd->postCount() >= $noop_post_count_1) { + $credits = $noop_credits_1; + $duration = $noop_duration_1; + } + else { + return false; + } + + if (!$credits || $credits > 3) { + return false; + } + + $expiration_ts = $current_time + $duration; + + // Require no more than 5 actions in the past 8 minutes + /* + $query = <<= DATE_SUB(NOW(), INTERVAL 8 MINUTE) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count > 5) { + return false; + } + */ + // Set credits + + $pwd = $userpwd->getPwd(); + + if (!$pwd) { + return false; + } + + $key = "cr-$pwd"; + $val = "$credits.$expiration_ts"; + + $res = $memcached->replace($key, $val, $expiration_ts); + + if ($res === false) { + if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) { + return $memcached->set($key, $val, $expiration_ts); + } + else { + return false; + } + } + + return true; +} + +// FIXME: The IP arg isn't used for now +function use_twister_captcha_credit($memcached, $ip, $userpwd) { + if (!$memcached || !$userpwd) { + return false; + } + + //$long_ip = ip2long($ip); + + //if (!$long_ip) { + //return false; + //} + + // Must match the check in set_twister_captcha_credits() + $noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes) + $noop_post_count_1 = 5; // required post count + + if (!$userpwd->isUserKnown($noop_known_ttl_1) || $userpwd->postCount() < $noop_post_count_1) { + return false; + } + + $pwd = $userpwd->getPwd(); + + if (!$pwd) { + return false; + } + + $key = "cr-$pwd"; + $credits = $memcached->get($key); + + if ($credits === false) { + return false; + } + + list($count, $ts) = explode('.', $credits); + + $count = (int)$count; + $ts = (int)$ts; + + // No credits left + if ($count <= 0 || $ts <= 0) { + $memcached->delete($key); + return false; + } + + $count -= 1; + + $res = $memcached->replace($key, "$count.$ts", $ts); + + if ($res === false && $memcached->getResultCode() !== Memcached::RES_NOTSTORED) { + return false; + } + + return true; +} + +function twister_captcha_form() { + return '
    '; +} + +function log_failed_captcha($ip, $userpwd, $board, $thread_id, $is_quiet, $meta = null) { + $data = [ + 'board' => $board, + 'thread_id' => $thread_id, + ]; + + if ($userpwd) { + $data['arg_num'] = $userpwd->pwdLifetime(); + $data['pwd'] = $userpwd->getPwd(); + } + + if ($meta) { + $data['meta'] = $meta; + } + + if ($is_quiet) { + $type = 'failed_captcha_quiet'; + } + else { + $type = 'failed_captcha'; + } + + write_to_event_log($type, $ip, $data); +} + +function h_captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) { + global $hcaptcha_public_key; + + $js_tag = ''; + + if ($autoload) { + $attrs = ' class="h-captcha" data-sitekey="' . $hcaptcha_public_key . '"'; + + if ($dark) { + $attrs .= ' data-theme="dark"'; + } + } + else { + $attrs = ''; + } + + $container_tag = '
    '; + + return $js_tag.$container_tag; +} + +// Moves css out of the form for html validation +function captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) { + global $recaptcha_public_key; + + $js_tag = ''; + + if ($autoload) { + $attrs = ' class="g-recaptcha" data-sitekey="' . $recaptcha_public_key . '"'; + + if ($dark) { + $attrs .= ' data-theme="dark"'; + } + } + else { + $attrs = ''; + } + + $container_tag = '
    '; + + $noscript_tag =<< +
    +
    +
    + +
    +
    + +
    +
    +
    + +HTML; + + if (defined('NOSCRIPT_CAPTCHA_ONLY') && NOSCRIPT_CAPTCHA_ONLY == 1) { + return $container_tag.$noscript_tag; + } + + return $js_tag.$container_tag.$noscript_tag; +} + +// Legacy captcha +// Uses recaptcha v2 for noscript captcha as the v1 seems to be broken currently. +function captcha_form_alt() { + global $recaptcha_public_key; + + $html = <<
    + + +HTML; + + return $html; +} + +function recaptcha_ban($n, $time, $return_error = 0, $length = 1) +{ + auto_ban_poster($name, $length, 1, "failed verification $n times per $time", "Possible spambot; repeatedly sent incorrect CAPTCHA verification."); + if( $return_error == 1 ) { + return S_GENERICERROR; + } + error(S_GENERICERROR); +} + +/** + * Works for both recaptcha and hcaptcha + */ +function recaptcha_bad_captcha($return_error = false, $codes = null) { + $error = S_BADCAPTCHA; + + if (is_array($codes)) { + if (in_array('missing-input-response', $codes)) { + $error = S_NOCAPTCHA; + } + + if ($return_error) { + return $error; + } + else { + error($error); + } + } + else { + if ($return_error) { + return $error; + } + else { + error($error); + } + } +} + +// ----------- +// hCaptcha +// ----------- +function start_hcaptcha_verify($return_error = false) { + global $hcaptcha_private_key, $hcaptcha_ch; + + $response = $_POST["h-captcha-response"]; + + if (!$response) { + if ($return_error == false) { + error(S_NOCAPTCHA); + } + return S_NOCAPTCHA; + } + + $response = urlencode($response); + + $rlen = strlen($response); + + if ($rlen > 32768) { + return recaptcha_bad_captcha($return_error); + } + + $api_url = 'https://hcaptcha.com/siteverify'; + + $post = array( + 'secret' => $hcaptcha_private_key, + 'response' => $response + ); + + $hcaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false); +} + +function end_hcaptcha_verify($return_error = false) { + global $hcaptcha_ch; + + if (!$hcaptcha_ch) { + return; + } + + $ret = rpc_finish_request($hcaptcha_ch, $error, $httperror); + + // BAD + // 413 Request Too Large is bad; it was caused intentionally by the user. + if ($httperror == 413) { + return recaptcha_bad_captcha($return_error); + } + + // BAD + if ($ret == null) { + return recaptcha_bad_captcha($return_error); + } + + $resp = json_decode($ret, true); + + // BAD + // Malformed JSON response from Google + if (json_last_error() !== JSON_ERROR_NONE) { + return recaptcha_bad_captcha($return_error); + } + + // GOOD + if ($resp['success']) { + return $resp; + } + + // BAD + return recaptcha_bad_captcha($return_error, $resp['error-codes']); +} + +// ----------- +// reCaptcha V2 +// ----------- +// FIXME $challenge_field is no longer used +function start_recaptcha_verify($return_error = false, $challenge_field = '') { + global $recaptcha_private_key, $recaptcha_ch; + + $response = $_POST["g-recaptcha-response"]; + + if (!$response) { + if ($return_error == false) { + error(S_NOCAPTCHA); + } + return S_NOCAPTCHA; + } + + $response = urlencode($response); + + $rlen = strlen($response); + + if ($rlen > 4096) { + return recaptcha_bad_captcha($return_error); + } + + $api_url = 'https://www.google.com/recaptcha/api/siteverify'; + + $post = array( + 'secret' => $recaptcha_private_key, + 'response' => $response + ); + + $recaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false); +} + +function end_recaptcha_verify($return_error = false) { + global $recaptcha_ch; + + if (!$recaptcha_ch) { + return; + } + + $ret = rpc_finish_request($recaptcha_ch, $error, $httperror); + + // BAD + // 413 Request Too Large is bad; it was caused intentionally by the user. + if ($httperror == 413) { + return recaptcha_bad_captcha($return_error); + } + + // BAD + if ($ret == null) { + return recaptcha_bad_captcha($return_error); + } + + $resp = json_decode($ret, true); + + // BAD + // Malformed JSON response from Google + if (json_last_error() !== JSON_ERROR_NONE) { + return recaptcha_bad_captcha($return_error); + } + + // GOOD + if ($resp['success']) { + return $resp; + } + + // BAD + return recaptcha_bad_captcha($return_error, $resp['error-codes']); +} + +// ----------- +// reCaptcha V1 +// ----------- +function start_recaptcha_verify_alt($return_error = false, $challenge_field = '') { + global $recaptcha_private_key, $recaptcha_ch; + + $challenge = ( $challenge_field == '' ) ? $_POST["recaptcha_challenge_field"] : $challenge_field; + $response = $_POST["recaptcha_response_field"]; + if (!$challenge || !$response) { + if( $return_error == false ) { + error(S_NOCAPTCHA); + } + return S_NOCAPTCHA; + } + + $num_words = 1 + preg_match_all('/\\s/', $response); + $rlen = strlen($response); + if ($num_words > 3 || $rlen > 128) { + return recaptcha_bad_captcha($return_error); + } + + $post = array( + "privatekey" => $recaptcha_private_key, + "challenge" => $challenge, + "remoteip" => $_SERVER["REMOTE_ADDR"], + "response" => $response + ); + + $recaptcha_ch = rpc_start_request("https://www.google.com/recaptcha/api/verify", $post, null, false); +} + +function end_recaptcha_verify_alt($return_error = false) { + global $recaptcha_ch; + + if (!$recaptcha_ch) return; + + $ret = rpc_finish_request($recaptcha_ch, $error, $httperror); + + if ($httperror == 413) { + return recaptcha_bad_captcha($return_error); + } + + if ($ret) { + $lines = explode("\n", $ret); + if ($lines[0] === "true") { + // GOOD + return; + } + } + + // BAD + return recaptcha_bad_captcha($return_error); +} + +?> diff --git a/lib/captcha.php b/lib/captcha.php new file mode 100644 index 0000000..2c2aa46 --- /dev/null +++ b/lib/captcha.php @@ -0,0 +1,686 @@ + 24) { + return false; + } + + if (strlen($_POST['t-challenge']) > 255) { + return false; + } + + // User agent + if (!isset($_SERVER['HTTP_USER_AGENT'])) { + $user_agent = '!'; + } + else { + $user_agent = md5($_SERVER['HTTP_USER_AGENT']); + } + + // Password + $password = '!'; + + if ($userpwd && !$userpwd->isNew()) { + $password = $userpwd->getPwd(); + } + + // --- + + list($uniq_id, $challenge_hash) = explode('.', $_POST['t-challenge']); + + $long_ip = ip2long($ip); + + if (!$uniq_id || !$challenge_hash || !$long_ip) { + return false; + } + + $challenge_key = "ch$long_ip"; + + $params = [$ip, $password, $user_agent, $board, $thread_id]; + + $response = TwisterCaptcha::normalizeReponseStr($_POST['t-response']); + + $is_valid = TwisterCaptcha::verifyChallengeHash($challenge_hash, $uniq_id, $response, $params); + + if ($is_valid) { + $active_uniq_id = $memcached->get($challenge_key); + + // Delete challenge + $memcached->delete($challenge_key); + + if (!$active_uniq_id || $uniq_id !== $active_uniq_id) { + return false; + } + + // Return and decrement the unsolved session count + $us = decrement_twister_captcha_session($memcached, $ip, $unsolved_count !== null); + + if ($unsolved_count !== null) { + $unsolved_count = $us; + } + + return true; + } + + // Delete challenge + $memcached->delete($challenge_key); + + return false; +} + +// Decrements the unsolved count by 2 and returns the old count +function decrement_twister_captcha_session($memcached, $ip, $return_old = true) { + if (!$memcached) { + return false; + } + + $long_ip = ip2long($ip); + + if (!$long_ip) { + return false; + } + + $key = "us$long_ip"; + + if ($return_old) { + $val = $memcached->get($key); + + if ($val === false) { + $val = 0; + } + } + else { + $val = 0; + } + + $memcached->decrement($key, 2); + + return $val; +} + +// FIXME: The IP arg isn't used for now +function set_twister_captcha_credits($memcached, $ip, $userpwd, $current_time) { + if (!$memcached || !$userpwd) { + return false; + } + + $current_time = (int)$current_time; + + if ($current_time <= 0) { + return false; + } + + //$long_ip = ip2long($ip); + + //if (!$long_ip) { + //return false; + //} + + $credits = 0; + + // Config + // Stage 1 should match the check in use_twister_captcha_credit() + // and captcha.php for optimisation purposes + + // Stage 1 + $noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes) + $noop_post_count_1 = 5; // required post count + $noop_credits_1 = 1; // credits given + $noop_duration_1 = 3600; // duration of the credits (1 hour, in seconds) + + // Stage 2 + $noop_known_ttl_2 = 21600; // 15 days + $noop_post_count_2 = 20; + $noop_credits_2 = 2; + $noop_duration_2 = 7200; // 2 hours + + // Stage 3 + $noop_known_ttl_3 = 129600; // 90 days + $noop_post_count_3 = 100; + $noop_credits_3 = 3; + $noop_duration_3 = 10800; // 3 hours + + // --- + + // The IP changed too recently + if ($userpwd->ipLifetime() < 60) { + return false; + } + + // Stage 3 + if ($userpwd->isUserKnown($noop_known_ttl_3) && $userpwd->postCount() >= $noop_post_count_3) { + $credits = $noop_credits_3; + $duration = $noop_duration_3; + } + // Stage 2 + else if ($userpwd->isUserKnown($noop_known_ttl_2) && $userpwd->postCount() >= $noop_post_count_2) { + $credits = $noop_credits_2; + $duration = $noop_duration_2; + } + // Stage 1 + else if ($userpwd->isUserKnown($noop_known_ttl_1) && $userpwd->postCount() >= $noop_post_count_1) { + $credits = $noop_credits_1; + $duration = $noop_duration_1; + } + else { + return false; + } + + if (!$credits || $credits > 3) { + return false; + } + + $expiration_ts = $current_time + $duration; + + // Require no more than 5 actions in the past 8 minutes + /* + $query = <<= DATE_SUB(NOW(), INTERVAL 8 MINUTE) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count > 5) { + return false; + } + */ + // Set credits + + $pwd = $userpwd->getPwd(); + + if (!$pwd) { + return false; + } + + $key = "cr-$pwd"; + $val = "$credits.$expiration_ts"; + + $res = $memcached->replace($key, $val, $expiration_ts); + + if ($res === false) { + if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) { + return $memcached->set($key, $val, $expiration_ts); + } + else { + return false; + } + } + + return true; +} + +// FIXME: The IP arg isn't used for now +function use_twister_captcha_credit($memcached, $ip, $userpwd) { + if (!$memcached || !$userpwd) { + return false; + } + + //$long_ip = ip2long($ip); + + //if (!$long_ip) { + //return false; + //} + + // Must match the check in set_twister_captcha_credits() + $noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes) + $noop_post_count_1 = 5; // required post count + + if (!$userpwd->isUserKnown($noop_known_ttl_1) || $userpwd->postCount() < $noop_post_count_1) { + return false; + } + + $pwd = $userpwd->getPwd(); + + if (!$pwd) { + return false; + } + + $key = "cr-$pwd"; + $credits = $memcached->get($key); + + if ($credits === false) { + return false; + } + + list($count, $ts) = explode('.', $credits); + + $count = (int)$count; + $ts = (int)$ts; + + // No credits left + if ($count <= 0 || $ts <= 0) { + $memcached->delete($key); + return false; + } + + $count -= 1; + + $res = $memcached->replace($key, "$count.$ts", $ts); + + if ($res === false && $memcached->getResultCode() !== Memcached::RES_NOTSTORED) { + return false; + } + + return true; +} + +function twister_captcha_form() { + return '
    '; +} + +function log_failed_captcha($ip, $userpwd, $board, $thread_id, $is_quiet, $meta = null) { + $data = [ + 'board' => $board, + 'thread_id' => $thread_id, + ]; + + if ($userpwd) { + $data['arg_num'] = $userpwd->pwdLifetime(); + $data['pwd'] = $userpwd->getPwd(); + } + + if ($meta) { + $data['meta'] = $meta; + } + + if ($is_quiet) { + $type = 'failed_captcha_quiet'; + } + else { + $type = 'failed_captcha'; + } + + write_to_event_log($type, $ip, $data); +} + +function h_captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) { + global $hcaptcha_public_key; + + $js_tag = ''; + + if ($autoload) { + $attrs = ' class="h-captcha" data-sitekey="' . $hcaptcha_public_key . '"'; + + if ($dark) { + $attrs .= ' data-theme="dark"'; + } + } + else { + $attrs = ''; + } + + $container_tag = '
    '; + + return $js_tag.$container_tag; +} + +// Moves css out of the form for html validation +function captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) { + global $recaptcha_public_key; + + $js_tag = ''; + + if ($autoload) { + $attrs = ' class="g-recaptcha" data-sitekey="' . $recaptcha_public_key . '"'; + + if ($dark) { + $attrs .= ' data-theme="dark"'; + } + } + else { + $attrs = ''; + } + + $container_tag = '
    '; + + $noscript_tag =<< +
    +
    +
    + +
    +
    + +
    +
    +
    + +HTML; + + if (defined('NOSCRIPT_CAPTCHA_ONLY') && NOSCRIPT_CAPTCHA_ONLY == 1) { + return $container_tag.$noscript_tag; + } + + return $js_tag.$container_tag.$noscript_tag; +} + +// Legacy captcha +// Uses recaptcha v2 for noscript captcha as the v1 seems to be broken currently. +function captcha_form_alt() { + global $recaptcha_public_key; + + $html = <<
    + + +HTML; + + return $html; +} + +function recaptcha_ban($n, $time, $return_error = 0, $length = 1) +{ + auto_ban_poster($name, $length, 1, "failed verification $n times per $time", "Possible spambot; repeatedly sent incorrect CAPTCHA verification."); + if( $return_error == 1 ) { + return S_GENERICERROR; + } + error(S_GENERICERROR); +} + +/** + * Works for both recaptcha and hcaptcha + */ +function recaptcha_bad_captcha($return_error = false, $codes = null) { + $error = S_BADCAPTCHA; + + if (is_array($codes)) { + if (in_array('missing-input-response', $codes)) { + $error = S_NOCAPTCHA; + } + + if ($return_error) { + return $error; + } + else { + error($error); + } + } + else { + if ($return_error) { + return $error; + } + else { + error($error); + } + } +} + +// ----------- +// hCaptcha +// ----------- +function start_hcaptcha_verify($return_error = false) { + global $hcaptcha_private_key, $hcaptcha_ch; + + $response = $_POST["h-captcha-response"]; + + if (!$response) { + if ($return_error == false) { + error(S_NOCAPTCHA); + } + return S_NOCAPTCHA; + } + + $response = urlencode($response); + + $rlen = strlen($response); + + if ($rlen > 32768) { + return recaptcha_bad_captcha($return_error); + } + + $api_url = 'https://hcaptcha.com/siteverify'; + + $post = array( + 'secret' => $hcaptcha_private_key, + 'response' => $response + ); + + $hcaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false); +} + +function end_hcaptcha_verify($return_error = false) { + global $hcaptcha_ch; + + if (!$hcaptcha_ch) { + return; + } + + $ret = rpc_finish_request($hcaptcha_ch, $error, $httperror); + + // BAD + // 413 Request Too Large is bad; it was caused intentionally by the user. + if ($httperror == 413) { + return recaptcha_bad_captcha($return_error); + } + + // BAD + if ($ret == null) { + return recaptcha_bad_captcha($return_error); + } + + $resp = json_decode($ret, true); + + // BAD + // Malformed JSON response from Google + if (json_last_error() !== JSON_ERROR_NONE) { + return recaptcha_bad_captcha($return_error); + } + + // GOOD + if ($resp['success']) { + return $resp; + } + + // BAD + return recaptcha_bad_captcha($return_error, $resp['error-codes']); +} + +// ----------- +// reCaptcha V2 +// ----------- +// FIXME $challenge_field is no longer used +function start_recaptcha_verify($return_error = false, $challenge_field = '') { + global $recaptcha_private_key, $recaptcha_ch; + + $response = $_POST["g-recaptcha-response"]; + + if (!$response) { + if ($return_error == false) { + error(S_NOCAPTCHA); + } + return S_NOCAPTCHA; + } + + $response = urlencode($response); + + $rlen = strlen($response); + + if ($rlen > 4096) { + return recaptcha_bad_captcha($return_error); + } + + $api_url = 'https://www.google.com/recaptcha/api/siteverify'; + + $post = array( + 'secret' => $recaptcha_private_key, + 'response' => $response + ); + + $recaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false); +} + +function end_recaptcha_verify($return_error = false) { + global $recaptcha_ch; + + if (!$recaptcha_ch) { + return; + } + + $ret = rpc_finish_request($recaptcha_ch, $error, $httperror); + + // BAD + // 413 Request Too Large is bad; it was caused intentionally by the user. + if ($httperror == 413) { + return recaptcha_bad_captcha($return_error); + } + + // BAD + if ($ret == null) { + return recaptcha_bad_captcha($return_error); + } + + $resp = json_decode($ret, true); + + // BAD + // Malformed JSON response from Google + if (json_last_error() !== JSON_ERROR_NONE) { + return recaptcha_bad_captcha($return_error); + } + + // GOOD + if ($resp['success']) { + return $resp; + } + + // BAD + return recaptcha_bad_captcha($return_error, $resp['error-codes']); +} + +// ----------- +// reCaptcha V1 +// ----------- +function start_recaptcha_verify_alt($return_error = false, $challenge_field = '') { + global $recaptcha_private_key, $recaptcha_ch; + + $challenge = ( $challenge_field == '' ) ? $_POST["recaptcha_challenge_field"] : $challenge_field; + $response = $_POST["recaptcha_response_field"]; + if (!$challenge || !$response) { + if( $return_error == false ) { + error(S_NOCAPTCHA); + } + return S_NOCAPTCHA; + } + + $num_words = 1 + preg_match_all('/\\s/', $response); + $rlen = strlen($response); + if ($num_words > 3 || $rlen > 128) { + return recaptcha_bad_captcha($return_error); + } + + $post = array( + "privatekey" => $recaptcha_private_key, + "challenge" => $challenge, + "remoteip" => $_SERVER["REMOTE_ADDR"], + "response" => $response + ); + + $recaptcha_ch = rpc_start_request("https://www.google.com/recaptcha/api/verify", $post, null, false); +} + +function end_recaptcha_verify_alt($return_error = false) { + global $recaptcha_ch; + + if (!$recaptcha_ch) return; + + $ret = rpc_finish_request($recaptcha_ch, $error, $httperror); + + if ($httperror == 413) { + return recaptcha_bad_captcha($return_error); + } + + if ($ret) { + $lines = explode("\n", $ret); + if ($lines[0] === "true") { + // GOOD + return; + } + } + + // BAD + return recaptcha_bad_captcha($return_error); +} + +?> diff --git a/lib/db.php b/lib/db.php new file mode 100644 index 0000000..4c46f76 --- /dev/null +++ b/lib/db.php @@ -0,0 +1,345 @@ + 0) { + mysql_internal_err($con, "recursively locked table", "lock tables ".BOARD_DIR); + return; + } + + $board_lock_level++; + + mysql_query("lock tables ".BOARD_DIR." read".($local ? " local" : ""), $con); +} + +function mysql_board_unlock($ignore_error=false) { + global $board_lock_level, $con; + + if ($board_lock_level == 0) { + if (!$ignore_error) + mysql_internal_err($con, "not already locked", "unlock tables"); + return; + } + + $board_lock_level--; + mysql_query("unlock tables", $con); +} + +function mysql_clear_locks() { + mysql_board_unlock(true); +} + +function mysql_global_connect($pconnect=true) { + global $gcon; + $gcon = mysql_try_connect(SQLHOST_GLOBAL, SQLUSER_GLOBAL, SQLPASS_GLOBAL, SQLDB_GLOBAL, $pconnect); + return $gcon; +} + +// assumes BOARD_DIR is set +function mysql_check_connections() { + global $gcon, $con; + + $gcon_res = mysql_ping($gcon); + $con_res = mysql_ping($con); + + if ($gcon_res == false || $con_res == false) { + mysql_internal_close($con); + mysql_internal_close($gcon); + $con = null; + $gcon = null; + mysql_board_connect(BOARD_DIR, false); + mysql_global_connect(false); + } +} + +//really bad error system... +function mysql_internal_err($conn, $priverr, $query="", $die=false) { + global $mysql_never_die; + global $mysql_suppress_err; + + $err = sprintf("%s error: %s - %d - %s%s", $query ? "query" : "connection", + $priverr, mysql_errno($conn), mysql_error($conn), $query ? " query: $query" : ""); + + if (SQL_DEBUG && ini_get('display_errors')) echo $err."\n"; + + internal_error_log("SQL", $err); + + if ($die && !$mysql_never_die) die($query ? S_SQLDBSF : S_SQLCONF); +} + +//pconnect - call _pconnect, not safe if tables are locked +function mysql_board_connect($board="", $pconnect=true) { + global $con; + global $did_add_lockfunc; + + if (!defined('SQLHOST')) { + $db = 1; // db is always 1 now + $host = "db-ena.int"; // and always "db-ena" (this should never happen because we define SQLHOST) + $db = "img$db"; + + define('SQLHOST', $host); + define('SQLDB', $db); + if ($board) define('BOARD_DIR', $board); + } else { + $host = SQLHOST; + $db = SQLDB; + } + + $con = mysql_try_connect($host, SQLUSER, SQLPASS, $db, $pconnect); + //if (SQL_DEBUG && ini_get('display_errors')) echo "connected to ".$host." SQLHOST is ".SQLHOST."\n"; + + if (!$did_add_lockfunc) { + $did_add_lockfunc = 1; + register_shutdown_function("mysql_clear_locks"); + } + return $con; +} + +function mysql_do_query($query, $con) { + global $mysql_unbuffered_reads; + global $mysql_suppress_err; + global $mysql_query_log; + global $mysql_debug_buf; + static $querylog_fd; + + $querylog = (defined('QUERY_LOG') && constant('QUERY_LOG')) || $mysql_query_log == true; + $is_select = stripos($query, "SELECT")===0; + + $time = 0; + if ($querylog) { + $time = microtime(true); + + if (!$querylog_fd) { + $querylog_fd = fopen("/www/perhost/querylog.log", "a"); + flock($querylog_fd, LOCK_EX); + } + + fprintf($querylog_fd, "%d query: %s\n", getmypid(), $query); + } + + if ($mysql_unbuffered_reads) + $ret = @mysql_unbuffered_query($query, $con); + else + $ret = @mysql_query($query, $con); + + if ($ret && $querylog) { + $elapsed = microtime(true) - $time; + + if (!$mysql_unbuffered_reads) { + $nr = @mysql_num_rows($ret); + if (!$nr) $nr = @mysql_affected_rows($ret); + } else + $nr = "?"; + + fprintf($querylog_fd, "%d rows, %f sec\n", $nr, $elapsed); + } + + if (isset($mysql_debug_buf)) { + if (!$mysql_unbuffered_reads) { + $nr = @mysql_num_rows($ret); + if (!$nr) $nr = @mysql_affected_rows($ret); + if (!$nr) $nr = 0; + } else + $nr = "?"; + + $mysql_debug_buf .= "Query: $query\nRows: $nr\n"; + } + + if ($ret === FALSE) { + mysql_internal_err($con, "in do_query", $query); + } + + return $ret; +} + +function try_escape_string($string, $con, $recon_func, $tries=0) +{ + $res = mysql_real_escape_string($string, $con); + + if ($res === FALSE) { + mysql_internal_err($con, "in escape_string", $string, $tries == 0); + } + + return $res; +} + +//note for use of db query functions +//old-style calls (escaping done manually beforehand) +//must connect manually too + +//for read queries +function mysql_global_call() { + global $gcon; + if(!$gcon) mysql_global_connect(); + $args = func_get_args(); + $format = array_shift($args); + + if (count($args)) { + foreach($args as &$arg) + $arg = try_escape_string($arg, $gcon, "mysql_global_connect" ); + $query = vsprintf($format, $args); + } else $query = $format; + + return mysql_do_query( $query, $gcon ); +} + +function mysql_global_error() { + global $gcon; + if(!$gcon) mysql_global_connect(); + + return mysql_error($gcon); +} + +//for r/w queries (historical, doesn't actually matter) +//TODO: remove +function mysql_global_do() { + global $gcon; + if(!$gcon) mysql_global_connect(); + $args = func_get_args(); + $format = array_shift($args); + + if (count($args)) { + foreach($args as &$arg) + $arg = try_escape_string($arg, $gcon, "mysql_global_connect" ); + $query = vsprintf($format, $args); + } else $query = $format; + + return mysql_do_query( $query, $gcon ); +} + +function mysql_global_insert_id() { + global $gcon; + return mysql_insert_id($gcon); +} + +function mysql_board_call() { + global $con; + if (!$con) mysql_board_connect(); + $args = func_get_args(); + $format = array_shift($args); + + if (count($args)) { + foreach($args as &$arg) + $arg = mysql_real_escape_string($arg, $con); + $query = vsprintf($format, $args); + } else $query = $format; + + return mysql_do_query( $query, $con ); +} + +function mysql_global_escape($string) { + global $gcon; + if(!$gcon) mysql_global_connect(); + + return mysql_real_escape_string($string, $gcon); +} + +function mysql_board_error() { + global $con; + if(!$con) mysql_board_connect(); + + return mysql_error($con); +} + +function mysql_board_escape($string) { + global $con; + if (!$con) mysql_board_connect(); + + return mysql_real_escape_string($string, $con); +} + +function mysql_board_get_post($board,$no) { + global $con; + mysql_board_connect($board); + $query = mysql_board_call("SELECT HIGH_PRIORITY * from `%s` WHERE no=%d",$board,$no); + $array = mysql_fetch_assoc($query); + mysql_close($con); + $con = NULL; + return $array; +} + +function mysql_board_get_post_lazy( $board, $no ) +{ + global $con; + mysql_board_connect($board); + $query = mysql_board_call("SELECT * from `%s` WHERE no=%d",$board,$no); + $array = mysql_fetch_assoc($query); + mysql_close($con); + $con = NULL; + return $array; +} + +function mysql_board_insert_id() { + global $con; + return mysql_insert_id($con); +} + +function mysql_global_row($table, $col, $val) +{ + $q = mysql_global_call("select * from `%s` where $col='%s'", $table, $val); + $r = mysql_fetch_assoc($q); + mysql_free_result($q); + return $r; +} + +function mysql_board_row($table, $col, $val) +{ + $q = mysql_board_call("select * from `%s` where $col='%s'", $table, $val); + $r = mysql_fetch_assoc($q); + mysql_free_result($q); + return $r; +} + +// answer must be one column +function mysql_column_array($q) { + $ret = array(); + while ($row = mysql_fetch_row($q)) + $ret[] = $row[0]; + return $ret; +} + +// turn "" into NULL +// incompatible with escaping :( +function mysql_nullify($s) { + return ($s || $s === '0') ? "'$s'" : "''"; //"NULL"; +} + +?> diff --git a/lib/db_pdo.php b/lib/db_pdo.php new file mode 100644 index 0000000..a967d91 --- /dev/null +++ b/lib/db_pdo.php @@ -0,0 +1,308 @@ + true) : null; + + $con = @new PDO( "mysql:host=$host;dbname=$db", $user, $pass, $pconnect ); + $failed = 1; $pconnect = false; + + if( $con ) $failed = 0; + } while( $failed && $tries-- ); + + if( $failed ) { + mysql_internal_err( NULL, "while connecting to $host", "", true ); + } + + // Set attributes + $con->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC ); + $con->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); + $con->setAttribute( PDO::ATTR_ORACLE_NULLS, PDO::NULL_TO_STRING ); + //$con->setAttribute( PDO::ATTR_EMULATE_PREPARES, false ); + $con->setAttribute( PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true ); + + return $con; +} + +/** + * @param PDO $conn + */ +function mysql_internal_err($conn, $priverr, $query="", $die=false) { + global $mysql_never_die; + global $mysql_suppress_err; + + if ($mysql_suppress_err) return; + + $errInfo = $conn->errorInfo(); + $errInfo = $errInfo[2]; + + $err = sprintf("%s error: %s - %d - %s%s", $query ? "query" : "connection", + $priverr, $conn->errorCode(), $errInfo, $query ? " query: $query" : ""); + + //internal_error_log("SQL", $err); + + echo $err; + if ($die && !$mysql_never_die) die($query ? S_SQLDBSF : S_SQLCONF); +} + + +/** + * @param PDO $con + */ +function mysql_internal_close( $con ) +{ + $con->exec('UNLOCK TABLES'); + unset($con); +} + +function mysql_board_lock( $local = false ) +{ + global $board_lock_level, $con; + + if( $board_lock_level > 0 ) { + mysql_internal_err( $con, "recursively locked table", "lock tables " . BOARD_DIR ); + return; + } + + $board_lock_level++; + + $local = $local ? ' local' : ''; + $con->exec("LOCK TABLE " . BOARD_DIR . " READ $local"); +} + +function mysql_board_unlock( $ignore_error = false ) +{ + global $board_lock_level, $con; + if( $board_lock_level == 0 ) { + if( !$ignore_error ) { + mysql_internal_err( $con, "not already locked", "UNLOCK TABLES" ); + } + + return; + } + + $board_lock_level--; + $con->exec('UNLOCK TABLES'); +} + +function mysql_clear_locks() +{ + mysql_board_unlock(true); +} + +/** CONNECTIONS **/ +function mysql_global_connect() +{ + global $gcon; + $gcon = mysql_try_connect( + SQLHOST_GLOBAL, + SQLUSER_GLOBAL, + SQLPASS_GLOBAL, + SQLDB_GLOBAL + ); + + return $gcon; +} + +function mysql_board_connect( $board = '', $pconnect = true ) +{ + global $con; + + if( !defined( 'SQLHOST' ) || ( constant( 'BOARD_DIR' ) != $board ) ) { + if( !$board ) { + if( !defined( 'BOARD_DIR' ) ) { + mysql_internal_err( null, 'no board defined to connect to' ); + } else { + $board = BOARD_DIR; + } + + $db = 1; + $host = "db-ena.int"; + $db = "img$db"; + + define( 'SQLHOST', $host ); + define( 'SQLDB', $db ); + define( 'BOARD_DIR', $board ); + } + } else { + $host = SQLHOST; + $db = SQLDB; + } + + $con = mysql_try_connect( $host, SQLUSER, SQLPASS, $db ); + register_shutdown_function( 'mysql_clear_locks' ); + + return $con; +} + +/** + * @param PDOStatement $query + * @param PDOStatement $res + * @param PDO $con + */ +function mysql_do_query( $query, $con, $querystr, $recon_func, $tries = 0 ) +{ + global $mysql_unbuffered_reads, $mysql_suppress_err, $has_set_unbuffered; + + $querylog = defined( 'QUERY_LOG' ) && QUERY_LOG; + $is_select = strpos( $querystr, 'SELECT' ) === 0; + + if( $querylog ) $time = microtime(true); + + if( $mysql_unbuffered_reads ) $query->closeCursor(); // close anything open + $con->setAttribute( PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, (bool)!$mysql_unbuffered_reads ); + $res = $query->execute(); + + if( $res && $querylog ) { + global $querylog_fd; + $elapsed = microtime(true); + + $nr = $mysql_unbuffered_reads ? '?' : $query->rowCount(); + + if( !$querylog_fd ) { + $querylog_fd = fopen( '/www/perhost/querylog.log', 'a' ); + flock( $querylog_fd, LOCK_EX ); + } + + fprintf($querylog_fd, "%d query: %s\n%d rows, %f sec\n", getmypid(), $query, $nr, $elapsed / 1000000.); + } + + if( $mysql_suppress_err ) return $query; + + if( $res === false || ( !$mysql_unbuffered_reads && $is_select && $res === false ) ) { + if( $is_select && $tries > 0 ) { + mysql_internal_close( $con ); + $con = $recon_func(); + + return mysql_do_query( $query, $con, $querystr, $recon_func, $tries-1 ); + } else { + error_log( 'do_query res = ' . gettype($res) . ': ' . $res ); + mysql_internal_err( $con, 'in do_query', $querystr, $tries == 0 ); + } + } + + return $query; +} + +function mysql_global_call() +{ + global $gcon; + if( !$gcon ) mysql_global_connect(); + + $args = func_get_args(); + $querystr = array_shift($args); + + $query = $gcon->prepare($querystr); + + if( count( $args ) ) { + $i = 1; + + foreach( $args as $arg ) { + if( $arg == null ) $arg = ''; + $type = is_int($arg) || ctype_digit($arg) ? PDO::PARAM_INT : PDO::PARAM_STR; + $query->bindValue( $i, $arg, $type ); + + $i++; + } + } + + return mysql_do_query( $query, $gcon, $querystr, 'mysql_global_connect' ); +} + +function mysql_board_call() +{ + global $con; + if( !$con ) mysql_board_connect(); + + $args = func_get_args(); + $querystr = array_shift($args); + + $query = $con->prepare($querystr); + + if( count( $args ) ) { + $i = 1; + + foreach( $args as $arg ) { + if( $arg == null ) $arg = ''; + $type = is_int($arg) || ctype_digit($arg) ? PDO::PARAM_INT : PDO::PARAM_STR; + $query->bindValue( $i, $arg, $type ); + + $i++; + } + } + + return mysql_do_query( $query, $con, $querystr, 'mysql_global_connect' ); +} + +function mysql_board_get_post( $board, $no ) +{ + global $con; + mysql_board_connect( $board ); + + /** + * @param PDOStatement $query + */ + $query = mysql_board_call( "SELECT HIGH_PRIORITY * FROM $board WHERE no=?", $no ); + $arr = $query->fetch(); + + $con = null; + return $arr; +} + +/** + * Utility function to make $query->fetch() on SELECT COUNT() not a pain in the ass + * + * @param PDOStatement $query + */ +function mysql_fetch_count( $query ) +{ + return $query->fetchColumn(); +} + +/** + * Utility function mimicking mysql_free_result() + * + * @param PDOStatement $query + */ +function mysql_close_result( $query ) +{ + $query->closeCursor(); +} + +function mysql_column_array( $query, $field ) +{ + $ret = array(); + while( $row = $query->fetch() ) { + $ret[] = $row[$field]; + } + + return $ret; +} + +function mysql_board_insert_id() +{ + global $con; + return $con->lastInsertId(); +} diff --git a/lib/geoip2-test.php b/lib/geoip2-test.php new file mode 100644 index 0000000..012d532 --- /dev/null +++ b/lib/geoip2-test.php @@ -0,0 +1,115 @@ +get($ip); + } catch (Exception $e) { + return null; + } + + $data = array(); + + // Continent + if (isset($entry['continent']['code'])) { + $data['continent_code'] = $entry['continent']['code']; + } + + // Country + if (isset($entry['country']['iso_code'])) { + $data['country_code'] = $entry['country']['iso_code']; + $data['country_name'] = $entry['country']['names']['en']; + + // State for US + if ($data['country_code'] === 'US' && isset($entry['subdivisions'][0]['iso_code'])) { + $data['state_code'] = $entry['subdivisions'][0]['iso_code']; + $data['state_name'] = $entry['subdivisions'][0]['names']['en']; + } + // FIXME: subdivisions for UK during sport events + else if ($data['country_code'] === 'GB' && isset($entry['subdivisions'][0]['iso_code'])) { + $data['sub_code'] = $entry['subdivisions'][0]['iso_code']; + } + } + + if (isset($entry['city']['names']['en'])) { + $data['city_name'] = $entry['city']['names']['en']; + } + + if (empty($data)) { + return null; + } + + return $data; + } + + public static function get_asn($ip) { + if (!$ip) { + return null; + } + + if (self::$mmdb_asn === null) { + self::$mmdb_asn = self::load_db(self::$db_file_asn); + } + + if (!self::$mmdb_asn) { + return null; + } + + try { + $entry = self::$mmdb_asn->get($ip); + } catch (Exception $e) { + return null; + } + + $data = array(); + + if (isset($entry['autonomous_system_number'])) { + $data['asn'] = $entry['autonomous_system_number']; + } + + if (isset($entry['autonomous_system_organization'])) { + $data['aso'] = $entry['autonomous_system_organization']; + } + + if (empty($data)) { + return null; + } + + return $data; + } +} diff --git a/lib/geoip2.php b/lib/geoip2.php new file mode 100644 index 0000000..012d532 --- /dev/null +++ b/lib/geoip2.php @@ -0,0 +1,115 @@ +get($ip); + } catch (Exception $e) { + return null; + } + + $data = array(); + + // Continent + if (isset($entry['continent']['code'])) { + $data['continent_code'] = $entry['continent']['code']; + } + + // Country + if (isset($entry['country']['iso_code'])) { + $data['country_code'] = $entry['country']['iso_code']; + $data['country_name'] = $entry['country']['names']['en']; + + // State for US + if ($data['country_code'] === 'US' && isset($entry['subdivisions'][0]['iso_code'])) { + $data['state_code'] = $entry['subdivisions'][0]['iso_code']; + $data['state_name'] = $entry['subdivisions'][0]['names']['en']; + } + // FIXME: subdivisions for UK during sport events + else if ($data['country_code'] === 'GB' && isset($entry['subdivisions'][0]['iso_code'])) { + $data['sub_code'] = $entry['subdivisions'][0]['iso_code']; + } + } + + if (isset($entry['city']['names']['en'])) { + $data['city_name'] = $entry['city']['names']['en']; + } + + if (empty($data)) { + return null; + } + + return $data; + } + + public static function get_asn($ip) { + if (!$ip) { + return null; + } + + if (self::$mmdb_asn === null) { + self::$mmdb_asn = self::load_db(self::$db_file_asn); + } + + if (!self::$mmdb_asn) { + return null; + } + + try { + $entry = self::$mmdb_asn->get($ip); + } catch (Exception $e) { + return null; + } + + $data = array(); + + if (isset($entry['autonomous_system_number'])) { + $data['asn'] = $entry['autonomous_system_number']; + } + + if (isset($entry['autonomous_system_organization'])) { + $data['aso'] = $entry['autonomous_system_organization']; + } + + if (empty($data)) { + return null; + } + + return $data; + } +} diff --git a/lib/global_constants.php b/lib/global_constants.php new file mode 100644 index 0000000..6775dca --- /dev/null +++ b/lib/global_constants.php @@ -0,0 +1,6 @@ +config = HTMLPurifier_Config::create($config); + $this->strategy = new HTMLPurifier_Strategy_Core(); + } + + /** + * Adds a filter to process the output. First come first serve + * + * @param HTMLPurifier_Filter $filter HTMLPurifier_Filter object + */ + public function addFilter($filter) + { + trigger_error( + 'HTMLPurifier->addFilter() is deprecated, use configuration directives' . + ' in the Filter namespace or Filter.Custom', + E_USER_WARNING + ); + $this->filters[] = $filter; + } + + /** + * Filters an HTML snippet/document to be XSS-free and standards-compliant. + * + * @param string $html String of HTML to purify + * @param HTMLPurifier_Config $config Config object for this operation, + * if omitted, defaults to the config object specified during this + * object's construction. The parameter can also be any type + * that HTMLPurifier_Config::create() supports. + * + * @return string Purified HTML + */ + public function purify($html, $config = null) + { + // :TODO: make the config merge in, instead of replace + $config = $config ? HTMLPurifier_Config::create($config) : $this->config; + + // implementation is partially environment dependant, partially + // configuration dependant + $lexer = HTMLPurifier_Lexer::create($config); + + $context = new HTMLPurifier_Context(); + + // setup HTML generator + $this->generator = new HTMLPurifier_Generator($config, $context); + $context->register('Generator', $this->generator); + + // set up global context variables + if ($config->get('Core.CollectErrors')) { + // may get moved out if other facilities use it + $language_factory = HTMLPurifier_LanguageFactory::instance(); + $language = $language_factory->create($config, $context); + $context->register('Locale', $language); + + $error_collector = new HTMLPurifier_ErrorCollector($context); + $context->register('ErrorCollector', $error_collector); + } + + // setup id_accumulator context, necessary due to the fact that + // AttrValidator can be called from many places + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + + $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); + + // setup filters + $filter_flags = $config->getBatch('Filter'); + $custom_filters = $filter_flags['Custom']; + unset($filter_flags['Custom']); + $filters = array(); + foreach ($filter_flags as $filter => $flag) { + if (!$flag) { + continue; + } + if (strpos($filter, '.') !== false) { + continue; + } + $class = "HTMLPurifier_Filter_$filter"; + $filters[] = new $class; + } + foreach ($custom_filters as $filter) { + // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat + $filters[] = $filter; + } + $filters = array_merge($filters, $this->filters); + // maybe prepare(), but later + + for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) { + $html = $filters[$i]->preFilter($html, $config, $context); + } + + // purified HTML + $html = + $this->generator->generateFromTokens( + // list of tokens + $this->strategy->execute( + // list of un-purified tokens + $lexer->tokenizeHTML( + // un-purified HTML + $html, + $config, + $context + ), + $config, + $context + ) + ); + + for ($i = $filter_size - 1; $i >= 0; $i--) { + $html = $filters[$i]->postFilter($html, $config, $context); + } + + $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context); + $this->context =& $context; + return $html; + } + + /** + * Filters an array of HTML snippets + * + * @param string[] $array_of_html Array of html snippets + * @param HTMLPurifier_Config $config Optional config object for this operation. + * See HTMLPurifier::purify() for more details. + * + * @return string[] Array of purified HTML + */ + public function purifyArray($array_of_html, $config = null) + { + $context_array = array(); + foreach ($array_of_html as $key => $html) { + $array_of_html[$key] = $this->purify($html, $config); + $context_array[$key] = $this->context; + } + $this->context = $context_array; + return $array_of_html; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + */ + public static function instance($prototype = null) + { + if (!self::$instance || $prototype) { + if ($prototype instanceof HTMLPurifier) { + self::$instance = $prototype; + } elseif ($prototype) { + self::$instance = new HTMLPurifier($prototype); + } else { + self::$instance = new HTMLPurifier(); + } + } + return self::$instance; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + * @note Backwards compatibility, see instance() + */ + public static function getInstance($prototype = null) + { + return HTMLPurifier::instance($prototype); + } +} + + + + + +/** + * Converts a stream of HTMLPurifier_Token into an HTMLPurifier_Node, + * and back again. + * + * @note This transformation is not an equivalence. We mutate the input + * token stream to make it so; see all [MUT] markers in code. + */ +class HTMLPurifier_Arborize +{ + public static function arborize($tokens, $config, $context) { + $definition = $config->getHTMLDefinition(); + $parent = new HTMLPurifier_Token_Start($definition->info_parent); + $stack = array($parent->toNode()); + foreach ($tokens as $token) { + $token->skip = null; // [MUT] + $token->carryover = null; // [MUT] + if ($token instanceof HTMLPurifier_Token_End) { + $token->start = null; // [MUT] + $r = array_pop($stack); + assert($r->name === $token->name); + assert(empty($token->attr)); + $r->endCol = $token->col; + $r->endLine = $token->line; + $r->endArmor = $token->armor; + continue; + } + $node = $token->toNode(); + $stack[count($stack)-1]->children[] = $node; + if ($token instanceof HTMLPurifier_Token_Start) { + $stack[] = $node; + } + } + assert(count($stack) == 1); + return $stack[0]; + } + + public static function flatten($node, $config, $context) { + $level = 0; + $nodes = array($level => new HTMLPurifier_Queue(array($node))); + $closingTokens = array(); + $tokens = array(); + do { + while (!$nodes[$level]->isEmpty()) { + $node = $nodes[$level]->shift(); // FIFO + list($start, $end) = $node->toTokenPair(); + if ($level > 0) { + $tokens[] = $start; + } + if ($end !== NULL) { + $closingTokens[$level][] = $end; + } + if ($node instanceof HTMLPurifier_Node_Element) { + $level++; + $nodes[$level] = new HTMLPurifier_Queue(); + foreach ($node->children as $childNode) { + $nodes[$level]->push($childNode); + } + } + } + $level--; + if ($level && isset($closingTokens[$level])) { + while ($token = array_pop($closingTokens[$level])) { + $tokens[] = $token; + } + } + } while ($level > 0); + return $tokens; + } +} + + + +/** + * Defines common attribute collections that modules reference + */ + +class HTMLPurifier_AttrCollections +{ + + /** + * Associative array of attribute collections, indexed by name. + * @type array + */ + public $info = array(); + + /** + * Performs all expansions on internal data for use by other inclusions + * It also collects all attribute collection extensions from + * modules + * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance + * @param HTMLPurifier_HTMLModule[] $modules Hash array of HTMLPurifier_HTMLModule members + */ + public function __construct($attr_types, $modules) + { + // load extensions from the modules + foreach ($modules as $module) { + foreach ($module->attr_collections as $coll_i => $coll) { + if (!isset($this->info[$coll_i])) { + $this->info[$coll_i] = array(); + } + foreach ($coll as $attr_i => $attr) { + if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) { + // merge in includes + $this->info[$coll_i][$attr_i] = array_merge( + $this->info[$coll_i][$attr_i], + $attr + ); + continue; + } + $this->info[$coll_i][$attr_i] = $attr; + } + } + } + // perform internal expansions and inclusions + foreach ($this->info as $name => $attr) { + // merge attribute collections that include others + $this->performInclusions($this->info[$name]); + // replace string identifiers with actual attribute objects + $this->expandIdentifiers($this->info[$name], $attr_types); + } + } + + /** + * Takes a reference to an attribute associative array and performs + * all inclusions specified by the zero index. + * @param array &$attr Reference to attribute array + */ + public function performInclusions(&$attr) + { + if (!isset($attr[0])) { + return; + } + $merge = $attr[0]; + $seen = array(); // recursion guard + // loop through all the inclusions + for ($i = 0; isset($merge[$i]); $i++) { + if (isset($seen[$merge[$i]])) { + continue; + } + $seen[$merge[$i]] = true; + // foreach attribute of the inclusion, copy it over + if (!isset($this->info[$merge[$i]])) { + continue; + } + foreach ($this->info[$merge[$i]] as $key => $value) { + if (isset($attr[$key])) { + continue; + } // also catches more inclusions + $attr[$key] = $value; + } + if (isset($this->info[$merge[$i]][0])) { + // recursion + $merge = array_merge($merge, $this->info[$merge[$i]][0]); + } + } + unset($attr[0]); + } + + /** + * Expands all string identifiers in an attribute array by replacing + * them with the appropriate values inside HTMLPurifier_AttrTypes + * @param array &$attr Reference to attribute array + * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance + */ + public function expandIdentifiers(&$attr, $attr_types) + { + // because foreach will process new elements we add, make sure we + // skip duplicates + $processed = array(); + + foreach ($attr as $def_i => $def) { + // skip inclusions + if ($def_i === 0) { + continue; + } + + if (isset($processed[$def_i])) { + continue; + } + + // determine whether or not attribute is required + if ($required = (strpos($def_i, '*') !== false)) { + // rename the definition + unset($attr[$def_i]); + $def_i = trim($def_i, '*'); + $attr[$def_i] = $def; + } + + $processed[$def_i] = true; + + // if we've already got a literal object, move on + if (is_object($def)) { + // preserve previous required + $attr[$def_i]->required = ($required || $attr[$def_i]->required); + continue; + } + + if ($def === false) { + unset($attr[$def_i]); + continue; + } + + if ($t = $attr_types->get($def)) { + $attr[$def_i] = $t; + $attr[$def_i]->required = $required; + } else { + unset($attr[$def_i]); + } + } + } +} + + + + + +/** + * Base class for all validating attribute definitions. + * + * This family of classes forms the core for not only HTML attribute validation, + * but also any sort of string that needs to be validated or cleaned (which + * means CSS properties and composite definitions are defined here too). + * Besides defining (through code) what precisely makes the string valid, + * subclasses are also responsible for cleaning the code if possible. + */ + +abstract class HTMLPurifier_AttrDef +{ + + /** + * Tells us whether or not an HTML attribute is minimized. + * Has no meaning in other contexts. + * @type bool + */ + public $minimized = false; + + /** + * Tells us whether or not an HTML attribute is required. + * Has no meaning in other contexts + * @type bool + */ + public $required = false; + + /** + * Validates and cleans passed string according to a definition. + * + * @param string $string String to be validated and cleaned. + * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object. + * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object. + */ + abstract public function validate($string, $config, $context); + + /** + * Convenience method that parses a string as if it were CDATA. + * + * This method process a string in the manner specified at + * by removing + * leading and trailing whitespace, ignoring line feeds, and replacing + * carriage returns and tabs with spaces. While most useful for HTML + * attributes specified as CDATA, it can also be applied to most CSS + * values. + * + * @note This method is not entirely standards compliant, as trim() removes + * more types of whitespace than specified in the spec. In practice, + * this is rarely a problem, as those extra characters usually have + * already been removed by HTMLPurifier_Encoder. + * + * @warning This processing is inconsistent with XML's whitespace handling + * as specified by section 3.3.3 and referenced XHTML 1.0 section + * 4.7. However, note that we are NOT necessarily + * parsing XML, thus, this behavior may still be correct. We + * assume that newlines have been normalized. + */ + public function parseCDATA($string) + { + $string = trim($string); + $string = str_replace(array("\n", "\t", "\r"), ' ', $string); + return $string; + } + + /** + * Factory method for creating this class from a string. + * @param string $string String construction info + * @return HTMLPurifier_AttrDef Created AttrDef object corresponding to $string + */ + public function make($string) + { + // default implementation, return a flyweight of this object. + // If $string has an effect on the returned object (i.e. you + // need to overload this method), it is best + // to clone or instantiate new copies. (Instantiation is safer.) + return $this; + } + + /** + * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work + * properly. THIS IS A HACK! + * @param string $string a CSS colour definition + * @return string + */ + protected function mungeRgb($string) + { + return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string); + } + + /** + * Parses a possibly escaped CSS string and returns the "pure" + * version of it. + */ + protected function expandCSSEscape($string) + { + // flexibly parse it + $ret = ''; + for ($i = 0, $c = strlen($string); $i < $c; $i++) { + if ($string[$i] === '\\') { + $i++; + if ($i >= $c) { + $ret .= '\\'; + break; + } + if (ctype_xdigit($string[$i])) { + $code = $string[$i]; + for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { + if (!ctype_xdigit($string[$i])) { + break; + } + $code .= $string[$i]; + } + // We have to be extremely careful when adding + // new characters, to make sure we're not breaking + // the encoding. + $char = HTMLPurifier_Encoder::unichr(hexdec($code)); + if (HTMLPurifier_Encoder::cleanUTF8($char) === '') { + continue; + } + $ret .= $char; + if ($i < $c && trim($string[$i]) !== '') { + $i--; + } + continue; + } + if ($string[$i] === "\n") { + continue; + } + } + $ret .= $string[$i]; + } + return $ret; + } +} + + + + + +/** + * Processes an entire attribute array for corrections needing multiple values. + * + * Occasionally, a certain attribute will need to be removed and popped onto + * another value. Instead of creating a complex return syntax for + * HTMLPurifier_AttrDef, we just pass the whole attribute array to a + * specialized object and have that do the special work. That is the + * family of HTMLPurifier_AttrTransform. + * + * An attribute transformation can be assigned to run before or after + * HTMLPurifier_AttrDef validation. See HTMLPurifier_HTMLDefinition for + * more details. + */ + +abstract class HTMLPurifier_AttrTransform +{ + + /** + * Abstract: makes changes to the attributes dependent on multiple values. + * + * @param array $attr Assoc array of attributes, usually from + * HTMLPurifier_Token_Tag::$attr + * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object. + * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object + * @return array Processed attribute array. + */ + abstract public function transform($attr, $config, $context); + + /** + * Prepends CSS properties to the style attribute, creating the + * attribute if it doesn't exist. + * @param array &$attr Attribute array to process (passed by reference) + * @param string $css CSS to prepend + */ + public function prependCSS(&$attr, $css) + { + $attr['style'] = isset($attr['style']) ? $attr['style'] : ''; + $attr['style'] = $css . $attr['style']; + } + + /** + * Retrieves and removes an attribute + * @param array &$attr Attribute array to process (passed by reference) + * @param mixed $key Key of attribute to confiscate + * @return mixed + */ + public function confiscateAttr(&$attr, $key) + { + if (!isset($attr[$key])) { + return null; + } + $value = $attr[$key]; + unset($attr[$key]); + return $value; + } +} + + + + + +/** + * Provides lookup array of attribute types to HTMLPurifier_AttrDef objects + */ +class HTMLPurifier_AttrTypes +{ + /** + * Lookup array of attribute string identifiers to concrete implementations. + * @type HTMLPurifier_AttrDef[] + */ + protected $info = array(); + + /** + * Constructs the info array, supplying default implementations for attribute + * types. + */ + public function __construct() + { + // XXX This is kind of poor, since we don't actually /clone/ + // instances; instead, we use the supplied make() attribute. So, + // the underlying class must know how to deal with arguments. + // With the old implementation of Enum, that ignored its + // arguments when handling a make dispatch, the IAlign + // definition wouldn't work. + + // pseudo-types, must be instantiated via shorthand + $this->info['Enum'] = new HTMLPurifier_AttrDef_Enum(); + $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool(); + + $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text(); + $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID(); + $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length(); + $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength(); + $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens(); + $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels(); + $this->info['Text'] = new HTMLPurifier_AttrDef_Text(); + $this->info['URI'] = new HTMLPurifier_AttrDef_URI(); + $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang(); + $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color(); + $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right'); + $this->info['LAlign'] = self::makeEnum('top,bottom,left,right'); + $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget(); + + // unimplemented aliases + $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text(); + $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Character'] = new HTMLPurifier_AttrDef_Text(); + + // "proprietary" types + $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class(); + + // number is really a positive integer (one or more digits) + // FIXME: ^^ not always, see start and value of list items + $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); + } + + private static function makeEnum($in) + { + return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in))); + } + + /** + * Retrieves a type + * @param string $type String type name + * @return HTMLPurifier_AttrDef Object AttrDef for type + */ + public function get($type) + { + // determine if there is any extra info tacked on + if (strpos($type, '#') !== false) { + list($type, $string) = explode('#', $type, 2); + } else { + $string = ''; + } + + if (!isset($this->info[$type])) { + trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); + return; + } + return $this->info[$type]->make($string); + } + + /** + * Sets a new implementation for a type + * @param string $type String type name + * @param HTMLPurifier_AttrDef $impl Object AttrDef for type + */ + public function set($type, $impl) + { + $this->info[$type] = $impl; + } +} + + + + + +/** + * Validates the attributes of a token. Doesn't manage required attributes + * very well. The only reason we factored this out was because RemoveForeignElements + * also needed it besides ValidateAttributes. + */ +class HTMLPurifier_AttrValidator +{ + + /** + * Validates the attributes of a token, mutating it as necessary. + * that has valid tokens + * @param HTMLPurifier_Token $token Token to validate. + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @param HTMLPurifier_Context $context Instance of HTMLPurifier_Context + */ + public function validateToken($token, $config, $context) + { + $definition = $config->getHTMLDefinition(); + $e =& $context->get('ErrorCollector', true); + + // initialize IDAccumulator if necessary + $ok =& $context->get('IDAccumulator', true); + if (!$ok) { + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + } + + // initialize CurrentToken if necessary + $current_token =& $context->get('CurrentToken', true); + if (!$current_token) { + $context->register('CurrentToken', $token); + } + + if (!$token instanceof HTMLPurifier_Token_Start && + !$token instanceof HTMLPurifier_Token_Empty + ) { + return; + } + + // create alias to global definition array, see also $defs + // DEFINITION CALL + $d_defs = $definition->info_global_attr; + + // don't update token until the very end, to ensure an atomic update + $attr = $token->attr; + + // do global transformations (pre) + // nothing currently utilizes this + foreach ($definition->info_attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // do local transformations only applicable to this element (pre) + // ex.

    to

    + foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // create alias to this element's attribute definition array, see + // also $d_defs (global attribute definition array) + // DEFINITION CALL + $defs = $definition->info[$token->name]->attr; + + $attr_key = false; + $context->register('CurrentAttr', $attr_key); + + // iterate through all the attribute keypairs + // Watch out for name collisions: $key has previously been used + foreach ($attr as $attr_key => $value) { + + // call the definition + if (isset($defs[$attr_key])) { + // there is a local definition defined + if ($defs[$attr_key] === false) { + // We've explicitly been told not to allow this element. + // This is usually when there's a global definition + // that must be overridden. + // Theoretically speaking, we could have a + // AttrDef_DenyAll, but this is faster! + $result = false; + } else { + // validate according to the element's definition + $result = $defs[$attr_key]->validate( + $value, + $config, + $context + ); + } + } elseif (isset($d_defs[$attr_key])) { + // there is a global definition defined, validate according + // to the global definition + $result = $d_defs[$attr_key]->validate( + $value, + $config, + $context + ); + } else { + // system never heard of the attribute? DELETE! + $result = false; + } + + // put the results into effect + if ($result === false || $result === null) { + // this is a generic error message that should replaced + // with more specific ones when possible + if ($e) { + $e->send(E_ERROR, 'AttrValidator: Attribute removed'); + } + + // remove the attribute + unset($attr[$attr_key]); + } elseif (is_string($result)) { + // generally, if a substitution is happening, there + // was some sort of implicit correction going on. We'll + // delegate it to the attribute classes to say exactly what. + + // simple substitution + $attr[$attr_key] = $result; + } else { + // nothing happens + } + + // we'd also want slightly more complicated substitution + // involving an array as the return value, + // although we're not sure how colliding attributes would + // resolve (certain ones would be completely overriden, + // others would prepend themselves). + } + + $context->destroy('CurrentAttr'); + + // post transforms + + // global (error reporting untested) + foreach ($definition->info_attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // local (error reporting untested) + foreach ($definition->info[$token->name]->attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + $token->attr = $attr; + + // destroy CurrentToken if we made it ourselves + if (!$current_token) { + $context->destroy('CurrentToken'); + } + + } + + +} + + + + + +// constants are slow, so we use as few as possible +if (!defined('HTMLPURIFIER_PREFIX')) { + define('HTMLPURIFIER_PREFIX', dirname(__FILE__) . '/standalone'); + set_include_path(HTMLPURIFIER_PREFIX . PATH_SEPARATOR . get_include_path()); +} + +// accomodations for versions earlier than 5.0.2 +// borrowed from PHP_Compat, LGPL licensed, by Aidan Lister +if (!defined('PHP_EOL')) { + switch (strtoupper(substr(PHP_OS, 0, 3))) { + case 'WIN': + define('PHP_EOL', "\r\n"); + break; + case 'DAR': + define('PHP_EOL', "\r"); + break; + default: + define('PHP_EOL', "\n"); + } +} + +/** + * Bootstrap class that contains meta-functionality for HTML Purifier such as + * the autoload function. + * + * @note + * This class may be used without any other files from HTML Purifier. + */ +class HTMLPurifier_Bootstrap +{ + + /** + * Autoload function for HTML Purifier + * @param string $class Class to load + * @return bool + */ + public static function autoload($class) + { + $file = HTMLPurifier_Bootstrap::getPath($class); + if (!$file) { + return false; + } + // Technically speaking, it should be ok and more efficient to + // just do 'require', but Antonio Parraga reports that with + // Zend extensions such as Zend debugger and APC, this invariant + // may be broken. Since we have efficient alternatives, pay + // the cost here and avoid the bug. + require_once HTMLPURIFIER_PREFIX . '/' . $file; + return true; + } + + /** + * Returns the path for a specific class. + * @param string $class Class path to get + * @return string + */ + public static function getPath($class) + { + if (strncmp('HTMLPurifier', $class, 12) !== 0) { + return false; + } + // Custom implementations + if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) { + $code = str_replace('_', '-', substr($class, 22)); + $file = 'HTMLPurifier/Language/classes/' . $code . '.php'; + } else { + $file = str_replace('_', '/', $class) . '.php'; + } + if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) { + return false; + } + return $file; + } + + /** + * "Pre-registers" our autoloader on the SPL stack. + */ + public static function registerAutoload() + { + $autoload = array('HTMLPurifier_Bootstrap', 'autoload'); + if (($funcs = spl_autoload_functions()) === false) { + spl_autoload_register($autoload); + } elseif (function_exists('spl_autoload_unregister')) { + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + // prepend flag exists, no need for shenanigans + spl_autoload_register($autoload, true, true); + } else { + $buggy = version_compare(PHP_VERSION, '5.2.11', '<'); + $compat = version_compare(PHP_VERSION, '5.1.2', '<=') && + version_compare(PHP_VERSION, '5.1.0', '>='); + foreach ($funcs as $func) { + if ($buggy && is_array($func)) { + // :TRICKY: There are some compatibility issues and some + // places where we need to error out + $reflector = new ReflectionMethod($func[0], $func[1]); + if (!$reflector->isStatic()) { + throw new Exception( + 'HTML Purifier autoloader registrar is not compatible + with non-static object methods due to PHP Bug #44144; + Please do not use HTMLPurifier.autoload.php (or any + file that includes this file); instead, place the code: + spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\')) + after your own autoloaders.' + ); + } + // Suprisingly, spl_autoload_register supports the + // Class::staticMethod callback format, although call_user_func doesn't + if ($compat) { + $func = implode('::', $func); + } + } + spl_autoload_unregister($func); + } + spl_autoload_register($autoload); + foreach ($funcs as $func) { + spl_autoload_register($func); + } + } + } + } +} + + + + + +/** + * Super-class for definition datatype objects, implements serialization + * functions for the class. + */ +abstract class HTMLPurifier_Definition +{ + + /** + * Has setup() been called yet? + * @type bool + */ + public $setup = false; + + /** + * If true, write out the final definition object to the cache after + * setup. This will be true only if all invocations to get a raw + * definition object are also optimized. This does not cause file + * system thrashing because on subsequent calls the cached object + * is used and any writes to the raw definition object are short + * circuited. See enduser-customize.html for the high-level + * picture. + * @type bool + */ + public $optimized = null; + + /** + * What type of definition is it? + * @type string + */ + public $type; + + /** + * Sets up the definition object into the final form, something + * not done by the constructor + * @param HTMLPurifier_Config $config + */ + abstract protected function doSetup($config); + + /** + * Setup function that aborts if already setup + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + if ($this->setup) { + return; + } + $this->setup = true; + $this->doSetup($config); + } +} + + + + + +/** + * Defines allowed CSS attributes and what their values are. + * @see HTMLPurifier_HTMLDefinition + */ +class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition +{ + + public $type = 'CSS'; + + /** + * Assoc array of attribute name to definition object. + * @type HTMLPurifier_AttrDef[] + */ + public $info = array(); + + /** + * Constructs the info array. The meat of this class. + * @param HTMLPurifier_Config $config + */ + protected function doSetup($config) + { + $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum( + array('left', 'right', 'center', 'justify'), + false + ); + + $border_style = + $this->info['border-bottom-style'] = + $this->info['border-right-style'] = + $this->info['border-left-style'] = + $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum( + array( + 'none', + 'hidden', + 'dotted', + 'dashed', + 'solid', + 'double', + 'groove', + 'ridge', + 'inset', + 'outset' + ), + false + ); + + $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style); + + $this->info['clear'] = new HTMLPurifier_AttrDef_Enum( + array('none', 'left', 'right', 'both'), + false + ); + $this->info['float'] = new HTMLPurifier_AttrDef_Enum( + array('none', 'left', 'right'), + false + ); + $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum( + array('normal', 'italic', 'oblique'), + false + ); + $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum( + array('normal', 'small-caps'), + false + ); + + $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('none')), + new HTMLPurifier_AttrDef_CSS_URI() + ) + ); + + $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum( + array('inside', 'outside'), + false + ); + $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum( + array( + 'disc', + 'circle', + 'square', + 'decimal', + 'lower-roman', + 'upper-roman', + 'lower-alpha', + 'upper-alpha', + 'none' + ), + false + ); + $this->info['list-style-image'] = $uri_or_none; + + $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config); + + $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum( + array('capitalize', 'uppercase', 'lowercase', 'none'), + false + ); + $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + $this->info['background-image'] = $uri_or_none; + $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum( + array('repeat', 'repeat-x', 'repeat-y', 'no-repeat') + ); + $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum( + array('scroll', 'fixed') + ); + $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition(); + + $border_color = + $this->info['border-top-color'] = + $this->info['border-bottom-color'] = + $this->info['border-left-color'] = + $this->info['border-right-color'] = + $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('transparent')), + new HTMLPurifier_AttrDef_CSS_Color() + ) + ); + + $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config); + + $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color); + + $border_width = + $this->info['border-top-width'] = + $this->info['border-bottom-width'] = + $this->info['border-left-width'] = + $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')), + new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative + ) + ); + + $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width); + + $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'xx-small', + 'x-small', + 'small', + 'medium', + 'large', + 'x-large', + 'xx-large', + 'larger', + 'smaller' + ) + ), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ) + ); + + $margin = + $this->info['margin-top'] = + $this->info['margin-bottom'] = + $this->info['margin-left'] = + $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ); + + $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin); + + // non-negative + $padding = + $this->info['padding-top'] = + $this->info['padding-bottom'] = + $this->info['padding-left'] = + $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ) + ); + + $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding); + + $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ) + ); + + $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ); + $max = $config->get('CSS.MaxImgLength'); + + $this->info['width'] = + $this->info['height'] = + $max === null ? + $trusted_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ), + // For everyone else: + $trusted_wh + ); + + $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); + + $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); + + // this could use specialized code + $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum( + array( + 'normal', + 'bold', + 'bolder', + 'lighter', + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900' + ), + false + ); + + // MUST be called after other font properties, as it references + // a CSSDefinition object + $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config); + + // same here + $this->info['border'] = + $this->info['border-bottom'] = + $this->info['border-top'] = + $this->info['border-left'] = + $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config); + + $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum( + array('collapse', 'separate') + ); + + $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum( + array('top', 'bottom') + ); + + $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum( + array('auto', 'fixed') + ); + + $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'baseline', + 'sub', + 'super', + 'top', + 'text-top', + 'middle', + 'bottom', + 'text-bottom' + ) + ), + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ) + ); + + $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2); + + // These CSS properties don't work on many browsers, but we live + // in THE FUTURE! + $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum( + array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line') + ); + + if ($config->get('CSS.Proprietary')) { + $this->doSetupProprietary($config); + } + + if ($config->get('CSS.AllowTricky')) { + $this->doSetupTricky($config); + } + + if ($config->get('CSS.Trusted')) { + $this->doSetupTrusted($config); + } + + $allow_important = $config->get('CSS.AllowImportant'); + // wrap all attr-defs with decorator that handles !important + foreach ($this->info as $k => $v) { + $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important); + } + + $this->setupConfigStuff($config); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupProprietary($config) + { + // Internet Explorer only scrollbar colors + $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + // technically not proprietary, but CSS3, and no one supports it + $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + + // only opacity, for now + $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter(); + + // more CSS3 + $this->info['page-break-after'] = + $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum( + array( + 'auto', + 'always', + 'avoid', + 'left', + 'right' + ) + ); + $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto', 'avoid')); + + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTricky($config) + { + $this->info['display'] = new HTMLPurifier_AttrDef_Enum( + array( + 'inline', + 'block', + 'list-item', + 'run-in', + 'compact', + 'marker', + 'table', + 'inline-block', + 'inline-table', + 'table-row-group', + 'table-header-group', + 'table-footer-group', + 'table-row', + 'table-column-group', + 'table-column', + 'table-cell', + 'table-caption', + 'none' + ) + ); + $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum( + array('visible', 'hidden', 'collapse') + ); + $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll')); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTrusted($config) + { + $this->info['position'] = new HTMLPurifier_AttrDef_Enum( + array('static', 'relative', 'absolute', 'fixed') + ); + $this->info['top'] = + $this->info['left'] = + $this->info['right'] = + $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Integer(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + } + + /** + * Performs extra config-based processing. Based off of + * HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_Config $config + * @todo Refactor duplicate elements into common class (probably using + * composition, not inheritance). + */ + protected function setupConfigStuff($config) + { + // setup allowed elements + $support = "(for information on implementing this, see the " . + "support forums) "; + $allowed_properties = $config->get('CSS.AllowedProperties'); + if ($allowed_properties !== null) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_properties[$name])) { + unset($this->info[$name]); + } + unset($allowed_properties[$name]); + } + // emit errors + foreach ($allowed_properties as $name => $d) { + // :TODO: Is this htmlspecialchars() call really necessary? + $name = htmlspecialchars($name); + trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING); + } + } + + $forbidden_properties = $config->get('CSS.ForbiddenProperties'); + if ($forbidden_properties !== null) { + foreach ($this->info as $name => $d) { + if (isset($forbidden_properties[$name])) { + unset($this->info[$name]); + } + } + } + } +} + + + + + +/** + * Defines allowed child nodes and validates nodes against it. + */ +abstract class HTMLPurifier_ChildDef +{ + /** + * Type of child definition, usually right-most part of class name lowercase. + * Used occasionally in terms of context. + * @type string + */ + public $type; + + /** + * Indicates whether or not an empty array of children is okay. + * + * This is necessary for redundant checking when changes affecting + * a child node may cause a parent node to now be disallowed. + * @type bool + */ + public $allow_empty; + + /** + * Lookup array of all elements that this definition could possibly allow. + * @type array + */ + public $elements = array(); + + /** + * Get lookup of tag names that should not close this element automatically. + * All other elements will do so. + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @return array + */ + public function getAllowedElements($config) + { + return $this->elements; + } + + /** + * Validates nodes according to definition and returns modification. + * + * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @param HTMLPurifier_Context $context HTMLPurifier_Context object + * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children + */ + abstract public function validateChildren($children, $config, $context); +} + + + + + +/** + * Configuration object that triggers customizable behavior. + * + * @warning This class is strongly defined: that means that the class + * will fail if an undefined directive is retrieved or set. + * + * @note Many classes that could (although many times don't) use the + * configuration object make it a mandatory parameter. This is + * because a configuration object should always be forwarded, + * otherwise, you run the risk of missing a parameter and then + * being stumped when a configuration directive doesn't work. + * + * @todo Reconsider some of the public member variables + */ +class HTMLPurifier_Config +{ + + /** + * HTML Purifier's version + * @type string + */ + public $version = '4.6.0'; + + /** + * Whether or not to automatically finalize + * the object if a read operation is done. + * @type bool + */ + public $autoFinalize = true; + + // protected member variables + + /** + * Namespace indexed array of serials for specific namespaces. + * @see getSerial() for more info. + * @type string[] + */ + protected $serials = array(); + + /** + * Serial for entire configuration object. + * @type string + */ + protected $serial; + + /** + * Parser for variables. + * @type HTMLPurifier_VarParser_Flexible + */ + protected $parser = null; + + /** + * Reference HTMLPurifier_ConfigSchema for value checking. + * @type HTMLPurifier_ConfigSchema + * @note This is public for introspective purposes. Please don't + * abuse! + */ + public $def; + + /** + * Indexed array of definitions. + * @type HTMLPurifier_Definition[] + */ + protected $definitions; + + /** + * Whether or not config is finalized. + * @type bool + */ + protected $finalized = false; + + /** + * Property list containing configuration directives. + * @type array + */ + protected $plist; + + /** + * Whether or not a set is taking place due to an alias lookup. + * @type bool + */ + private $aliasMode; + + /** + * Set to false if you do not want line and file numbers in errors. + * (useful when unit testing). This will also compress some errors + * and exceptions. + * @type bool + */ + public $chatty = true; + + /** + * Current lock; only gets to this namespace are allowed. + * @type string + */ + private $lock; + + /** + * Constructor + * @param HTMLPurifier_ConfigSchema $definition ConfigSchema that defines + * what directives are allowed. + * @param HTMLPurifier_PropertyList $parent + */ + public function __construct($definition, $parent = null) + { + $parent = $parent ? $parent : $definition->defaultPlist; + $this->plist = new HTMLPurifier_PropertyList($parent); + $this->def = $definition; // keep a copy around for checking + $this->parser = new HTMLPurifier_VarParser_Flexible(); + } + + /** + * Convenience constructor that creates a config object based on a mixed var + * @param mixed $config Variable that defines the state of the config + * object. Can be: a HTMLPurifier_Config() object, + * an array of directives based on loadArray(), + * or a string filename of an ini file. + * @param HTMLPurifier_ConfigSchema $schema Schema object + * @return HTMLPurifier_Config Configured object + */ + public static function create($config, $schema = null) + { + if ($config instanceof HTMLPurifier_Config) { + // pass-through + return $config; + } + if (!$schema) { + $ret = HTMLPurifier_Config::createDefault(); + } else { + $ret = new HTMLPurifier_Config($schema); + } + if (is_string($config)) { + $ret->loadIni($config); + } elseif (is_array($config)) $ret->loadArray($config); + return $ret; + } + + /** + * Creates a new config object that inherits from a previous one. + * @param HTMLPurifier_Config $config Configuration object to inherit from. + * @return HTMLPurifier_Config object with $config as its parent. + */ + public static function inherit(HTMLPurifier_Config $config) + { + return new HTMLPurifier_Config($config->def, $config->plist); + } + + /** + * Convenience constructor that creates a default configuration object. + * @return HTMLPurifier_Config default object. + */ + public static function createDefault() + { + $definition = HTMLPurifier_ConfigSchema::instance(); + $config = new HTMLPurifier_Config($definition); + return $config; + } + + /** + * Retrieves a value from the configuration. + * + * @param string $key String key + * @param mixed $a + * + * @return mixed + */ + public function get($key, $a = null) + { + if ($a !== null) { + $this->triggerError( + "Using deprecated API: use \$config->get('$key.$a') instead", + E_USER_WARNING + ); + $key = "$key.$a"; + } + if (!$this->finalized) { + $this->autoFinalize(); + } + if (!isset($this->def->info[$key])) { + // can't add % due to SimpleTest bug + $this->triggerError( + 'Cannot retrieve value of undefined directive ' . htmlspecialchars($key), + E_USER_WARNING + ); + return; + } + if (isset($this->def->info[$key]->isAlias)) { + $d = $this->def->info[$key]; + $this->triggerError( + 'Cannot get value from aliased directive, use real name ' . $d->key, + E_USER_ERROR + ); + return; + } + if ($this->lock) { + list($ns) = explode('.', $key); + if ($ns !== $this->lock) { + $this->triggerError( + 'Cannot get value of namespace ' . $ns . ' when lock for ' . + $this->lock . + ' is active, this probably indicates a Definition setup method ' . + 'is accessing directives that are not within its namespace', + E_USER_ERROR + ); + return; + } + } + return $this->plist->get($key); + } + + /** + * Retrieves an array of directives to values from a given namespace + * + * @param string $namespace String namespace + * + * @return array + */ + public function getBatch($namespace) + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $full = $this->getAll(); + if (!isset($full[$namespace])) { + $this->triggerError( + 'Cannot retrieve undefined namespace ' . + htmlspecialchars($namespace), + E_USER_WARNING + ); + return; + } + return $full[$namespace]; + } + + /** + * Returns a SHA-1 signature of a segment of the configuration object + * that uniquely identifies that particular configuration + * + * @param string $namespace Namespace to get serial for + * + * @return string + * @note Revision is handled specially and is removed from the batch + * before processing! + */ + public function getBatchSerial($namespace) + { + if (empty($this->serials[$namespace])) { + $batch = $this->getBatch($namespace); + unset($batch['DefinitionRev']); + $this->serials[$namespace] = sha1(serialize($batch)); + } + return $this->serials[$namespace]; + } + + /** + * Returns a SHA-1 signature for the entire configuration object + * that uniquely identifies that particular configuration + * + * @return string + */ + public function getSerial() + { + if (empty($this->serial)) { + $this->serial = sha1(serialize($this->getAll())); + } + return $this->serial; + } + + /** + * Retrieves all directives, organized by namespace + * + * @warning This is a pretty inefficient function, avoid if you can + */ + public function getAll() + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $ret = array(); + foreach ($this->plist->squash() as $name => $value) { + list($ns, $key) = explode('.', $name, 2); + $ret[$ns][$key] = $value; + } + return $ret; + } + + /** + * Sets a value to configuration. + * + * @param string $key key + * @param mixed $value value + * @param mixed $a + */ + public function set($key, $value, $a = null) + { + if (strpos($key, '.') === false) { + $namespace = $key; + $directive = $value; + $value = $a; + $key = "$key.$directive"; + $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE); + } else { + list($namespace) = explode('.', $key); + } + if ($this->isFinalized('Cannot set directive after finalization')) { + return; + } + if (!isset($this->def->info[$key])) { + $this->triggerError( + 'Cannot set undefined directive ' . htmlspecialchars($key) . ' to value', + E_USER_WARNING + ); + return; + } + $def = $this->def->info[$key]; + + if (isset($def->isAlias)) { + if ($this->aliasMode) { + $this->triggerError( + 'Double-aliases not allowed, please fix '. + 'ConfigSchema bug with' . $key, + E_USER_ERROR + ); + return; + } + $this->aliasMode = true; + $this->set($def->key, $value); + $this->aliasMode = false; + $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE); + return; + } + + // Raw type might be negative when using the fully optimized form + // of stdclass, which indicates allow_null == true + $rtype = is_int($def) ? $def : $def->type; + if ($rtype < 0) { + $type = -$rtype; + $allow_null = true; + } else { + $type = $rtype; + $allow_null = isset($def->allow_null); + } + + try { + $value = $this->parser->parse($value, $type, $allow_null); + } catch (HTMLPurifier_VarParserException $e) { + $this->triggerError( + 'Value for ' . $key . ' is of invalid type, should be ' . + HTMLPurifier_VarParser::getTypeName($type), + E_USER_WARNING + ); + return; + } + if (is_string($value) && is_object($def)) { + // resolve value alias if defined + if (isset($def->aliases[$value])) { + $value = $def->aliases[$value]; + } + // check to see if the value is allowed + if (isset($def->allowed) && !isset($def->allowed[$value])) { + $this->triggerError( + 'Value not supported, valid values are: ' . + $this->_listify($def->allowed), + E_USER_WARNING + ); + return; + } + } + $this->plist->set($key, $value); + + // reset definitions if the directives they depend on changed + // this is a very costly process, so it's discouraged + // with finalization + if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') { + $this->definitions[$namespace] = null; + } + + $this->serials[$namespace] = false; + } + + /** + * Convenience function for error reporting + * + * @param array $lookup + * + * @return string + */ + private function _listify($lookup) + { + $list = array(); + foreach ($lookup as $name => $b) { + $list[] = $name; + } + return implode(', ', $list); + } + + /** + * Retrieves object reference to the HTML definition. + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawHTMLDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_HTMLDefinition + */ + public function getHTMLDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('HTML', $raw, $optimized); + } + + /** + * Retrieves object reference to the CSS definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawCSSDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_CSSDefinition + */ + public function getCSSDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('CSS', $raw, $optimized); + } + + /** + * Retrieves object reference to the URI definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawURIDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_URIDefinition + */ + public function getURIDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('URI', $raw, $optimized); + } + + /** + * Retrieves a definition + * + * @param string $type Type of definition: HTML, CSS, etc + * @param bool $raw Whether or not definition should be returned raw + * @param bool $optimized Only has an effect when $raw is true. Whether + * or not to return null if the result is already present in + * the cache. This is off by default for backwards + * compatibility reasons, but you need to do things this + * way in order to ensure that caching is done properly. + * Check out enduser-customize.html for more details. + * We probably won't ever change this default, as much as the + * maybe semantics is the "right thing to do." + * + * @throws HTMLPurifier_Exception + * @return HTMLPurifier_Definition + */ + public function getDefinition($type, $raw = false, $optimized = false) + { + if ($optimized && !$raw) { + throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false"); + } + if (!$this->finalized) { + $this->autoFinalize(); + } + // temporarily suspend locks, so we can handle recursive definition calls + $lock = $this->lock; + $this->lock = null; + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create($type, $this); + $this->lock = $lock; + if (!$raw) { + // full definition + // --------------- + // check if definition is in memory + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + // check if the definition is setup + if ($def->setup) { + return $def; + } else { + $def->setup($this); + if ($def->optimized) { + $cache->add($def, $this); + } + return $def; + } + } + // check if definition is in cache + $def = $cache->get($this); + if ($def) { + // definition in cache, save to memory and return it + $this->definitions[$type] = $def; + return $def; + } + // initialize it + $def = $this->initDefinition($type); + // set it up + $this->lock = $type; + $def->setup($this); + $this->lock = null; + // save in cache + $cache->add($def, $this); + // return it + return $def; + } else { + // raw definition + // -------------- + // check preconditions + $def = null; + if ($optimized) { + if (is_null($this->get($type . '.DefinitionID'))) { + // fatally error out if definition ID not set + throw new HTMLPurifier_Exception( + "Cannot retrieve raw version without specifying %$type.DefinitionID" + ); + } + } + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + if ($def->setup && !$optimized) { + $extra = $this->chatty ? + " (try moving this code block earlier in your initialization)" : + ""; + throw new HTMLPurifier_Exception( + "Cannot retrieve raw definition after it has already been setup" . + $extra + ); + } + if ($def->optimized === null) { + $extra = $this->chatty ? " (try flushing your cache)" : ""; + throw new HTMLPurifier_Exception( + "Optimization status of definition is unknown" . $extra + ); + } + if ($def->optimized !== $optimized) { + $msg = $optimized ? "optimized" : "unoptimized"; + $extra = $this->chatty ? + " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" + : ""; + throw new HTMLPurifier_Exception( + "Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra + ); + } + } + // check if definition was in memory + if ($def) { + if ($def->setup) { + // invariant: $optimized === true (checked above) + return null; + } else { + return $def; + } + } + // if optimized, check if definition was in cache + // (because we do the memory check first, this formulation + // is prone to cache slamming, but I think + // guaranteeing that either /all/ of the raw + // setup code or /none/ of it is run is more important.) + if ($optimized) { + // This code path only gets run once; once we put + // something in $definitions (which is guaranteed by the + // trailing code), we always short-circuit above. + $def = $cache->get($this); + if ($def) { + // save the full definition for later, but don't + // return it yet + $this->definitions[$type] = $def; + return null; + } + } + // check invariants for creation + if (!$optimized) { + if (!is_null($this->get($type . '.DefinitionID'))) { + if ($this->chatty) { + $this->triggerError( + 'Due to a documentation error in previous version of HTML Purifier, your ' . + 'definitions are not being cached. If this is OK, you can remove the ' . + '%$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, ' . + 'modify your code to use maybeGetRawDefinition, and test if the returned ' . + 'value is null before making any edits (if it is null, that means that a ' . + 'cached version is available, and no raw operations are necessary). See ' . + '' . + 'Customize for more details', + E_USER_WARNING + ); + } else { + $this->triggerError( + "Useless DefinitionID declaration", + E_USER_WARNING + ); + } + } + } + // initialize it + $def = $this->initDefinition($type); + $def->optimized = $optimized; + return $def; + } + throw new HTMLPurifier_Exception("The impossible happened!"); + } + + /** + * Initialise definition + * + * @param string $type What type of definition to create + * + * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition + * @throws HTMLPurifier_Exception + */ + private function initDefinition($type) + { + // quick checks failed, let's create the object + if ($type == 'HTML') { + $def = new HTMLPurifier_HTMLDefinition(); + } elseif ($type == 'CSS') { + $def = new HTMLPurifier_CSSDefinition(); + } elseif ($type == 'URI') { + $def = new HTMLPurifier_URIDefinition(); + } else { + throw new HTMLPurifier_Exception( + "Definition of $type type not supported" + ); + } + $this->definitions[$type] = $def; + return $def; + } + + public function maybeGetRawDefinition($name) + { + return $this->getDefinition($name, true, true); + } + + public function maybeGetRawHTMLDefinition() + { + return $this->getDefinition('HTML', true, true); + } + + public function maybeGetRawCSSDefinition() + { + return $this->getDefinition('CSS', true, true); + } + + public function maybeGetRawURIDefinition() + { + return $this->getDefinition('URI', true, true); + } + + /** + * Loads configuration values from an array with the following structure: + * Namespace.Directive => Value + * + * @param array $config_array Configuration associative array + */ + public function loadArray($config_array) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + foreach ($config_array as $key => $value) { + $key = str_replace('_', '.', $key); + if (strpos($key, '.') !== false) { + $this->set($key, $value); + } else { + $namespace = $key; + $namespace_values = $value; + foreach ($namespace_values as $directive => $value2) { + $this->set($namespace .'.'. $directive, $value2); + } + } + } + } + + /** + * Returns a list of array(namespace, directive) for all directives + * that are allowed in a web-form context as per an allowed + * namespaces/directives list. + * + * @param array $allowed List of allowed namespaces/directives + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function getAllowedDirectivesForForm($allowed, $schema = null) + { + if (!$schema) { + $schema = HTMLPurifier_ConfigSchema::instance(); + } + if ($allowed !== true) { + if (is_string($allowed)) { + $allowed = array($allowed); + } + $allowed_ns = array(); + $allowed_directives = array(); + $blacklisted_directives = array(); + foreach ($allowed as $ns_or_directive) { + if (strpos($ns_or_directive, '.') !== false) { + // directive + if ($ns_or_directive[0] == '-') { + $blacklisted_directives[substr($ns_or_directive, 1)] = true; + } else { + $allowed_directives[$ns_or_directive] = true; + } + } else { + // namespace + $allowed_ns[$ns_or_directive] = true; + } + } + } + $ret = array(); + foreach ($schema->info as $key => $def) { + list($ns, $directive) = explode('.', $key, 2); + if ($allowed !== true) { + if (isset($blacklisted_directives["$ns.$directive"])) { + continue; + } + if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) { + continue; + } + } + if (isset($def->isAlias)) { + continue; + } + if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') { + continue; + } + $ret[] = array($ns, $directive); + } + return $ret; + } + + /** + * Loads configuration values from $_GET/$_POST that were posted + * via ConfigForm + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return mixed + */ + public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema); + $config = HTMLPurifier_Config::create($ret, $schema); + return $config; + } + + /** + * Merges in configuration values from $_GET/$_POST to object. NOT STATIC. + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + */ + public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def); + $this->loadArray($ret); + } + + /** + * Prepares an array from a form into something usable for the more + * strict parts of HTMLPurifier_Config + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + if ($index !== false) { + $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); + } + $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc(); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema); + $ret = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $skey = "$ns.$directive"; + if (!empty($array["Null_$skey"])) { + $ret[$ns][$directive] = null; + continue; + } + if (!isset($array[$skey])) { + continue; + } + $value = $mq ? stripslashes($array[$skey]) : $array[$skey]; + $ret[$ns][$directive] = $value; + } + return $ret; + } + + /** + * Loads configuration values from an ini file + * + * @param string $filename Name of ini file + */ + public function loadIni($filename) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + $array = parse_ini_file($filename, true); + $this->loadArray($array); + } + + /** + * Checks whether or not the configuration object is finalized. + * + * @param string|bool $error String error message, or false for no error + * + * @return bool + */ + public function isFinalized($error = false) + { + if ($this->finalized && $error) { + $this->triggerError($error, E_USER_ERROR); + } + return $this->finalized; + } + + /** + * Finalizes configuration only if auto finalize is on and not + * already finalized + */ + public function autoFinalize() + { + if ($this->autoFinalize) { + $this->finalize(); + } else { + $this->plist->squash(true); + } + } + + /** + * Finalizes a configuration object, prohibiting further change + */ + public function finalize() + { + $this->finalized = true; + $this->parser = null; + } + + /** + * Produces a nicely formatted error message by supplying the + * stack frame information OUTSIDE of HTMLPurifier_Config. + * + * @param string $msg An error message + * @param int $no An error number + */ + protected function triggerError($msg, $no) + { + // determine previous stack frame + $extra = ''; + if ($this->chatty) { + $trace = debug_backtrace(); + // zip(tail(trace), trace) -- but PHP is not Haskell har har + for ($i = 0, $c = count($trace); $i < $c - 1; $i++) { + // XXX this is not correct on some versions of HTML Purifier + if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') { + continue; + } + $frame = $trace[$i]; + $extra = " invoked on line {$frame['line']} in file {$frame['file']}"; + break; + } + } + trigger_error($msg . $extra, $no); + } + + /** + * Returns a serialized form of the configuration object that can + * be reconstituted. + * + * @return string + */ + public function serialize() + { + $this->getDefinition('HTML'); + $this->getDefinition('CSS'); + $this->getDefinition('URI'); + return serialize($this); + } + +} + + + + + +/** + * Configuration definition, defines directives and their defaults. + */ +class HTMLPurifier_ConfigSchema +{ + /** + * Defaults of the directives and namespaces. + * @type array + * @note This shares the exact same structure as HTMLPurifier_Config::$conf + */ + public $defaults = array(); + + /** + * The default property list. Do not edit this property list. + * @type array + */ + public $defaultPlist; + + /** + * Definition of the directives. + * The structure of this is: + * + * array( + * 'Namespace' => array( + * 'Directive' => new stdclass(), + * ) + * ) + * + * The stdclass may have the following properties: + * + * - If isAlias isn't set: + * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions + * - allow_null: If set, this directive allows null values + * - aliases: If set, an associative array of value aliases to real values + * - allowed: If set, a lookup array of allowed (string) values + * - If isAlias is set: + * - namespace: Namespace this directive aliases to + * - name: Directive name this directive aliases to + * + * In certain degenerate cases, stdclass will actually be an integer. In + * that case, the value is equivalent to an stdclass with the type + * property set to the integer. If the integer is negative, type is + * equal to the absolute value of integer, and allow_null is true. + * + * This class is friendly with HTMLPurifier_Config. If you need introspection + * about the schema, you're better of using the ConfigSchema_Interchange, + * which uses more memory but has much richer information. + * @type array + */ + public $info = array(); + + /** + * Application-wide singleton + * @type HTMLPurifier_ConfigSchema + */ + protected static $singleton; + + public function __construct() + { + $this->defaultPlist = new HTMLPurifier_PropertyList(); + } + + /** + * Unserializes the default ConfigSchema. + * @return HTMLPurifier_ConfigSchema + */ + public static function makeFromSerial() + { + $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'); + $r = unserialize($contents); + if (!$r) { + $hash = sha1($contents); + trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR); + } + return $r; + } + + /** + * Retrieves an instance of the application-wide configuration definition. + * @param HTMLPurifier_ConfigSchema $prototype + * @return HTMLPurifier_ConfigSchema + */ + public static function instance($prototype = null) + { + if ($prototype !== null) { + HTMLPurifier_ConfigSchema::$singleton = $prototype; + } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) { + HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial(); + } + return HTMLPurifier_ConfigSchema::$singleton; + } + + /** + * Defines a directive for configuration + * @warning Will fail of directive's namespace is defined. + * @warning This method's signature is slightly different from the legacy + * define() static method! Beware! + * @param string $key Name of directive + * @param mixed $default Default value of directive + * @param string $type Allowed type of the directive. See + * HTMLPurifier_DirectiveDef::$type for allowed values + * @param bool $allow_null Whether or not to allow null values + */ + public function add($key, $default, $type, $allow_null) + { + $obj = new stdclass(); + $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; + if ($allow_null) { + $obj->allow_null = true; + } + $this->info[$key] = $obj; + $this->defaults[$key] = $default; + $this->defaultPlist->set($key, $default); + } + + /** + * Defines a directive value alias. + * + * Directive value aliases are convenient for developers because it lets + * them set a directive to several values and get the same result. + * @param string $key Name of Directive + * @param array $aliases Hash of aliased values to the real alias + */ + public function addValueAliases($key, $aliases) + { + if (!isset($this->info[$key]->aliases)) { + $this->info[$key]->aliases = array(); + } + foreach ($aliases as $alias => $real) { + $this->info[$key]->aliases[$alias] = $real; + } + } + + /** + * Defines a set of allowed values for a directive. + * @warning This is slightly different from the corresponding static + * method definition. + * @param string $key Name of directive + * @param array $allowed Lookup array of allowed values + */ + public function addAllowedValues($key, $allowed) + { + $this->info[$key]->allowed = $allowed; + } + + /** + * Defines a directive alias for backwards compatibility + * @param string $key Directive that will be aliased + * @param string $new_key Directive that the alias will be to + */ + public function addAlias($key, $new_key) + { + $obj = new stdclass; + $obj->key = $new_key; + $obj->isAlias = true; + $this->info[$key] = $obj; + } + + /** + * Replaces any stdclass that only has the type property with type integer. + */ + public function postProcess() + { + foreach ($this->info as $key => $v) { + if (count((array) $v) == 1) { + $this->info[$key] = $v->type; + } elseif (count((array) $v) == 2 && isset($v->allow_null)) { + $this->info[$key] = -$v->type; + } + } + } +} + + + + + +/** + * @todo Unit test + */ +class HTMLPurifier_ContentSets +{ + + /** + * List of content set strings (pipe separators) indexed by name. + * @type array + */ + public $info = array(); + + /** + * List of content set lookups (element => true) indexed by name. + * @type array + * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets + */ + public $lookup = array(); + + /** + * Synchronized list of defined content sets (keys of info). + * @type array + */ + protected $keys = array(); + /** + * Synchronized list of defined content values (values of info). + * @type array + */ + protected $values = array(); + + /** + * Merges in module's content sets, expands identifiers in the content + * sets and populates the keys, values and lookup member variables. + * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule + */ + public function __construct($modules) + { + if (!is_array($modules)) { + $modules = array($modules); + } + // populate content_sets based on module hints + // sorry, no way of overloading + foreach ($modules as $module) { + foreach ($module->content_sets as $key => $value) { + $temp = $this->convertToLookup($value); + if (isset($this->lookup[$key])) { + // add it into the existing content set + $this->lookup[$key] = array_merge($this->lookup[$key], $temp); + } else { + $this->lookup[$key] = $temp; + } + } + } + $old_lookup = false; + while ($old_lookup !== $this->lookup) { + $old_lookup = $this->lookup; + foreach ($this->lookup as $i => $set) { + $add = array(); + foreach ($set as $element => $x) { + if (isset($this->lookup[$element])) { + $add += $this->lookup[$element]; + unset($this->lookup[$i][$element]); + } + } + $this->lookup[$i] += $add; + } + } + + foreach ($this->lookup as $key => $lookup) { + $this->info[$key] = implode(' | ', array_keys($lookup)); + } + $this->keys = array_keys($this->info); + $this->values = array_values($this->info); + } + + /** + * Accepts a definition; generates and assigns a ChildDef for it + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + */ + public function generateChildDef(&$def, $module) + { + if (!empty($def->child)) { // already done! + return; + } + $content_model = $def->content_model; + if (is_string($content_model)) { + // Assume that $this->keys is alphanumeric + $def->content_model = preg_replace_callback( + '/\b(' . implode('|', $this->keys) . ')\b/', + array($this, 'generateChildDefCallback'), + $content_model + ); + //$def->content_model = str_replace( + // $this->keys, $this->values, $content_model); + } + $def->child = $this->getChildDef($def, $module); + } + + public function generateChildDefCallback($matches) + { + return $this->info[$matches[0]]; + } + + /** + * Instantiates a ChildDef based on content_model and content_model_type + * member variables in HTMLPurifier_ElementDef + * @note This will also defer to modules for custom HTMLPurifier_ChildDef + * subclasses that need content set expansion + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + * @return HTMLPurifier_ChildDef corresponding to ElementDef + */ + public function getChildDef($def, $module) + { + $value = $def->content_model; + if (is_object($value)) { + trigger_error( + 'Literal object child definitions should be stored in '. + 'ElementDef->child not ElementDef->content_model', + E_USER_NOTICE + ); + return $value; + } + switch ($def->content_model_type) { + case 'required': + return new HTMLPurifier_ChildDef_Required($value); + case 'optional': + return new HTMLPurifier_ChildDef_Optional($value); + case 'empty': + return new HTMLPurifier_ChildDef_Empty(); + case 'custom': + return new HTMLPurifier_ChildDef_Custom($value); + } + // defer to its module + $return = false; + if ($module->defines_child_def) { // save a func call + $return = $module->getChildDef($def); + } + if ($return !== false) { + return $return; + } + // error-out + trigger_error( + 'Could not determine which ChildDef class to instantiate', + E_USER_ERROR + ); + return false; + } + + /** + * Converts a string list of elements separated by pipes into + * a lookup array. + * @param string $string List of elements + * @return array Lookup array of elements + */ + protected function convertToLookup($string) + { + $array = explode('|', str_replace(' ', '', $string)); + $ret = array(); + foreach ($array as $k) { + $ret[$k] = true; + } + return $ret; + } +} + + + + + +/** + * Registry object that contains information about the current context. + * @warning Is a bit buggy when variables are set to null: it thinks + * they don't exist! So use false instead, please. + * @note Since the variables Context deals with may not be objects, + * references are very important here! Do not remove! + */ +class HTMLPurifier_Context +{ + + /** + * Private array that stores the references. + * @type array + */ + private $_storage = array(); + + /** + * Registers a variable into the context. + * @param string $name String name + * @param mixed $ref Reference to variable to be registered + */ + public function register($name, &$ref) + { + if (array_key_exists($name, $this->_storage)) { + trigger_error( + "Name $name produces collision, cannot re-register", + E_USER_ERROR + ); + return; + } + $this->_storage[$name] =& $ref; + } + + /** + * Retrieves a variable reference from the context. + * @param string $name String name + * @param bool $ignore_error Boolean whether or not to ignore error + * @return mixed + */ + public function &get($name, $ignore_error = false) + { + if (!array_key_exists($name, $this->_storage)) { + if (!$ignore_error) { + trigger_error( + "Attempted to retrieve non-existent variable $name", + E_USER_ERROR + ); + } + $var = null; // so we can return by reference + return $var; + } + return $this->_storage[$name]; + } + + /** + * Destroys a variable in the context. + * @param string $name String name + */ + public function destroy($name) + { + if (!array_key_exists($name, $this->_storage)) { + trigger_error( + "Attempted to destroy non-existent variable $name", + E_USER_ERROR + ); + return; + } + unset($this->_storage[$name]); + } + + /** + * Checks whether or not the variable exists. + * @param string $name String name + * @return bool + */ + public function exists($name) + { + return array_key_exists($name, $this->_storage); + } + + /** + * Loads a series of variables from an associative array + * @param array $context_array Assoc array of variables to load + */ + public function loadArray($context_array) + { + foreach ($context_array as $key => $discard) { + $this->register($key, $context_array[$key]); + } + } +} + + + + + +/** + * Abstract class representing Definition cache managers that implements + * useful common methods and is a factory. + * @todo Create a separate maintenance file advanced users can use to + * cache their custom HTMLDefinition, which can be loaded + * via a configuration directive + * @todo Implement memcached + */ +abstract class HTMLPurifier_DefinitionCache +{ + /** + * @type string + */ + public $type; + + /** + * @param string $type Type of definition objects this instance of the + * cache will handle. + */ + public function __construct($type) + { + $this->type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @return string + */ + public function generateKey($config) + { + return $config->version . ',' . // possibly replace with function calls + $config->getBatchSerial($this->type) . ',' . + $config->get($this->type . '.DefinitionRev'); + } + + /** + * Tests whether or not a key is old with respect to the configuration's + * version and revision number. + * @param string $key Key to test + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against + * @return bool + */ + public function isOld($key, $config) + { + if (substr_count($key, ',') < 2) { + return true; + } + list($version, $hash, $revision) = explode(',', $key, 3); + $compare = version_compare($version, $config->version); + // version mismatch, is always old + if ($compare != 0) { + return true; + } + // versions match, ids match, check revision number + if ($hash == $config->getBatchSerial($this->type) && + $revision < $config->get($this->type . '.DefinitionRev')) { + return true; + } + return false; + } + + /** + * Checks if a definition's type jives with the cache's type + * @note Throws an error on failure + * @param HTMLPurifier_Definition $def Definition object to check + * @return bool true if good, false if not + */ + public function checkDefType($def) + { + if ($def->type !== $this->type) { + trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); + return false; + } + return true; + } + + /** + * Adds a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function add($def, $config); + + /** + * Unconditionally saves a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function set($def, $config); + + /** + * Replace an object in the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function replace($def, $config); + + /** + * Retrieves a definition object from the cache + * @param HTMLPurifier_Config $config + */ + abstract public function get($config); + + /** + * Removes a definition object to the cache + * @param HTMLPurifier_Config $config + */ + abstract public function remove($config); + + /** + * Clears all objects from cache + * @param HTMLPurifier_Config $config + */ + abstract public function flush($config); + + /** + * Clears all expired (older version or revision) objects from cache + * @note Be carefuly implementing this method as flush. Flush must + * not interfere with other Definition types, and cleanup() + * should not be repeatedly called by userland code. + * @param HTMLPurifier_Config $config + */ + abstract public function cleanup($config); +} + + + + + +/** + * Responsible for creating definition caches. + */ +class HTMLPurifier_DefinitionCacheFactory +{ + /** + * @type array + */ + protected $caches = array('Serializer' => array()); + + /** + * @type array + */ + protected $implementations = array(); + + /** + * @type HTMLPurifier_DefinitionCache_Decorator[] + */ + protected $decorators = array(); + + /** + * Initialize default decorators + */ + public function setup() + { + $this->addDecorator('Cleanup'); + } + + /** + * Retrieves an instance of global definition cache factory. + * @param HTMLPurifier_DefinitionCacheFactory $prototype + * @return HTMLPurifier_DefinitionCacheFactory + */ + public static function instance($prototype = null) + { + static $instance; + if ($prototype !== null) { + $instance = $prototype; + } elseif ($instance === null || $prototype === true) { + $instance = new HTMLPurifier_DefinitionCacheFactory(); + $instance->setup(); + } + return $instance; + } + + /** + * Registers a new definition cache object + * @param string $short Short name of cache object, for reference + * @param string $long Full class name of cache object, for construction + */ + public function register($short, $long) + { + $this->implementations[$short] = $long; + } + + /** + * Factory method that creates a cache object based on configuration + * @param string $type Name of definitions handled by cache + * @param HTMLPurifier_Config $config Config instance + * @return mixed + */ + public function create($type, $config) + { + $method = $config->get('Cache.DefinitionImpl'); + if ($method === null) { + return new HTMLPurifier_DefinitionCache_Null($type); + } + if (!empty($this->caches[$method][$type])) { + return $this->caches[$method][$type]; + } + if (isset($this->implementations[$method]) && + class_exists($class = $this->implementations[$method], false)) { + $cache = new $class($type); + } else { + if ($method != 'Serializer') { + trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING); + } + $cache = new HTMLPurifier_DefinitionCache_Serializer($type); + } + foreach ($this->decorators as $decorator) { + $new_cache = $decorator->decorate($cache); + // prevent infinite recursion in PHP 4 + unset($cache); + $cache = $new_cache; + } + $this->caches[$method][$type] = $cache; + return $this->caches[$method][$type]; + } + + /** + * Registers a decorator to add to all new cache objects + * @param HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator + */ + public function addDecorator($decorator) + { + if (is_string($decorator)) { + $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; + $decorator = new $class; + } + $this->decorators[$decorator->name] = $decorator; + } +} + + + + + +/** + * Represents a document type, contains information on which modules + * need to be loaded. + * @note This class is inspected by Printer_HTMLDefinition->renderDoctype. + * If structure changes, please update that function. + */ +class HTMLPurifier_Doctype +{ + /** + * Full name of doctype + * @type string + */ + public $name; + + /** + * List of standard modules (string identifiers or literal objects) + * that this doctype uses + * @type array + */ + public $modules = array(); + + /** + * List of modules to use for tidying up code + * @type array + */ + public $tidyModules = array(); + + /** + * Is the language derived from XML (i.e. XHTML)? + * @type bool + */ + public $xml = true; + + /** + * List of aliases for this doctype + * @type array + */ + public $aliases = array(); + + /** + * Public DTD identifier + * @type string + */ + public $dtdPublic; + + /** + * System DTD identifier + * @type string + */ + public $dtdSystem; + + public function __construct( + $name = null, + $xml = true, + $modules = array(), + $tidyModules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + $this->name = $name; + $this->xml = $xml; + $this->modules = $modules; + $this->tidyModules = $tidyModules; + $this->aliases = $aliases; + $this->dtdPublic = $dtd_public; + $this->dtdSystem = $dtd_system; + } +} + + + + + +class HTMLPurifier_DoctypeRegistry +{ + + /** + * Hash of doctype names to doctype objects. + * @type array + */ + protected $doctypes; + + /** + * Lookup table of aliases to real doctype names. + * @type array + */ + protected $aliases; + + /** + * Registers a doctype to the registry + * @note Accepts a fully-formed doctype object, or the + * parameters for constructing a doctype object + * @param string $doctype Name of doctype or literal doctype object + * @param bool $xml + * @param array $modules Modules doctype will load + * @param array $tidy_modules Modules doctype will load for certain modes + * @param array $aliases Alias names for doctype + * @param string $dtd_public + * @param string $dtd_system + * @return HTMLPurifier_Doctype Editable registered doctype + */ + public function register( + $doctype, + $xml = true, + $modules = array(), + $tidy_modules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + if (!is_array($modules)) { + $modules = array($modules); + } + if (!is_array($tidy_modules)) { + $tidy_modules = array($tidy_modules); + } + if (!is_array($aliases)) { + $aliases = array($aliases); + } + if (!is_object($doctype)) { + $doctype = new HTMLPurifier_Doctype( + $doctype, + $xml, + $modules, + $tidy_modules, + $aliases, + $dtd_public, + $dtd_system + ); + } + $this->doctypes[$doctype->name] = $doctype; + $name = $doctype->name; + // hookup aliases + foreach ($doctype->aliases as $alias) { + if (isset($this->doctypes[$alias])) { + continue; + } + $this->aliases[$alias] = $name; + } + // remove old aliases + if (isset($this->aliases[$name])) { + unset($this->aliases[$name]); + } + return $doctype; + } + + /** + * Retrieves reference to a doctype of a certain name + * @note This function resolves aliases + * @note When possible, use the more fully-featured make() + * @param string $doctype Name of doctype + * @return HTMLPurifier_Doctype Editable doctype object + */ + public function get($doctype) + { + if (isset($this->aliases[$doctype])) { + $doctype = $this->aliases[$doctype]; + } + if (!isset($this->doctypes[$doctype])) { + trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); + $anon = new HTMLPurifier_Doctype($doctype); + return $anon; + } + return $this->doctypes[$doctype]; + } + + /** + * Creates a doctype based on a configuration object, + * will perform initialization on the doctype + * @note Use this function to get a copy of doctype that config + * can hold on to (this is necessary in order to tell + * Generator whether or not the current document is XML + * based or not). + * @param HTMLPurifier_Config $config + * @return HTMLPurifier_Doctype + */ + public function make($config) + { + return clone $this->get($this->getDoctypeFromConfig($config)); + } + + /** + * Retrieves the doctype from the configuration object + * @param HTMLPurifier_Config $config + * @return string + */ + public function getDoctypeFromConfig($config) + { + // recommended test + $doctype = $config->get('HTML.Doctype'); + if (!empty($doctype)) { + return $doctype; + } + $doctype = $config->get('HTML.CustomDoctype'); + if (!empty($doctype)) { + return $doctype; + } + // backwards-compatibility + if ($config->get('HTML.XHTML')) { + $doctype = 'XHTML 1.0'; + } else { + $doctype = 'HTML 4.01'; + } + if ($config->get('HTML.Strict')) { + $doctype .= ' Strict'; + } else { + $doctype .= ' Transitional'; + } + return $doctype; + } +} + + + + + +/** + * Structure that stores an HTML element definition. Used by + * HTMLPurifier_HTMLDefinition and HTMLPurifier_HTMLModule. + * @note This class is inspected by HTMLPurifier_Printer_HTMLDefinition. + * Please update that class too. + * @warning If you add new properties to this class, you MUST update + * the mergeIn() method. + */ +class HTMLPurifier_ElementDef +{ + /** + * Does the definition work by itself, or is it created solely + * for the purpose of merging into another definition? + * @type bool + */ + public $standalone = true; + + /** + * Associative array of attribute name to HTMLPurifier_AttrDef. + * @type array + * @note Before being processed by HTMLPurifier_AttrCollections + * when modules are finalized during + * HTMLPurifier_HTMLDefinition->setup(), this array may also + * contain an array at index 0 that indicates which attribute + * collections to load into the full array. It may also + * contain string indentifiers in lieu of HTMLPurifier_AttrDef, + * see HTMLPurifier_AttrTypes on how they are expanded during + * HTMLPurifier_HTMLDefinition->setup() processing. + */ + public $attr = array(); + + // XXX: Design note: currently, it's not possible to override + // previously defined AttrTransforms without messing around with + // the final generated config. This is by design; a previous version + // used an associated list of attr_transform, but it was extremely + // easy to accidentally override other attribute transforms by + // forgetting to specify an index (and just using 0.) While we + // could check this by checking the index number and complaining, + // there is a second problem which is that it is not at all easy to + // tell when something is getting overridden. Combine this with a + // codebase where this isn't really being used, and it's perfect for + // nuking. + + /** + * List of tags HTMLPurifier_AttrTransform to be done before validation. + * @type array + */ + public $attr_transform_pre = array(); + + /** + * List of tags HTMLPurifier_AttrTransform to be done after validation. + * @type array + */ + public $attr_transform_post = array(); + + /** + * HTMLPurifier_ChildDef of this tag. + * @type HTMLPurifier_ChildDef + */ + public $child; + + /** + * Abstract string representation of internal ChildDef rules. + * @see HTMLPurifier_ContentSets for how this is parsed and then transformed + * into an HTMLPurifier_ChildDef. + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model; + + /** + * Value of $child->type, used to determine which ChildDef to use, + * used in combination with $content_model. + * @warning This must be lowercase + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model_type; + + /** + * Does the element have a content model (#PCDATA | Inline)*? This + * is important for chameleon ins and del processing in + * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't + * have to worry about this one. + * @type bool + */ + public $descendants_are_inline = false; + + /** + * List of the names of required attributes this element has. + * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement() + * @type array + */ + public $required_attr = array(); + + /** + * Lookup table of tags excluded from all descendants of this tag. + * @type array + * @note SGML permits exclusions for all descendants, but this is + * not possible with DTDs or XML Schemas. W3C has elected to + * use complicated compositions of content_models to simulate + * exclusion for children, but we go the simpler, SGML-style + * route of flat-out exclusions, which correctly apply to + * all descendants and not just children. Note that the XHTML + * Modularization Abstract Modules are blithely unaware of such + * distinctions. + */ + public $excludes = array(); + + /** + * This tag is explicitly auto-closed by the following tags. + * @type array + */ + public $autoclose = array(); + + /** + * If a foreign element is found in this element, test if it is + * allowed by this sub-element; if it is, instead of closing the + * current element, place it inside this element. + * @type string + */ + public $wrap; + + /** + * Whether or not this is a formatting element affected by the + * "Active Formatting Elements" algorithm. + * @type bool + */ + public $formatting; + + /** + * Low-level factory constructor for creating new standalone element defs + */ + public static function create($content_model, $content_model_type, $attr) + { + $def = new HTMLPurifier_ElementDef(); + $def->content_model = $content_model; + $def->content_model_type = $content_model_type; + $def->attr = $attr; + return $def; + } + + /** + * Merges the values of another element definition into this one. + * Values from the new element def take precedence if a value is + * not mergeable. + * @param HTMLPurifier_ElementDef $def + */ + public function mergeIn($def) + { + // later keys takes precedence + foreach ($def->attr as $k => $v) { + if ($k === 0) { + // merge in the includes + // sorry, no way to override an include + foreach ($v as $v2) { + $this->attr[0][] = $v2; + } + continue; + } + if ($v === false) { + if (isset($this->attr[$k])) { + unset($this->attr[$k]); + } + continue; + } + $this->attr[$k] = $v; + } + $this->_mergeAssocArray($this->excludes, $def->excludes); + $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); + $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); + + if (!empty($def->content_model)) { + $this->content_model = + str_replace("#SUPER", $this->content_model, $def->content_model); + $this->child = false; + } + if (!empty($def->content_model_type)) { + $this->content_model_type = $def->content_model_type; + $this->child = false; + } + if (!is_null($def->child)) { + $this->child = $def->child; + } + if (!is_null($def->formatting)) { + $this->formatting = $def->formatting; + } + if ($def->descendants_are_inline) { + $this->descendants_are_inline = $def->descendants_are_inline; + } + } + + /** + * Merges one array into another, removes values which equal false + * @param $a1 Array by reference that is merged into + * @param $a2 Array that merges into $a1 + */ + private function _mergeAssocArray(&$a1, $a2) + { + foreach ($a2 as $k => $v) { + if ($v === false) { + if (isset($a1[$k])) { + unset($a1[$k]); + } + continue; + } + $a1[$k] = $v; + } + } +} + + + + + +/** + * A UTF-8 specific character encoder that handles cleaning and transforming. + * @note All functions in this class should be static. + */ +class HTMLPurifier_Encoder +{ + + /** + * Constructor throws fatal error if you attempt to instantiate class + */ + private function __construct() + { + trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR); + } + + /** + * Error-handler that mutes errors, alternative to shut-up operator. + */ + public static function muteErrorHandler() + { + } + + /** + * iconv wrapper which mutes errors, but doesn't work around bugs. + * @param string $in Input encoding + * @param string $out Output encoding + * @param string $text The text to convert + * @return string + */ + public static function unsafeIconv($in, $out, $text) + { + set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); + $r = iconv($in, $out, $text); + restore_error_handler(); + return $r; + } + + /** + * iconv wrapper which mutes errors and works around bugs. + * @param string $in Input encoding + * @param string $out Output encoding + * @param string $text The text to convert + * @param int $max_chunk_size + * @return string + */ + public static function iconv($in, $out, $text, $max_chunk_size = 8000) + { + $code = self::testIconvTruncateBug(); + if ($code == self::ICONV_OK) { + return self::unsafeIconv($in, $out, $text); + } elseif ($code == self::ICONV_TRUNCATES) { + // we can only work around this if the input character set + // is utf-8 + if ($in == 'utf-8') { + if ($max_chunk_size < 4) { + trigger_error('max_chunk_size is too small', E_USER_WARNING); + return false; + } + // split into 8000 byte chunks, but be careful to handle + // multibyte boundaries properly + if (($c = strlen($text)) <= $max_chunk_size) { + return self::unsafeIconv($in, $out, $text); + } + $r = ''; + $i = 0; + while (true) { + if ($i + $max_chunk_size >= $c) { + $r .= self::unsafeIconv($in, $out, substr($text, $i)); + break; + } + // wibble the boundary + if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { + $chunk_size = $max_chunk_size; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { + $chunk_size = $max_chunk_size - 1; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { + $chunk_size = $max_chunk_size - 2; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { + $chunk_size = $max_chunk_size - 3; + } else { + return false; // rather confusing UTF-8... + } + $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths + $r .= self::unsafeIconv($in, $out, $chunk); + $i += $chunk_size; + } + return $r; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Cleans a UTF-8 string for well-formedness and SGML validity + * + * It will parse according to UTF-8 and return a valid UTF8 string, with + * non-SGML codepoints excluded. + * + * @param string $str The string to clean + * @param bool $force_php + * @return string + * + * @note Just for reference, the non-SGML code points are 0 to 31 and + * 127 to 159, inclusive. However, we allow code points 9, 10 + * and 13, which are the tab, line feed and carriage return + * respectively. 128 and above the code points map to multibyte + * UTF-8 representations. + * + * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and + * hsivonen@iki.fi at under the + * LGPL license. Notes on what changed are inside, but in general, + * the original code transformed UTF-8 text into an array of integer + * Unicode codepoints. Understandably, transforming that back to + * a string would be somewhat expensive, so the function was modded to + * directly operate on the string. However, this discourages code + * reuse, and the logic enumerated here would be useful for any + * function that needs to be able to understand UTF-8 characters. + * As of right now, only smart lossless character encoding converters + * would need that, and I'm probably not going to implement them. + * Once again, PHP 6 should solve all our problems. + */ + public static function cleanUTF8($str, $force_php = false) + { + // UTF-8 validity is checked since PHP 4.3.5 + // This is an optimization: if the string is already valid UTF-8, no + // need to do PHP stuff. 99% of the time, this will be the case. + // The regexp matches the XML char production, as well as well as excluding + // non-SGML codepoints U+007F to U+009F + if (preg_match( + '/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', + $str + )) { + return $str; + } + + $mState = 0; // cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // cached Unicode character + $mBytes = 1; // cached expected number of octets in the current sequence + + // original code involved an $out that was an array of Unicode + // codepoints. Instead of having to convert back into UTF-8, we've + // decided to directly append valid UTF-8 characters onto a string + // $out once they're done. $char accumulates raw bytes, while $mUcs4 + // turns into the Unicode code point, so there's some redundancy. + + $out = ''; + $char = ''; + + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $in = ord($str{$i}); + $char .= $str[$i]; // append byte to char + if (0 == $mState) { + // When mState is zero we expect either a US-ASCII character + // or a multi-octet sequence. + if (0 == (0x80 & ($in))) { + // US-ASCII, pass straight through. + if (($in <= 31 || $in == 127) && + !($in == 9 || $in == 13 || $in == 10) // save \r\t\n + ) { + // control characters, remove + } else { + $out .= $char; + } + // reset + $char = ''; + $mBytes = 1; + } elseif (0xC0 == (0xE0 & ($in))) { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } elseif (0xE0 == (0xF0 & ($in))) { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } elseif (0xF0 == (0xF8 & ($in))) { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } elseif (0xF8 == (0xFC & ($in))) { + // First octet of 5 octet sequence. + // + // This is illegal because the encoded codepoint must be + // either: + // (a) not the shortest form or + // (b) outside the Unicode range of 0-0x10FFFF. + // Rather than trying to resynchronize, we will carry on + // until the end of the sequence and let the later error + // handling code catch it. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } elseif (0xFC == (0xFE & ($in))) { + // First octet of 6 octet sequence, see comments for 5 + // octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } else { + // Current octet is neither in the US-ASCII range nor a + // legal first octet of a multi-octet sequence. + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // When mState is non-zero, we expect a continuation of the + // multi-octet sequence + if (0x80 == (0xC0 & ($in))) { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + if (0 == --$mState) { + // End of the multi-octet sequence. mUcs4 now contains + // the final Unicode codepoint to be output + + // Check for illegal sequences and codepoints. + + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || + ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || + (4 < $mBytes) || + // From Unicode 3.2, surrogate characters = illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF) + ) { + + } elseif (0xFEFF != $mUcs4 && // omit BOM + // check for valid Char unicode codepoints + ( + 0x9 == $mUcs4 || + 0xA == $mUcs4 || + 0xD == $mUcs4 || + (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || + // 7F-9F is not strictly prohibited by XML, + // but it is non-SGML, and thus we don't allow it + (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || + (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) + ) + ) { + $out .= $char; + } + // initialize UTF8 cache (reset) + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // ((0xC0 & (*in) != 0x80) && (mState != 0)) + // Incomplete multi-octet sequence. + // used to result in complete fail, but we'll reset + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char =''; + } + } + } + return $out; + } + + /** + * Translates a Unicode codepoint into its corresponding UTF-8 character. + * @note Based on Feyd's function at + * , + * which is in public domain. + * @note While we're going to do code point parsing anyway, a good + * optimization would be to refuse to translate code points that + * are non-SGML characters. However, this could lead to duplication. + * @note This is very similar to the unichr function in + * maintenance/generate-entity-file.php (although this is superior, + * due to its sanity checks). + */ + + // +----------+----------+----------+----------+ + // | 33222222 | 22221111 | 111111 | | + // | 10987654 | 32109876 | 54321098 | 76543210 | bit + // +----------+----------+----------+----------+ + // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F + // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF + // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF + // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF + // +----------+----------+----------+----------+ + // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) + // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes + // +----------+----------+----------+----------+ + + public static function unichr($code) + { + if ($code > 1114111 or $code < 0 or + ($code >= 55296 and $code <= 57343) ) { + // bits are set outside the "valid" range as defined + // by UNICODE 4.1.0 + return ''; + } + + $x = $y = $z = $w = 0; + if ($code < 128) { + // regular ASCII character + $x = $code; + } else { + // set up bits for UTF-8 + $x = ($code & 63) | 128; + if ($code < 2048) { + $y = (($code & 2047) >> 6) | 192; + } else { + $y = (($code & 4032) >> 6) | 128; + if ($code < 65536) { + $z = (($code >> 12) & 15) | 224; + } else { + $z = (($code >> 12) & 63) | 128; + $w = (($code >> 18) & 7) | 240; + } + } + } + // set up the actual character + $ret = ''; + if ($w) { + $ret .= chr($w); + } + if ($z) { + $ret .= chr($z); + } + if ($y) { + $ret .= chr($y); + } + $ret .= chr($x); + + return $ret; + } + + /** + * @return bool + */ + public static function iconvAvailable() + { + static $iconv = null; + if ($iconv === null) { + $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; + } + return $iconv; + } + + /** + * Convert a string to UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public static function convertToUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // unaffected by bugs, since UTF-8 support all characters + $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); + if ($str === false) { + // $encoding is not a valid encoding + trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); + return ''; + } + // If the string is bjorked by Shift_JIS or a similar encoding + // that doesn't support all of ASCII, convert the naughty + // characters to their true byte-wise ASCII/UTF-8 equivalents. + $str = strtr($str, self::testEncodingSupportsASCII($encoding)); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_encode($str); + return $str; + } + $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); + if ($bug == self::ICONV_OK) { + trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); + } else { + trigger_error( + 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . + 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', + E_USER_ERROR + ); + } + } + + /** + * Converts a string from UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + * @note Currently, this is a lossy conversion, with unexpressable + * characters being omitted. + */ + public static function convertFromUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { + $str = self::convertToASCIIDumbLossless($str); + } + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // Undo our previous fix in convertToUTF8, otherwise iconv will barf + $ascii_fix = self::testEncodingSupportsASCII($encoding); + if (!$escape && !empty($ascii_fix)) { + $clear_fix = array(); + foreach ($ascii_fix as $utf8 => $native) { + $clear_fix[$utf8] = ''; + } + $str = strtr($str, $clear_fix); + } + $str = strtr($str, array_flip($ascii_fix)); + // Normal stuff + $str = self::iconv('utf-8', $encoding . '//IGNORE', $str); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_decode($str); + return $str; + } + trigger_error('Encoding not supported', E_USER_ERROR); + // You might be tempted to assume that the ASCII representation + // might be OK, however, this is *not* universally true over all + // encodings. So we take the conservative route here, rather + // than forcibly turn on %Core.EscapeNonASCIICharacters + } + + /** + * Lossless (character-wise) conversion of HTML to ASCII + * @param string $str UTF-8 string to be converted to ASCII + * @return string ASCII encoded string with non-ASCII character entity-ized + * @warning Adapted from MediaWiki, claiming fair use: this is a common + * algorithm. If you disagree with this license fudgery, + * implement it yourself. + * @note Uses decimal numeric entities since they are best supported. + * @note This is a DUMB function: it has no concept of keeping + * character entities that the projected character encoding + * can allow. We could possibly implement a smart version + * but that would require it to also know which Unicode + * codepoints the charset supported (not an easy task). + * @note Sort of with cleanUTF8() but it assumes that $str is + * well-formed UTF-8 + */ + public static function convertToASCIIDumbLossless($str) + { + $bytesleft = 0; + $result = ''; + $working = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $bytevalue = ord($str[$i]); + if ($bytevalue <= 0x7F) { //0xxx xxxx + $result .= chr($bytevalue); + $bytesleft = 0; + } elseif ($bytevalue <= 0xBF) { //10xx xxxx + $working = $working << 6; + $working += ($bytevalue & 0x3F); + $bytesleft--; + if ($bytesleft <= 0) { + $result .= "&#" . $working . ";"; + } + } elseif ($bytevalue <= 0xDF) { //110x xxxx + $working = $bytevalue & 0x1F; + $bytesleft = 1; + } elseif ($bytevalue <= 0xEF) { //1110 xxxx + $working = $bytevalue & 0x0F; + $bytesleft = 2; + } else { //1111 0xxx + $working = $bytevalue & 0x07; + $bytesleft = 3; + } + } + return $result; + } + + /** No bugs detected in iconv. */ + const ICONV_OK = 0; + + /** Iconv truncates output if converting from UTF-8 to another + * character set with //IGNORE, and a non-encodable character is found */ + const ICONV_TRUNCATES = 1; + + /** Iconv does not support //IGNORE, making it unusable for + * transcoding purposes */ + const ICONV_UNUSABLE = 2; + + /** + * glibc iconv has a known bug where it doesn't handle the magic + * //IGNORE stanza correctly. In particular, rather than ignore + * characters, it will return an EILSEQ after consuming some number + * of characters, and expect you to restart iconv as if it were + * an E2BIG. Old versions of PHP did not respect the errno, and + * returned the fragment, so as a result you would see iconv + * mysteriously truncating output. We can work around this by + * manually chopping our input into segments of about 8000 + * characters, as long as PHP ignores the error code. If PHP starts + * paying attention to the error code, iconv becomes unusable. + * + * @return int Error code indicating severity of bug. + */ + public static function testIconvTruncateBug() + { + static $code = null; + if ($code === null) { + // better not use iconv, otherwise infinite loop! + $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); + if ($r === false) { + $code = self::ICONV_UNUSABLE; + } elseif (($c = strlen($r)) < 9000) { + $code = self::ICONV_TRUNCATES; + } elseif ($c > 9000) { + trigger_error( + 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . + 'include your iconv version as per phpversion()', + E_USER_ERROR + ); + } else { + $code = self::ICONV_OK; + } + } + return $code; + } + + /** + * This expensive function tests whether or not a given character + * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will + * fail this test, and require special processing. Variable width + * encodings shouldn't ever fail. + * + * @param string $encoding Encoding name to test, as per iconv format + * @param bool $bypass Whether or not to bypass the precompiled arrays. + * @return Array of UTF-8 characters to their corresponding ASCII, + * which can be used to "undo" any overzealous iconv action. + */ + public static function testEncodingSupportsASCII($encoding, $bypass = false) + { + // All calls to iconv here are unsafe, proof by case analysis: + // If ICONV_OK, no difference. + // If ICONV_TRUNCATE, all calls involve one character inputs, + // so bug is not triggered. + // If ICONV_UNUSABLE, this call is irrelevant + static $encodings = array(); + if (!$bypass) { + if (isset($encodings[$encoding])) { + return $encodings[$encoding]; + } + $lenc = strtolower($encoding); + switch ($lenc) { + case 'shift_jis': + return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); + case 'johab': + return array("\xE2\x82\xA9" => '\\'); + } + if (strpos($lenc, 'iso-8859-') === 0) { + return array(); + } + } + $ret = array(); + if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) { + return false; + } + for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars + $c = chr($i); // UTF-8 char + $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion + if ($r === '' || + // This line is needed for iconv implementations that do not + // omit characters that do not exist in the target character set + ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c) + ) { + // Reverse engineer: what's the UTF-8 equiv of this byte + // sequence? This assumes that there's no variable width + // encoding that doesn't support ASCII. + $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; + } + } + $encodings[$encoding] = $ret; + return $ret; + } +} + + + + + +/** + * Object that provides entity lookup table from entity name to character + */ +class HTMLPurifier_EntityLookup +{ + /** + * Assoc array of entity name to character represented. + * @type array + */ + public $table; + + /** + * Sets up the entity lookup table from the serialized file contents. + * @param bool $file + * @note The serialized contents are versioned, but were generated + * using the maintenance script generate_entity_file.php + * @warning This is not in constructor to help enforce the Singleton + */ + public function setup($file = false) + { + if (!$file) { + $file = HTMLPURIFIER_PREFIX . '/HTMLPurifier/EntityLookup/entities.ser'; + } + $this->table = unserialize(file_get_contents($file)); + } + + /** + * Retrieves sole instance of the object. + * @param bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with. + * @return HTMLPurifier_EntityLookup + */ + public static function instance($prototype = false) + { + // no references, since PHP doesn't copy unless modified + static $instance = null; + if ($prototype) { + $instance = $prototype; + } elseif (!$instance) { + $instance = new HTMLPurifier_EntityLookup(); + $instance->setup(); + } + return $instance; + } +} + + + + + +// if want to implement error collecting here, we'll need to use some sort +// of global data (probably trigger_error) because it's impossible to pass +// $config or $context to the callback functions. + +/** + * Handles referencing and derefencing character entities + */ +class HTMLPurifier_EntityParser +{ + + /** + * Reference to entity lookup table. + * @type HTMLPurifier_EntityLookup + */ + protected $_entity_lookup; + + /** + * Callback regex string for parsing entities. + * @type string + */ + protected $_substituteEntitiesRegex = + '/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/'; + // 1. hex 2. dec 3. string (XML style) + + /** + * Decimal to parsed string conversion table for special entities. + * @type array + */ + protected $_special_dec2str = + array( + 34 => '"', + 38 => '&', + 39 => "'", + 60 => '<', + 62 => '>' + ); + + /** + * Stripped entity names to decimal conversion table for special entities. + * @type array + */ + protected $_special_ent2dec = + array( + 'quot' => 34, + 'amp' => 38, + 'lt' => 60, + 'gt' => 62 + ); + + /** + * Substitutes non-special entities with their parsed equivalents. Since + * running this whenever you have parsed character is t3h 5uck, we run + * it before everything else. + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteNonSpecialEntities($string) + { + // it will try to detect missing semicolons, but don't rely on it + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'nonSpecialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function nonSpecialEntityCallback($matches) + { + // replaces all but big five + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + // abort for special characters + if (isset($this->_special_dec2str[$code])) { + return $entity; + } + return HTMLPurifier_Encoder::unichr($code); + } else { + if (isset($this->_special_ent2dec[$matches[3]])) { + return $entity; + } + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$matches[3]])) { + return $this->_entity_lookup->table[$matches[3]]; + } else { + return $entity; + } + } + } + + /** + * Substitutes only special entities with their parsed equivalents. + * + * @notice We try to avoid calling this function because otherwise, it + * would have to be called a lot (for every parsed section). + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteSpecialEntities($string) + { + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'specialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteSpecialEntities() that does the work. + * + * This callback has same syntax as nonSpecialEntityCallback(). + * + * @param array $matches PCRE-style matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + protected function specialEntityCallback($matches) + { + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + return isset($this->_special_dec2str[$int]) ? + $this->_special_dec2str[$int] : + $entity; + } else { + return isset($this->_special_ent2dec[$matches[3]]) ? + $this->_special_ent2dec[$matches[3]] : + $entity; + } + } +} + + + + + +/** + * Error collection class that enables HTML Purifier to report HTML + * problems back to the user + */ +class HTMLPurifier_ErrorCollector +{ + + /** + * Identifiers for the returned error array. These are purposely numeric + * so list() can be used. + */ + const LINENO = 0; + const SEVERITY = 1; + const MESSAGE = 2; + const CHILDREN = 3; + + /** + * @type array + */ + protected $errors; + + /** + * @type array + */ + protected $_current; + + /** + * @type array + */ + protected $_stacks = array(array()); + + /** + * @type HTMLPurifier_Language + */ + protected $locale; + + /** + * @type HTMLPurifier_Generator + */ + protected $generator; + + /** + * @type HTMLPurifier_Context + */ + protected $context; + + /** + * @type array + */ + protected $lines = array(); + + /** + * @param HTMLPurifier_Context $context + */ + public function __construct($context) + { + $this->locale =& $context->get('Locale'); + $this->context = $context; + $this->_current =& $this->_stacks[0]; + $this->errors =& $this->_stacks[0]; + } + + /** + * Sends an error message to the collector for later use + * @param int $severity Error severity, PHP error style (don't use E_USER_) + * @param string $msg Error message text + */ + public function send($severity, $msg) + { + $args = array(); + if (func_num_args() > 2) { + $args = func_get_args(); + array_shift($args); + unset($args[0]); + } + + $token = $this->context->get('CurrentToken', true); + $line = $token ? $token->line : $this->context->get('CurrentLine', true); + $col = $token ? $token->col : $this->context->get('CurrentCol', true); + $attr = $this->context->get('CurrentAttr', true); + + // perform special substitutions, also add custom parameters + $subst = array(); + if (!is_null($token)) { + $args['CurrentToken'] = $token; + } + if (!is_null($attr)) { + $subst['$CurrentAttr.Name'] = $attr; + if (isset($token->attr[$attr])) { + $subst['$CurrentAttr.Value'] = $token->attr[$attr]; + } + } + + if (empty($args)) { + $msg = $this->locale->getMessage($msg); + } else { + $msg = $this->locale->formatMessage($msg, $args); + } + + if (!empty($subst)) { + $msg = strtr($msg, $subst); + } + + // (numerically indexed) + $error = array( + self::LINENO => $line, + self::SEVERITY => $severity, + self::MESSAGE => $msg, + self::CHILDREN => array() + ); + $this->_current[] = $error; + + // NEW CODE BELOW ... + // Top-level errors are either: + // TOKEN type, if $value is set appropriately, or + // "syntax" type, if $value is null + $new_struct = new HTMLPurifier_ErrorStruct(); + $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; + if ($token) { + $new_struct->value = clone $token; + } + if (is_int($line) && is_int($col)) { + if (isset($this->lines[$line][$col])) { + $struct = $this->lines[$line][$col]; + } else { + $struct = $this->lines[$line][$col] = $new_struct; + } + // These ksorts may present a performance problem + ksort($this->lines[$line], SORT_NUMERIC); + } else { + if (isset($this->lines[-1])) { + $struct = $this->lines[-1]; + } else { + $struct = $this->lines[-1] = $new_struct; + } + } + ksort($this->lines, SORT_NUMERIC); + + // Now, check if we need to operate on a lower structure + if (!empty($attr)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); + if (!$struct->value) { + $struct->value = array($attr, 'PUT VALUE HERE'); + } + } + if (!empty($cssprop)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); + if (!$struct->value) { + // if we tokenize CSS this might be a little more difficult to do + $struct->value = array($cssprop, 'PUT VALUE HERE'); + } + } + + // Ok, structs are all setup, now time to register the error + $struct->addError($severity, $msg); + } + + /** + * Retrieves raw error data for custom formatter to use + */ + public function getRaw() + { + return $this->errors; + } + + /** + * Default HTML formatting implementation for error messages + * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature + * @param array $errors Errors array to display; used for recursion. + * @return string + */ + public function getHTMLFormatted($config, $errors = null) + { + $ret = array(); + + $this->generator = new HTMLPurifier_Generator($config, $this->context); + if ($errors === null) { + $errors = $this->errors; + } + + // 'At line' message needs to be removed + + // generation code for new structure goes here. It needs to be recursive. + foreach ($this->lines as $line => $col_array) { + if ($line == -1) { + continue; + } + foreach ($col_array as $col => $struct) { + $this->_renderStruct($ret, $struct, $line, $col); + } + } + if (isset($this->lines[-1])) { + $this->_renderStruct($ret, $this->lines[-1]); + } + + if (empty($errors)) { + return '

    ' . $this->locale->getMessage('ErrorCollector: No errors') . '

    '; + } else { + return '
    • ' . implode('
    • ', $ret) . '
    '; + } + + } + + private function _renderStruct(&$ret, $struct, $line = null, $col = null) + { + $stack = array($struct); + $context_stack = array(array()); + while ($current = array_pop($stack)) { + $context = array_pop($context_stack); + foreach ($current->errors as $error) { + list($severity, $msg) = $error; + $string = ''; + $string .= '
    '; + // W3C uses an icon to indicate the severity of the error. + $error = $this->locale->getErrorName($severity); + $string .= "$error "; + if (!is_null($line) && !is_null($col)) { + $string .= "Line $line, Column $col: "; + } else { + $string .= 'End of Document: '; + } + $string .= '' . $this->generator->escape($msg) . ' '; + $string .= '
    '; + // Here, have a marker for the character on the column appropriate. + // Be sure to clip extremely long lines. + //$string .= '
    ';
    +                //$string .= '';
    +                //$string .= '
    '; + $ret[] = $string; + } + foreach ($current->children as $array) { + $context[] = $current; + $stack = array_merge($stack, array_reverse($array, true)); + for ($i = count($array); $i > 0; $i--) { + $context_stack[] = $context; + } + } + } + } +} + + + + + +/** + * Records errors for particular segments of an HTML document such as tokens, + * attributes or CSS properties. They can contain error structs (which apply + * to components of what they represent), but their main purpose is to hold + * errors applying to whatever struct is being used. + */ +class HTMLPurifier_ErrorStruct +{ + + /** + * Possible values for $children first-key. Note that top-level structures + * are automatically token-level. + */ + const TOKEN = 0; + const ATTR = 1; + const CSSPROP = 2; + + /** + * Type of this struct. + * @type string + */ + public $type; + + /** + * Value of the struct we are recording errors for. There are various + * values for this: + * - TOKEN: Instance of HTMLPurifier_Token + * - ATTR: array('attr-name', 'value') + * - CSSPROP: array('prop-name', 'value') + * @type mixed + */ + public $value; + + /** + * Errors registered for this structure. + * @type array + */ + public $errors = array(); + + /** + * Child ErrorStructs that are from this structure. For example, a TOKEN + * ErrorStruct would contain ATTR ErrorStructs. This is a multi-dimensional + * array in structure: [TYPE]['identifier'] + * @type array + */ + public $children = array(); + + /** + * @param string $type + * @param string $id + * @return mixed + */ + public function getChild($type, $id) + { + if (!isset($this->children[$type][$id])) { + $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); + $this->children[$type][$id]->type = $type; + } + return $this->children[$type][$id]; + } + + /** + * @param int $severity + * @param string $message + */ + public function addError($severity, $message) + { + $this->errors[] = array($severity, $message); + } +} + + + + + +/** + * Global exception class for HTML Purifier; any exceptions we throw + * are from here. + */ +class HTMLPurifier_Exception extends Exception +{ + +} + + + + + +/** + * Represents a pre or post processing filter on HTML Purifier's output + * + * Sometimes, a little ad-hoc fixing of HTML has to be done before + * it gets sent through HTML Purifier: you can use filters to acheive + * this effect. For instance, YouTube videos can be preserved using + * this manner. You could have used a decorator for this task, but + * PHP's support for them is not terribly robust, so we're going + * to just loop through the filters. + * + * Filters should be exited first in, last out. If there are three filters, + * named 1, 2 and 3, the order of execution should go 1->preFilter, + * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, + * 1->postFilter. + * + * @note Methods are not declared abstract as it is perfectly legitimate + * for an implementation not to want anything to happen on a step + */ + +class HTMLPurifier_Filter +{ + + /** + * Name of the filter for identification purposes. + * @type string + */ + public $name; + + /** + * Pre-processor function, handles HTML before HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function preFilter($html, $config, $context) + { + return $html; + } + + /** + * Post-processor function, handles HTML after HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + return $html; + } +} + + + + + +/** + * Generates HTML from tokens. + * @todo Refactor interface so that configuration/context is determined + * upon instantiation, no need for messy generateFromTokens() calls + * @todo Make some of the more internal functions protected, and have + * unit tests work around that + */ +class HTMLPurifier_Generator +{ + + /** + * Whether or not generator should produce XML output. + * @type bool + */ + private $_xhtml = true; + + /** + * :HACK: Whether or not generator should comment the insides of )#si', + array($this, 'scriptCallback'), + $html + ); + } + + $html = $this->normalize($html, $config, $context); + + $cursor = 0; // our location in the text + $inside_tag = false; // whether or not we're parsing the inside of a tag + $array = array(); // result array + + // This is also treated to mean maintain *column* numbers too + $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); + + if ($maintain_line_numbers === null) { + // automatically determine line numbering by checking + // if error collection is on + $maintain_line_numbers = $config->get('Core.CollectErrors'); + } + + if ($maintain_line_numbers) { + $current_line = 1; + $current_col = 0; + $length = strlen($html); + } else { + $current_line = false; + $current_col = false; + $length = false; + } + $context->register('CurrentLine', $current_line); + $context->register('CurrentCol', $current_col); + $nl = "\n"; + // how often to manually recalculate. This will ALWAYS be right, + // but it's pretty wasteful. Set to 0 to turn off + $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // for testing synchronization + $loops = 0; + + while (++$loops) { + // $cursor is either at the start of a token, or inside of + // a tag (i.e. there was a < immediately before it), as indicated + // by $inside_tag + + if ($maintain_line_numbers) { + // $rcursor, however, is always at the start of a token. + $rcursor = $cursor - (int)$inside_tag; + + // Column number is cheap, so we calculate it every round. + // We're interested at the *end* of the newline string, so + // we need to add strlen($nl) == 1 to $nl_pos before subtracting it + // from our "rcursor" position. + $nl_pos = strrpos($html, $nl, $rcursor - $length); + $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); + + // recalculate lines + if ($synchronize_interval && // synchronization is on + $cursor > 0 && // cursor is further than zero + $loops % $synchronize_interval === 0) { // time to synchronize! + $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); + } + } + + $position_next_lt = strpos($html, '<', $cursor); + $position_next_gt = strpos($html, '>', $cursor); + + // triggers on "asdf" but not "asdf " + // special case to set up context + if ($position_next_lt === $cursor) { + $inside_tag = true; + $cursor++; + } + + if (!$inside_tag && $position_next_lt !== false) { + // We are not inside tag and there still is another tag to parse + $token = new + HTMLPurifier_Token_Text( + $this->parseData( + substr( + $html, + $cursor, + $position_next_lt - $cursor + ) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); + } + $array[] = $token; + $cursor = $position_next_lt + 1; + $inside_tag = true; + continue; + } elseif (!$inside_tag) { + // We are not inside tag but there are no more tags + // If we're already at the end, break + if ($cursor === strlen($html)) { + break; + } + // Create Text of rest of string + $token = new + HTMLPurifier_Token_Text( + $this->parseData( + substr( + $html, + $cursor + ) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + $array[] = $token; + break; + } elseif ($inside_tag && $position_next_gt !== false) { + // We are in tag and it is well formed + // Grab the internals of the tag + $strlen_segment = $position_next_gt - $cursor; + + if ($strlen_segment < 1) { + // there's nothing to process! + $token = new HTMLPurifier_Token_Text('<'); + $cursor++; + continue; + } + + $segment = substr($html, $cursor, $strlen_segment); + + if ($segment === false) { + // somehow, we attempted to access beyond the end of + // the string, defense-in-depth, reported by Nate Abele + break; + } + + // Check if it's a comment + if (substr($segment, 0, 3) === '!--') { + // re-determine segment length, looking for --> + $position_comment_end = strpos($html, '-->', $cursor); + if ($position_comment_end === false) { + // uh oh, we have a comment that extends to + // infinity. Can't be helped: set comment + // end position to end of string + if ($e) { + $e->send(E_WARNING, 'Lexer: Unclosed comment'); + } + $position_comment_end = strlen($html); + $end = true; + } else { + $end = false; + } + $strlen_segment = $position_comment_end - $cursor; + $segment = substr($html, $cursor, $strlen_segment); + $token = new + HTMLPurifier_Token_Comment( + substr( + $segment, + 3, + $strlen_segment - 3 + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); + } + $array[] = $token; + $cursor = $end ? $position_comment_end : $position_comment_end + 3; + $inside_tag = false; + continue; + } + + // Check if it's an end tag + $is_end_tag = (strpos($segment, '/') === 0); + if ($is_end_tag) { + $type = substr($segment, 1); + $token = new HTMLPurifier_Token_End($type); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Check leading character is alnum, if not, we may + // have accidently grabbed an emoticon. Translate into + // text and go our merry way + if (!ctype_alpha($segment[0])) { + // XML: $segment[0] !== '_' && $segment[0] !== ':' + if ($e) { + $e->send(E_NOTICE, 'Lexer: Unescaped lt'); + } + $token = new HTMLPurifier_Token_Text('<'); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + continue; + } + + // Check if it is explicitly self closing, if so, remove + // trailing slash. Remember, we could have a tag like
    , so + // any later token processing scripts must convert improperly + // classified EmptyTags from StartTags. + $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); + if ($is_self_closing) { + $strlen_segment--; + $segment = substr($segment, 0, $strlen_segment); + } + + // Check if there are any attributes + $position_first_space = strcspn($segment, $this->_whitespace); + + if ($position_first_space >= $strlen_segment) { + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($segment); + } else { + $token = new HTMLPurifier_Token_Start($segment); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Grab out all the data + $type = substr($segment, 0, $position_first_space); + $attribute_string = + trim( + substr( + $segment, + $position_first_space + ) + ); + if ($attribute_string) { + $attr = $this->parseAttributeString( + $attribute_string, + $config, + $context + ); + } else { + $attr = array(); + } + + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($type, $attr); + } else { + $token = new HTMLPurifier_Token_Start($type, $attr); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $cursor = $position_next_gt + 1; + $inside_tag = false; + continue; + } else { + // inside tag, but there's no ending > sign + if ($e) { + $e->send(E_WARNING, 'Lexer: Missing gt'); + } + $token = new + HTMLPurifier_Token_Text( + '<' . + $this->parseData( + substr($html, $cursor) + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + // no cursor scroll? Hmm... + $array[] = $token; + break; + } + break; + } + + $context->destroy('CurrentLine'); + $context->destroy('CurrentCol'); + return $array; + } + + /** + * PHP 5.0.x compatible substr_count that implements offset and length + * @param string $haystack + * @param string $needle + * @param int $offset + * @param int $length + * @return int + */ + protected function substrCount($haystack, $needle, $offset, $length) + { + static $oldVersion; + if ($oldVersion === null) { + $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); + } + if ($oldVersion) { + $haystack = substr($haystack, $offset, $length); + return substr_count($haystack, $needle); + } else { + return substr_count($haystack, $needle, $offset, $length); + } + } + + /** + * Takes the inside of an HTML tag and makes an assoc array of attributes. + * + * @param string $string Inside of tag excluding name. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array Assoc array of attributes. + */ + public function parseAttributeString($string, $config, $context) + { + $string = (string)$string; // quick typecast + + if ($string == '') { + return array(); + } // no attributes + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // let's see if we can abort as quickly as possible + // one equal sign, no spaces => one attribute + $num_equal = substr_count($string, '='); + $has_space = strpos($string, ' '); + if ($num_equal === 0 && !$has_space) { + // bool attribute + return array($string => $string); + } elseif ($num_equal === 1 && !$has_space) { + // only one attribute + list($key, $quoted_value) = explode('=', $string); + $quoted_value = trim($quoted_value); + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + return array(); + } + if (!$quoted_value) { + return array($key => ''); + } + $first_char = @$quoted_value[0]; + $last_char = @$quoted_value[strlen($quoted_value) - 1]; + + $same_quote = ($first_char == $last_char); + $open_quote = ($first_char == '"' || $first_char == "'"); + + if ($same_quote && $open_quote) { + // well behaved + $value = substr($quoted_value, 1, strlen($quoted_value) - 2); + } else { + // not well behaved + if ($open_quote) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing end quote'); + } + $value = substr($quoted_value, 1); + } else { + $value = $quoted_value; + } + } + if ($value === false) { + $value = ''; + } + return array($key => $this->parseData($value)); + } + + // setup loop environment + $array = array(); // return assoc array of attributes + $cursor = 0; // current position in string (moves forward) + $size = strlen($string); // size of the string (stays the same) + + // if we have unquoted attributes, the parser expects a terminating + // space, so let's guarantee that there's always a terminating space. + $string .= ' '; + + $old_cursor = -1; + while ($cursor < $size) { + if ($old_cursor >= $cursor) { + throw new Exception("Infinite loop detected"); + } + $old_cursor = $cursor; + + $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); + // grab the key + + $key_begin = $cursor; //we're currently at the start of the key + + // scroll past all characters that are the key (not whitespace or =) + $cursor += strcspn($string, $this->_whitespace . '=', $cursor); + + $key_end = $cursor; // now at the end of the key + + $key = substr($string, $key_begin, $key_end - $key_begin); + + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop + continue; // empty key + } + + // scroll past all whitespace + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor >= $size) { + $array[$key] = $key; + break; + } + + // if the next character is an equal sign, we've got a regular + // pair, otherwise, it's a bool attribute + $first_char = @$string[$cursor]; + + if ($first_char == '=') { + // key="value" + + $cursor++; + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor === false) { + $array[$key] = ''; + break; + } + + // we might be in front of a quote right now + + $char = @$string[$cursor]; + + if ($char == '"' || $char == "'") { + // it's quoted, end bound is $char + $cursor++; + $value_begin = $cursor; + $cursor = strpos($string, $char, $cursor); + $value_end = $cursor; + } else { + // it's not quoted, end bound is whitespace + $value_begin = $cursor; + $cursor += strcspn($string, $this->_whitespace, $cursor); + $value_end = $cursor; + } + + // we reached a premature end + if ($cursor === false) { + $cursor = $size; + $value_end = $cursor; + } + + $value = substr($string, $value_begin, $value_end - $value_begin); + if ($value === false) { + $value = ''; + } + $array[$key] = $this->parseData($value); + $cursor++; + } else { + // boolattr + if ($key !== '') { + $array[$key] = $key; + } else { + // purely theoretical + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + } + } + } + return $array; + } +} + + + + + +/** + * Concrete comment node class. + */ +class HTMLPurifier_Node_Comment extends HTMLPurifier_Node +{ + /** + * Character data within comment. + * @type string + */ + public $data; + + /** + * @type bool + */ + public $is_whitespace = true; + + /** + * Transparent constructor. + * + * @param string $data String comment data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); + } +} + + + +/** + * Concrete element node class. + */ +class HTMLPurifier_Node_Element extends HTMLPurifier_Node +{ + /** + * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. + * + * @note Strictly speaking, XML tags are case sensitive, so we shouldn't + * be lower-casing them, but these tokens cater to HTML tags, which are + * insensitive. + * @type string + */ + public $name; + + /** + * Associative array of the node's attributes. + * @type array + */ + public $attr = array(); + + /** + * List of child elements. + * @type array + */ + public $children = array(); + + /** + * Does this use the form or the form, i.e. + * is it a pair of start/end tokens or an empty token. + * @bool + */ + public $empty = false; + + public $endCol = null, $endLine = null, $endArmor = array(); + + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { + $this->name = $name; + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toTokenPair() { + // XXX inefficiency here, normalization is not necessary + if ($this->empty) { + return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); + } else { + $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); + $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); + //$end->start = $start; + return array($start, $end); + } + } +} + + + + +/** + * Concrete text token class. + * + * Text tokens comprise of regular parsed character data (PCDATA) and raw + * character data (from the CDATA sections). Internally, their + * data is parsed with all entities expanded. Surprisingly, the text token + * does have a "tag name" called #PCDATA, which is how the DTD represents it + * in permissible child nodes. + */ +class HTMLPurifier_Node_Text extends HTMLPurifier_Node +{ + + /** + * PCDATA tag name compatible with DTD, see + * HTMLPurifier_ChildDef_Custom for details. + * @type string + */ + public $name = '#PCDATA'; + + /** + * @type string + */ + public $data; + /**< Parsed character data of text. */ + + /** + * @type bool + */ + public $is_whitespace; + + /**< Bool indicating if node is whitespace. */ + + /** + * Constructor, accepts data and determines if it is whitespace. + * @param string $data String parsed character data. + * @param int $line + * @param int $col + */ + public function __construct($data, $is_whitespace, $line = null, $col = null) + { + $this->data = $data; + $this->is_whitespace = $is_whitespace; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); + } +} + + + + + +/** + * Composite strategy that runs multiple strategies on tokens. + */ +abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy +{ + + /** + * List of strategies to run tokens through. + * @type HTMLPurifier_Strategy[] + */ + protected $strategies = array(); + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + foreach ($this->strategies as $strategy) { + $tokens = $strategy->execute($tokens, $config, $context); + } + return $tokens; + } +} + + + + + +/** + * Core strategy composed of the big four strategies. + */ +class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite +{ + public function __construct() + { + $this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); + $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); + $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); + $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); + } +} + + + + + +/** + * Takes a well formed list of tokens and fixes their nesting. + * + * HTML elements dictate which elements are allowed to be their children, + * for example, you can't have a p tag in a span tag. Other elements have + * much more rigorous definitions: tables, for instance, require a specific + * order for their elements. There are also constraints not expressible by + * document type definitions, such as the chameleon nature of ins/del + * tags and global child exclusions. + * + * The first major objective of this strategy is to iterate through all + * the nodes and determine whether or not their children conform to the + * element's definition. If they do not, the child definition may + * optionally supply an amended list of elements that is valid or + * require that the entire node be deleted (and the previous node + * rescanned). + * + * The second objective is to ensure that explicitly excluded elements of + * an element do not appear in its children. Code that accomplishes this + * task is pervasive through the strategy, though the two are distinct tasks + * and could, theoretically, be seperated (although it's not recommended). + * + * @note Whether or not unrecognized children are silently dropped or + * translated into text depends on the child definitions. + * + * @todo Enable nodes to be bubbled out of the structure. This is + * easier with our new algorithm. + */ + +class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array|HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + + //####################################################################// + // Pre-processing + + // O(n) pass to convert to a tree, so that we can efficiently + // refer to substrings + $top_node = HTMLPurifier_Arborize::arborize($tokens, $config, $context); + + // get a copy of the HTML definition + $definition = $config->getHTMLDefinition(); + + $excludes_enabled = !$config->get('Core.DisableExcludes'); + + // setup the context variable 'IsInline', for chameleon processing + // is 'false' when we are not inline, 'true' when it must always + // be inline, and an integer when it is inline for a certain + // branch of the document tree + $is_inline = $definition->info_parent_def->descendants_are_inline; + $context->register('IsInline', $is_inline); + + // setup error collector + $e =& $context->get('ErrorCollector', true); + + //####################################################################// + // Loop initialization + + // stack that contains all elements that are excluded + // it is organized by parent elements, similar to $stack, + // but it is only populated when an element with exclusions is + // processed, i.e. there won't be empty exclusions. + $exclude_stack = array($definition->info_parent_def->excludes); + + // variable that contains the start token while we are processing + // nodes. This enables error reporting to do its job + $node = $top_node; + // dummy token + list($token, $d) = $node->toTokenPair(); + $context->register('CurrentNode', $node); + $context->register('CurrentToken', $token); + + //####################################################################// + // Loop + + // We need to implement a post-order traversal iteratively, to + // avoid running into stack space limits. This is pretty tricky + // to reason about, so we just manually stack-ify the recursive + // variant: + // + // function f($node) { + // foreach ($node->children as $child) { + // f($child); + // } + // validate($node); + // } + // + // Thus, we will represent a stack frame as array($node, + // $is_inline, stack of children) + // e.g. array_reverse($node->children) - already processed + // children. + + $parent_def = $definition->info_parent_def; + $stack = array( + array($top_node, + $parent_def->descendants_are_inline, + $parent_def->excludes, // exclusions + 0) + ); + + while (!empty($stack)) { + list($node, $is_inline, $excludes, $ix) = array_pop($stack); + // recursive call + $go = false; + $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; + while (isset($node->children[$ix])) { + $child = $node->children[$ix++]; + if ($child instanceof HTMLPurifier_Node_Element) { + $go = true; + $stack[] = array($node, $is_inline, $excludes, $ix); + $stack[] = array($child, + // ToDo: I don't think it matters if it's def or + // child_def, but double check this... + $is_inline || $def->descendants_are_inline, + empty($def->excludes) ? $excludes + : array_merge($excludes, $def->excludes), + 0); + break; + } + }; + if ($go) continue; + list($token, $d) = $node->toTokenPair(); + // base case + if ($excludes_enabled && isset($excludes[$node->name])) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); + } else { + // XXX I suppose it would be slightly more efficient to + // avoid the allocation here and have children + // strategies handle it + $children = array(); + foreach ($node->children as $child) { + if (!$child->dead) $children[] = $child; + } + $result = $def->child->validateChildren($children, $config, $context); + if ($result === true) { + // nop + $node->children = $children; + } elseif ($result === false) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); + } else { + $node->children = $result; + if ($e) { + // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators + if (empty($result) && !empty($children)) { + $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); + } else if ($result != $children) { + $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); + } + } + } + } + } + + //####################################################################// + // Post-processing + + // remove context variables + $context->destroy('IsInline'); + $context->destroy('CurrentNode'); + $context->destroy('CurrentToken'); + + //####################################################################// + // Return + + return HTMLPurifier_Arborize::flatten($node, $config, $context); + } +} + + + + + +/** + * Takes tokens makes them well-formed (balance end tags, etc.) + * + * Specification of the armor attributes this strategy uses: + * + * - MakeWellFormed_TagClosedError: This armor field is used to + * suppress tag closed errors for certain tokens [TagClosedSuppress], + * in particular, if a tag was generated automatically by HTML + * Purifier, we may rely on our infrastructure to close it for us + * and shouldn't report an error to the user [TagClosedAuto]. + */ +class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy +{ + + /** + * Array stream of tokens being processed. + * @type HTMLPurifier_Token[] + */ + protected $tokens; + + /** + * Current token. + * @type HTMLPurifier_Token + */ + protected $token; + + /** + * Zipper managing the true state. + * @type HTMLPurifier_Zipper + */ + protected $zipper; + + /** + * Current nesting of elements. + * @type array + */ + protected $stack; + + /** + * Injectors active in this stream processing. + * @type HTMLPurifier_Injector[] + */ + protected $injectors; + + /** + * Current instance of HTMLPurifier_Config. + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * Current instance of HTMLPurifier_Context. + * @type HTMLPurifier_Context + */ + protected $context; + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + * @throws HTMLPurifier_Exception + */ + public function execute($tokens, $config, $context) + { + $definition = $config->getHTMLDefinition(); + + // local variables + $generator = new HTMLPurifier_Generator($config, $context); + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + // used for autoclose early abortion + $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config); + $e = $context->get('ErrorCollector', true); + $i = false; // injector index + list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens); + if ($token === NULL) { + return array(); + } + $reprocess = false; // whether or not to reprocess the same token + $stack = array(); + + // member variables + $this->stack =& $stack; + $this->tokens =& $tokens; + $this->token =& $token; + $this->zipper =& $zipper; + $this->config = $config; + $this->context = $context; + + // context variables + $context->register('CurrentNesting', $stack); + $context->register('InputZipper', $zipper); + $context->register('CurrentToken', $token); + + // -- begin INJECTOR -- + + $this->injectors = array(); + + $injectors = $config->getBatch('AutoFormat'); + $def_injectors = $definition->info_injector; + $custom_injectors = $injectors['Custom']; + unset($injectors['Custom']); // special case + foreach ($injectors as $injector => $b) { + // XXX: Fix with a legitimate lookup table of enabled filters + if (strpos($injector, '.') !== false) { + continue; + } + $injector = "HTMLPurifier_Injector_$injector"; + if (!$b) { + continue; + } + $this->injectors[] = new $injector; + } + foreach ($def_injectors as $injector) { + // assumed to be objects + $this->injectors[] = $injector; + } + foreach ($custom_injectors as $injector) { + if (!$injector) { + continue; + } + if (is_string($injector)) { + $injector = "HTMLPurifier_Injector_$injector"; + $injector = new $injector; + } + $this->injectors[] = $injector; + } + + // give the injectors references to the definition and context + // variables for performance reasons + foreach ($this->injectors as $ix => $injector) { + $error = $injector->prepare($config, $context); + if (!$error) { + continue; + } + array_splice($this->injectors, $ix, 1); // rm the injector + trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); + } + + // -- end INJECTOR -- + + // a note on reprocessing: + // In order to reduce code duplication, whenever some code needs + // to make HTML changes in order to make things "correct", the + // new HTML gets sent through the purifier, regardless of its + // status. This means that if we add a start token, because it + // was totally necessary, we don't have to update nesting; we just + // punt ($reprocess = true; continue;) and it does that for us. + + // isset is in loop because $tokens size changes during loop exec + for (;; + // only increment if we don't need to reprocess + $reprocess ? $reprocess = false : $token = $zipper->next($token)) { + + // check for a rewind + if (is_int($i)) { + // possibility: disable rewinding if the current token has a + // rewind set on it already. This would offer protection from + // infinite loop, but might hinder some advanced rewinding. + $rewind_offset = $this->injectors[$i]->getRewindOffset(); + if (is_int($rewind_offset)) { + for ($j = 0; $j < $rewind_offset; $j++) { + if (empty($zipper->front)) break; + $token = $zipper->prev($token); + // indicate that other injectors should not process this token, + // but we need to reprocess it + unset($token->skip[$i]); + $token->rewind = $i; + if ($token instanceof HTMLPurifier_Token_Start) { + array_pop($this->stack); + } elseif ($token instanceof HTMLPurifier_Token_End) { + $this->stack[] = $token->start; + } + } + } + $i = false; + } + + // handle case of document end + if ($token === NULL) { + // kill processing if stack is empty + if (empty($this->stack)) { + break; + } + + // peek + $top_nesting = array_pop($this->stack); + $this->stack[] = $top_nesting; + + // send error [TagClosedSuppress] + if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); + } + + // append, don't splice, since this is the end + $token = new HTMLPurifier_Token_End($top_nesting->name); + + // punt! + $reprocess = true; + continue; + } + + //echo '
    '; printZipper($zipper, $token);//printTokens($this->stack); + //flush(); + + // quick-check: if it's not a tag, no need to process + if (empty($token->is_tag)) { + if ($token instanceof HTMLPurifier_Token_Text) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + // XXX fuckup + $r = $token; + $injector->handleText($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + } + // another possibility is a comment + continue; + } + + if (isset($definition->info[$token->name])) { + $type = $definition->info[$token->name]->child->type; + } else { + $type = false; // Type is unknown, treat accordingly + } + + // quick tag checks: anything that's *not* an end tag + $ok = false; + if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { + // claims to be a start tag but is empty + $token = new HTMLPurifier_Token_Empty( + $token->name, + $token->attr, + $token->line, + $token->col, + $token->armor + ); + $ok = true; + } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { + // claims to be empty but really is a start tag + // NB: this assignment is required + $old_token = $token; + $token = new HTMLPurifier_Token_End($token->name); + $token = $this->insertBefore( + new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor) + ); + // punt (since we had to modify the input stream in a non-trivial way) + $reprocess = true; + continue; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // real empty token + $ok = true; + } elseif ($token instanceof HTMLPurifier_Token_Start) { + // start tag + + // ...unless they also have to close their parent + if (!empty($this->stack)) { + + // Performance note: you might think that it's rather + // inefficient, recalculating the autoclose information + // for every tag that a token closes (since when we + // do an autoclose, we push a new token into the + // stream and then /process/ that, before + // re-processing this token.) But this is + // necessary, because an injector can make an + // arbitrary transformations to the autoclosing + // tokens we introduce, so things may have changed + // in the meantime. Also, doing the inefficient thing is + // "easy" to reason about (for certain perverse definitions + // of "easy") + + $parent = array_pop($this->stack); + $this->stack[] = $parent; + + $parent_def = null; + $parent_elements = null; + $autoclose = false; + if (isset($definition->info[$parent->name])) { + $parent_def = $definition->info[$parent->name]; + $parent_elements = $parent_def->child->getAllowedElements($config); + $autoclose = !isset($parent_elements[$token->name]); + } + + if ($autoclose && $definition->info[$token->name]->wrap) { + // Check if an element can be wrapped by another + // element to make it valid in a context (for + // example,
        needs a
      • in between) + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $elements = $wrapdef->child->getAllowedElements($config); + if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) { + $newtoken = new HTMLPurifier_Token_Start($wrapname); + $token = $this->insertBefore($newtoken); + $reprocess = true; + continue; + } + } + + $carryover = false; + if ($autoclose && $parent_def->formatting) { + $carryover = true; + } + + if ($autoclose) { + // check if this autoclose is doomed to fail + // (this rechecks $parent, which his harmless) + $autoclose_ok = isset($global_parent_allowed_elements[$token->name]); + if (!$autoclose_ok) { + foreach ($this->stack as $ancestor) { + $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config); + if (isset($elements[$token->name])) { + $autoclose_ok = true; + break; + } + if ($definition->info[$token->name]->wrap) { + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $wrap_elements = $wrapdef->child->getAllowedElements($config); + if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) { + $autoclose_ok = true; + break; + } + } + } + } + if ($autoclose_ok) { + // errors need to be updated + $new_token = new HTMLPurifier_Token_End($parent->name); + $new_token->start = $parent; + // [TagClosedSuppress] + if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) { + if (!$carryover) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent); + } else { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent); + } + } + if ($carryover) { + $element = clone $parent; + // [TagClosedAuto] + $element->armor['MakeWellFormed_TagClosedError'] = true; + $element->carryover = true; + $token = $this->processToken(array($new_token, $token, $element)); + } else { + $token = $this->insertBefore($new_token); + } + } else { + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + } + $ok = true; + } + + if ($ok) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + $r = $token; + $injector->handleElement($r); + $token = $this->processToken($r, $i); + $reprocess = true; + break; + } + if (!$reprocess) { + // ah, nothing interesting happened; do normal processing + if ($token instanceof HTMLPurifier_Token_Start) { + $this->stack[] = $token; + } elseif ($token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception( + 'Improper handling of end tag in start code; possible error in MakeWellFormed' + ); + } + } + continue; + } + + // sanity check: we should be dealing with a closing tag + if (!$token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier'); + } + + // make sure that we have something open + if (empty($this->stack)) { + if ($escape_invalid_tags) { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text'); + } + $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); + } else { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); + } + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + // first, check for the simplest case: everything closes neatly. + // Eventually, everything passes through here; if there are problems + // we modify the input stream accordingly and then punt, so that + // the tokens get processed again. + $current_parent = array_pop($this->stack); + if ($current_parent->name == $token->name) { + $token->start = $current_parent; + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) { + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + $r = $token; + $injector->handleEnd($r); + $token = $this->processToken($r, $i); + $this->stack[] = $current_parent; + $reprocess = true; + break; + } + continue; + } + + // okay, so we're trying to close the wrong tag + + // undo the pop previous pop + $this->stack[] = $current_parent; + + // scroll back the entire nest, trying to find our tag. + // (feature could be to specify how far you'd like to go) + $size = count($this->stack); + // -2 because -1 is the last element, but we already checked that + $skipped_tags = false; + for ($j = $size - 2; $j >= 0; $j--) { + if ($this->stack[$j]->name == $token->name) { + $skipped_tags = array_slice($this->stack, $j); + break; + } + } + + // we didn't find the tag, so remove + if ($skipped_tags === false) { + if ($escape_invalid_tags) { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text'); + } + $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token)); + } else { + if ($e) { + $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed'); + } + $token = $this->remove(); + } + $reprocess = true; + continue; + } + + // do errors, in REVERSE $j order: a,b,c with + $c = count($skipped_tags); + if ($e) { + for ($j = $c - 1; $j > 0; $j--) { + // notice we exclude $j == 0, i.e. the current ending tag, from + // the errors... [TagClosedSuppress] + if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]); + } + } + } + + // insert tags, in FORWARD $j order: c,b,a with + $replace = array($token); + for ($j = 1; $j < $c; $j++) { + // ...as well as from the insertions + $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name); + $new_token->start = $skipped_tags[$j]; + array_unshift($replace, $new_token); + if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) { + // [TagClosedAuto] + $element = clone $skipped_tags[$j]; + $element->carryover = true; + $element->armor['MakeWellFormed_TagClosedError'] = true; + $replace[] = $element; + } + } + $token = $this->processToken($replace); + $reprocess = true; + continue; + } + + $context->destroy('CurrentToken'); + $context->destroy('CurrentNesting'); + $context->destroy('InputZipper'); + + unset($this->injectors, $this->stack, $this->tokens); + return $zipper->toArray($token); + } + + /** + * Processes arbitrary token values for complicated substitution patterns. + * In general: + * + * If $token is an array, it is a list of tokens to substitute for the + * current token. These tokens then get individually processed. If there + * is a leading integer in the list, that integer determines how many + * tokens from the stream should be removed. + * + * If $token is a regular token, it is swapped with the current token. + * + * If $token is false, the current token is deleted. + * + * If $token is an integer, that number of tokens (with the first token + * being the current one) will be deleted. + * + * @param HTMLPurifier_Token|array|int|bool $token Token substitution value + * @param HTMLPurifier_Injector|int $injector Injector that performed the substitution; default is if + * this is not an injector related operation. + * @throws HTMLPurifier_Exception + */ + protected function processToken($token, $injector = -1) + { + // normalize forms of token + if (is_object($token)) { + $token = array(1, $token); + } + if (is_int($token)) { + $token = array($token); + } + if ($token === false) { + $token = array(1); + } + if (!is_array($token)) { + throw new HTMLPurifier_Exception('Invalid token type from injector'); + } + if (!is_int($token[0])) { + array_unshift($token, 1); + } + if ($token[0] === 0) { + throw new HTMLPurifier_Exception('Deleting zero tokens is not valid'); + } + + // $token is now an array with the following form: + // array(number nodes to delete, new node 1, new node 2, ...) + + $delete = array_shift($token); + list($old, $r) = $this->zipper->splice($this->token, $delete, $token); + + if ($injector > -1) { + // determine appropriate skips + $oldskip = isset($old[0]) ? $old[0]->skip : array(); + foreach ($token as $object) { + $object->skip = $oldskip; + $object->skip[$injector] = true; + } + } + + return $r; + + } + + /** + * Inserts a token before the current token. Cursor now points to + * this token. You must reprocess after this. + * @param HTMLPurifier_Token $token + */ + private function insertBefore($token) + { + // NB not $this->zipper->insertBefore(), due to positioning + // differences + $splice = $this->zipper->splice($this->token, 0, array($token)); + + return $splice[1]; + } + + /** + * Removes current token. Cursor now points to new token occupying previously + * occupied space. You must reprocess after this. + */ + private function remove() + { + return $this->zipper->delete(); + } +} + + + + + +/** + * Removes all unrecognized tags from the list of tokens. + * + * This strategy iterates through all the tokens and removes unrecognized + * tokens. If a token is not recognized but a TagTransform is defined for + * that element, the element will be transformed accordingly. + */ + +class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array|HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + $definition = $config->getHTMLDefinition(); + $generator = new HTMLPurifier_Generator($config, $context); + $result = array(); + + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + $remove_invalid_img = $config->get('Core.RemoveInvalidImg'); + + // currently only used to determine if comments should be kept + $trusted = $config->get('HTML.Trusted'); + $comment_lookup = $config->get('HTML.AllowedComments'); + $comment_regexp = $config->get('HTML.AllowedCommentsRegexp'); + $check_comments = $comment_lookup !== array() || $comment_regexp !== null; + + $remove_script_contents = $config->get('Core.RemoveScriptContents'); + $hidden_elements = $config->get('Core.HiddenElements'); + + // remove script contents compatibility + if ($remove_script_contents === true) { + $hidden_elements['script'] = true; + } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) { + unset($hidden_elements['script']); + } + + $attr_validator = new HTMLPurifier_AttrValidator(); + + // removes tokens until it reaches a closing tag with its value + $remove_until = false; + + // converts comments into text tokens when this is equal to a tag name + $textify_comments = false; + + $token = false; + $context->register('CurrentToken', $token); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + foreach ($tokens as $token) { + if ($remove_until) { + if (empty($token->is_tag) || $token->name !== $remove_until) { + continue; + } + } + if (!empty($token->is_tag)) { + // DEFINITION CALL + + // before any processing, try to transform the element + if (isset($definition->info_tag_transform[$token->name])) { + $original_name = $token->name; + // there is a transformation for this tag + // DEFINITION CALL + $token = $definition-> + info_tag_transform[$token->name]->transform($token, $config, $context); + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name); + } + } + + if (isset($definition->info[$token->name])) { + // mostly everything's good, but + // we need to make sure required attributes are in order + if (($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) && + $definition->info[$token->name]->required_attr && + ($token->name != 'img' || $remove_invalid_img) // ensure config option still works + ) { + $attr_validator->validateToken($token, $config, $context); + $ok = true; + foreach ($definition->info[$token->name]->required_attr as $name) { + if (!isset($token->attr[$name])) { + $ok = false; + break; + } + } + if (!$ok) { + if ($e) { + $e->send( + E_ERROR, + 'Strategy_RemoveForeignElements: Missing required attribute', + $name + ); + } + continue; + } + $token->armor['ValidateAttributes'] = true; + } + + if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) { + $textify_comments = $token->name; + } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) { + $textify_comments = false; + } + + } elseif ($escape_invalid_tags) { + // invalid tag, generate HTML representation and insert in + if ($e) { + $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text'); + } + $token = new HTMLPurifier_Token_Text( + $generator->generateFromToken($token) + ); + } else { + // check if we need to destroy all of the tag's children + // CAN BE GENERICIZED + if (isset($hidden_elements[$token->name])) { + if ($token instanceof HTMLPurifier_Token_Start) { + $remove_until = $token->name; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // do nothing: we're still looking + } else { + $remove_until = false; + } + if ($e) { + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed'); + } + } else { + if ($e) { + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed'); + } + } + continue; + } + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + // textify comments in script tags when they are allowed + if ($textify_comments !== false) { + $data = $token->data; + $token = new HTMLPurifier_Token_Text($data); + } elseif ($trusted || $check_comments) { + // always cleanup comments + $trailing_hyphen = false; + if ($e) { + // perform check whether or not there's a trailing hyphen + if (substr($token->data, -1) == '-') { + $trailing_hyphen = true; + } + } + $token->data = rtrim($token->data, '-'); + $found_double_hyphen = false; + while (strpos($token->data, '--') !== false) { + $found_double_hyphen = true; + $token->data = str_replace('--', '-', $token->data); + } + if ($trusted || !empty($comment_lookup[trim($token->data)]) || + ($comment_regexp !== null && preg_match($comment_regexp, trim($token->data)))) { + // OK good + if ($e) { + if ($trailing_hyphen) { + $e->send( + E_NOTICE, + 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' + ); + } + if ($found_double_hyphen) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed'); + } + } + } else { + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); + } + continue; + } + } else { + // strip comments + if ($e) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); + } + continue; + } + } elseif ($token instanceof HTMLPurifier_Token_Text) { + } else { + continue; + } + $result[] = $token; + } + if ($remove_until && $e) { + // we removed tokens until the end, throw error + $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until); + } + $context->destroy('CurrentToken'); + return $result; + } +} + + + + + +/** + * Validate all attributes in the tokens. + */ + +class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy +{ + + /** + * @param HTMLPurifier_Token[] $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + */ + public function execute($tokens, $config, $context) + { + // setup validator + $validator = new HTMLPurifier_AttrValidator(); + + $token = false; + $context->register('CurrentToken', $token); + + foreach ($tokens as $key => $token) { + + // only process tokens that have attributes, + // namely start and empty tags + if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) { + continue; + } + + // skip tokens that are armored + if (!empty($token->armor['ValidateAttributes'])) { + continue; + } + + // note that we have no facilities here for removing tokens + $validator->validateToken($token, $config, $context); + } + $context->destroy('CurrentToken'); + return $tokens; + } +} + + + + + +/** + * Transforms FONT tags to the proper form (SPAN with CSS styling) + * + * This transformation takes the three proprietary attributes of FONT and + * transforms them into their corresponding CSS attributes. These are color, + * face, and size. + * + * @note Size is an interesting case because it doesn't map cleanly to CSS. + * Thanks to + * http://style.cleverchimp.com/font_size_intervals/altintervals.html + * for reasonable mappings. + * @warning This doesn't work completely correctly; specifically, this + * TagTransform operates before well-formedness is enforced, so + * the "active formatting elements" algorithm doesn't get applied. + */ +class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform +{ + /** + * @type string + */ + public $transform_to = 'span'; + + /** + * @type array + */ + protected $_size_lookup = array( + '0' => 'xx-small', + '1' => 'xx-small', + '2' => 'small', + '3' => 'medium', + '4' => 'large', + '5' => 'x-large', + '6' => 'xx-large', + '7' => '300%', + '-1' => 'smaller', + '-2' => '60%', + '+1' => 'larger', + '+2' => '150%', + '+3' => '200%', + '+4' => '300%' + ); + + /** + * @param HTMLPurifier_Token_Tag $tag + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token_End|string + */ + public function transform($tag, $config, $context) + { + if ($tag instanceof HTMLPurifier_Token_End) { + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + return $new_tag; + } + + $attr = $tag->attr; + $prepend_style = ''; + + // handle color transform + if (isset($attr['color'])) { + $prepend_style .= 'color:' . $attr['color'] . ';'; + unset($attr['color']); + } + + // handle face transform + if (isset($attr['face'])) { + $prepend_style .= 'font-family:' . $attr['face'] . ';'; + unset($attr['face']); + } + + // handle size transform + if (isset($attr['size'])) { + // normalize large numbers + if ($attr['size'] !== '') { + if ($attr['size']{0} == '+' || $attr['size']{0} == '-') { + $size = (int)$attr['size']; + if ($size < -2) { + $attr['size'] = '-2'; + } + if ($size > 4) { + $attr['size'] = '+4'; + } + } else { + $size = (int)$attr['size']; + if ($size > 7) { + $attr['size'] = '7'; + } + } + } + if (isset($this->_size_lookup[$attr['size']])) { + $prepend_style .= 'font-size:' . + $this->_size_lookup[$attr['size']] . ';'; + } + unset($attr['size']); + } + + if ($prepend_style) { + $attr['style'] = isset($attr['style']) ? + $prepend_style . $attr['style'] : + $prepend_style; + } + + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + $new_tag->attr = $attr; + + return $new_tag; + } +} + + + + + +/** + * Simple transformation, just change tag name to something else, + * and possibly add some styling. This will cover most of the deprecated + * tag cases. + */ +class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform +{ + /** + * @type string + */ + protected $style; + + /** + * @param string $transform_to Tag name to transform to. + * @param string $style CSS style to add to the tag + */ + public function __construct($transform_to, $style = null) + { + $this->transform_to = $transform_to; + $this->style = $style; + } + + /** + * @param HTMLPurifier_Token_Tag $tag + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function transform($tag, $config, $context) + { + $new_tag = clone $tag; + $new_tag->name = $this->transform_to; + if (!is_null($this->style) && + ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty) + ) { + $this->prependCSS($new_tag->attr, $this->style); + } + return $new_tag; + } +} + + + + + +/** + * Concrete comment token class. Generally will be ignored. + */ +class HTMLPurifier_Token_Comment extends HTMLPurifier_Token +{ + /** + * Character data within comment. + * @type string + */ + public $data; + + /** + * @type bool + */ + public $is_whitespace = true; + + /** + * Transparent constructor. + * + * @param string $data String comment data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toNode() { + return new HTMLPurifier_Node_Comment($this->data, $this->line, $this->col); + } +} + + + + + +/** + * Abstract class of a tag token (start, end or empty), and its behavior. + */ +abstract class HTMLPurifier_Token_Tag extends HTMLPurifier_Token +{ + /** + * Static bool marker that indicates the class is a tag. + * + * This allows us to check objects with !empty($obj->is_tag) + * without having to use a function call is_a(). + * @type bool + */ + public $is_tag = true; + + /** + * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. + * + * @note Strictly speaking, XML tags are case sensitive, so we shouldn't + * be lower-casing them, but these tokens cater to HTML tags, which are + * insensitive. + * @type string + */ + public $name; + + /** + * Associative array of the tag's attributes. + * @type array + */ + public $attr = array(); + + /** + * Non-overloaded constructor, which lower-cases passed tag name. + * + * @param string $name String name. + * @param array $attr Associative array of attributes. + * @param int $line + * @param int $col + * @param array $armor + */ + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) + { + $this->name = ctype_lower($name) ? $name : strtolower($name); + foreach ($attr as $key => $value) { + // normalization only necessary when key is not lowercase + if (!ctype_lower($key)) { + $new_key = strtolower($key); + if (!isset($attr[$new_key])) { + $attr[$new_key] = $attr[$key]; + } + if ($new_key !== $key) { + unset($attr[$key]); + } + } + } + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toNode() { + return new HTMLPurifier_Node_Element($this->name, $this->attr, $this->line, $this->col, $this->armor); + } +} + + + + + +/** + * Concrete empty token class. + */ +class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag +{ + public function toNode() { + $n = parent::toNode(); + $n->empty = true; + return $n; + } +} + + + + + +/** + * Concrete end token class. + * + * @warning This class accepts attributes even though end tags cannot. This + * is for optimization reasons, as under normal circumstances, the Lexers + * do not pass attributes. + */ +class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag +{ + /** + * Token that started this node. + * Added by MakeWellFormed. Please do not edit this! + * @type HTMLPurifier_Token + */ + public $start; + + public function toNode() { + throw new Exception("HTMLPurifier_Token_End->toNode not supported!"); + } +} + + + + + +/** + * Concrete start token class. + */ +class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag +{ +} + + + + + +/** + * Concrete text token class. + * + * Text tokens comprise of regular parsed character data (PCDATA) and raw + * character data (from the CDATA sections). Internally, their + * data is parsed with all entities expanded. Surprisingly, the text token + * does have a "tag name" called #PCDATA, which is how the DTD represents it + * in permissible child nodes. + */ +class HTMLPurifier_Token_Text extends HTMLPurifier_Token +{ + + /** + * @type string + */ + public $name = '#PCDATA'; + /**< PCDATA tag name compatible with DTD. */ + + /** + * @type string + */ + public $data; + /**< Parsed character data of text. */ + + /** + * @type bool + */ + public $is_whitespace; + + /**< Bool indicating if node is whitespace. */ + + /** + * Constructor, accepts data and determines if it is whitespace. + * @param string $data String parsed character data. + * @param int $line + * @param int $col + */ + public function __construct($data, $line = null, $col = null) + { + $this->data = $data; + $this->is_whitespace = ctype_space($data); + $this->line = $line; + $this->col = $col; + } + + public function toNode() { + return new HTMLPurifier_Node_Text($this->data, $this->is_whitespace, $this->line, $this->col); + } +} + + + + + +class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'DisableExternal'; + + /** + * @type array + */ + protected $ourHostParts = false; + + /** + * @param HTMLPurifier_Config $config + * @return void + */ + public function prepare($config) + { + $our_host = $config->getDefinition('URI')->host; + if ($our_host !== null) { + $this->ourHostParts = array_reverse(explode('.', $our_host)); + } + } + + /** + * @param HTMLPurifier_URI $uri Reference + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (is_null($uri->host)) { + return true; + } + if ($this->ourHostParts === false) { + return false; + } + $host_parts = array_reverse(explode('.', $uri->host)); + foreach ($this->ourHostParts as $i => $x) { + if (!isset($host_parts[$i])) { + return false; + } + if ($host_parts[$i] != $this->ourHostParts[$i]) { + return false; + } + } + return true; + } +} + + + + + +class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal +{ + /** + * @type string + */ + public $name = 'DisableExternalResources'; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (!$context->get('EmbeddedURI', true)) { + return true; + } + return parent::filter($uri, $config, $context); + } +} + + + + + +class HTMLPurifier_URIFilter_DisableResources extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'DisableResources'; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + return !$context->get('EmbeddedURI', true); + } +} + + + + + +// It's not clear to me whether or not Punycode means that hostnames +// do not have canonical forms anymore. As far as I can tell, it's +// not a problem (punycoding should be identity when no Unicode +// points are involved), but I'm not 100% sure +class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'HostBlacklist'; + + /** + * @type array + */ + protected $blacklist = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->blacklist = $config->get('URI.HostBlacklist'); + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + foreach ($this->blacklist as $blacklisted_host_fragment) { + if (strpos($uri->host, $blacklisted_host_fragment) !== false) { + return false; + } + } + return true; + } +} + + + + + +// does not support network paths + +class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'MakeAbsolute'; + + /** + * @type + */ + protected $base; + + /** + * @type array + */ + protected $basePathStack = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $def = $config->getDefinition('URI'); + $this->base = $def->base; + if (is_null($this->base)) { + trigger_error( + 'URI.MakeAbsolute is being ignored due to lack of ' . + 'value for URI.Base configuration', + E_USER_WARNING + ); + return false; + } + $this->base->fragment = null; // fragment is invalid for base URI + $stack = explode('/', $this->base->path); + array_pop($stack); // discard last segment + $stack = $this->_collapseStack($stack); // do pre-parsing + $this->basePathStack = $stack; + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if (is_null($this->base)) { + return true; + } // abort early + if ($uri->path === '' && is_null($uri->scheme) && + is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)) { + // reference to current document + $uri = clone $this->base; + return true; + } + if (!is_null($uri->scheme)) { + // absolute URI already: don't change + if (!is_null($uri->host)) { + return true; + } + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + // scheme not recognized + return false; + } + if (!$scheme_obj->hierarchical) { + // non-hierarchal URI with explicit scheme, don't change + return true; + } + // special case: had a scheme but always is hierarchical and had no authority + } + if (!is_null($uri->host)) { + // network path, don't bother + return true; + } + if ($uri->path === '') { + $uri->path = $this->base->path; + } elseif ($uri->path[0] !== '/') { + // relative path, needs more complicated processing + $stack = explode('/', $uri->path); + $new_stack = array_merge($this->basePathStack, $stack); + if ($new_stack[0] !== '' && !is_null($this->base->host)) { + array_unshift($new_stack, ''); + } + $new_stack = $this->_collapseStack($new_stack); + $uri->path = implode('/', $new_stack); + } else { + // absolute path, but still we should collapse + $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path))); + } + // re-combine + $uri->scheme = $this->base->scheme; + if (is_null($uri->userinfo)) { + $uri->userinfo = $this->base->userinfo; + } + if (is_null($uri->host)) { + $uri->host = $this->base->host; + } + if (is_null($uri->port)) { + $uri->port = $this->base->port; + } + return true; + } + + /** + * Resolve dots and double-dots in a path stack + * @param array $stack + * @return array + */ + private function _collapseStack($stack) + { + $result = array(); + $is_folder = false; + for ($i = 0; isset($stack[$i]); $i++) { + $is_folder = false; + // absorb an internally duplicated slash + if ($stack[$i] == '' && $i && isset($stack[$i + 1])) { + continue; + } + if ($stack[$i] == '..') { + if (!empty($result)) { + $segment = array_pop($result); + if ($segment === '' && empty($result)) { + // error case: attempted to back out too far: + // restore the leading slash + $result[] = ''; + } elseif ($segment === '..') { + $result[] = '..'; // cannot remove .. with .. + } + } else { + // relative path, preserve the double-dots + $result[] = '..'; + } + $is_folder = true; + continue; + } + if ($stack[$i] == '.') { + // silently absorb + $is_folder = true; + continue; + } + $result[] = $stack[$i]; + } + if ($is_folder) { + $result[] = ''; + } + return $result; + } +} + + + + + +class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'Munge'; + + /** + * @type bool + */ + public $post = true; + + /** + * @type string + */ + private $target; + + /** + * @type HTMLPurifier_URIParser + */ + private $parser; + + /** + * @type bool + */ + private $doEmbed; + + /** + * @type string + */ + private $secretKey; + + /** + * @type array + */ + protected $replace = array(); + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->target = $config->get('URI.' . $this->name); + $this->parser = new HTMLPurifier_URIParser(); + $this->doEmbed = $config->get('URI.MungeResources'); + $this->secretKey = $config->get('URI.MungeSecretKey'); + if ($this->secretKey && !function_exists('hash_hmac')) { + throw new Exception("Cannot use %URI.MungeSecretKey without hash_hmac support."); + } + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + if ($context->get('EmbeddedURI', true) && !$this->doEmbed) { + return true; + } + + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + return true; + } // ignore unknown schemes, maybe another postfilter did it + if (!$scheme_obj->browsable) { + return true; + } // ignore non-browseable schemes, since we can't munge those in a reasonable way + if ($uri->isBenign($config, $context)) { + return true; + } // don't redirect if a benign URL + + $this->makeReplace($uri, $config, $context); + $this->replace = array_map('rawurlencode', $this->replace); + + $new_uri = strtr($this->target, $this->replace); + $new_uri = $this->parser->parse($new_uri); + // don't redirect if the target host is the same as the + // starting host + if ($uri->host === $new_uri->host) { + return true; + } + $uri = $new_uri; // overwrite + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + protected function makeReplace($uri, $config, $context) + { + $string = $uri->toString(); + // always available + $this->replace['%s'] = $string; + $this->replace['%r'] = $context->get('EmbeddedURI', true); + $token = $context->get('CurrentToken', true); + $this->replace['%n'] = $token ? $token->name : null; + $this->replace['%m'] = $context->get('CurrentAttr', true); + $this->replace['%p'] = $context->get('CurrentCSSProperty', true); + // not always available + if ($this->secretKey) { + $this->replace['%t'] = hash_hmac("sha256", $string, $this->secretKey); + } + } +} + + + + + +/** + * Implements safety checks for safe iframes. + * + * @warning This filter is *critical* for ensuring that %HTML.SafeIframe + * works safely. + */ +class HTMLPurifier_URIFilter_SafeIframe extends HTMLPurifier_URIFilter +{ + /** + * @type string + */ + public $name = 'SafeIframe'; + + /** + * @type bool + */ + public $always_load = true; + + /** + * @type string + */ + protected $regexp = null; + + // XXX: The not so good bit about how this is all set up now is we + // can't check HTML.SafeIframe in the 'prepare' step: we have to + // defer till the actual filtering. + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function prepare($config) + { + $this->regexp = $config->get('URI.SafeIframeRegexp'); + return true; + } + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function filter(&$uri, $config, $context) + { + // check if filter not applicable + if (!$config->get('HTML.SafeIframe')) { + return true; + } + // check if the filter should actually trigger + if (!$context->get('EmbeddedURI', true)) { + return true; + } + $token = $context->get('CurrentToken', true); + if (!($token && $token->name == 'iframe')) { + return true; + } + // check if we actually have some whitelists enabled + if ($this->regexp === null) { + return false; + } + // actually check the whitelists + return preg_match($this->regexp, $uri->toString()); + } +} + + + + + +/** + * Implements data: URI for base64 encoded images supported by GD. + */ +class HTMLPurifier_URIScheme_data extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = true; + + /** + * @type array + */ + public $allowed_types = array( + // you better write validation code for other types if you + // decide to allow them + 'image/jpeg' => true, + 'image/gif' => true, + 'image/png' => true, + ); + // this is actually irrelevant since we only write out the path + // component + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $result = explode(',', $uri->path, 2); + $is_base64 = false; + $charset = null; + $content_type = null; + if (count($result) == 2) { + list($metadata, $data) = $result; + // do some legwork on the metadata + $metas = explode(';', $metadata); + while (!empty($metas)) { + $cur = array_shift($metas); + if ($cur == 'base64') { + $is_base64 = true; + break; + } + if (substr($cur, 0, 8) == 'charset=') { + // doesn't match if there are arbitrary spaces, but + // whatever dude + if ($charset !== null) { + continue; + } // garbage + $charset = substr($cur, 8); // not used + } else { + if ($content_type !== null) { + continue; + } // garbage + $content_type = $cur; + } + } + } else { + $data = $result[0]; + } + if ($content_type !== null && empty($this->allowed_types[$content_type])) { + return false; + } + if ($charset !== null) { + // error; we don't allow plaintext stuff + $charset = null; + } + $data = rawurldecode($data); + if ($is_base64) { + $raw_data = base64_decode($data); + } else { + $raw_data = $data; + } + // XXX probably want to refactor this into a general mechanism + // for filtering arbitrary content types + $file = tempnam("/tmp", ""); + file_put_contents($file, $raw_data); + if (function_exists('exif_imagetype')) { + $image_code = exif_imagetype($file); + unlink($file); + } elseif (function_exists('getimagesize')) { + set_error_handler(array($this, 'muteErrorHandler')); + $info = getimagesize($file); + restore_error_handler(); + unlink($file); + if ($info == false) { + return false; + } + $image_code = $info[2]; + } else { + trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR); + } + $real_content_type = image_type_to_mime_type($image_code); + if ($real_content_type != $content_type) { + // we're nice guys; if the content type is something else we + // support, change it over + if (empty($this->allowed_types[$real_content_type])) { + return false; + } + $content_type = $real_content_type; + } + // ok, it's kosher, rewrite what we need + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->fragment = null; + $uri->query = null; + $uri->path = "$content_type;base64," . base64_encode($raw_data); + return true; + } + + /** + * @param int $errno + * @param string $errstr + */ + public function muteErrorHandler($errno, $errstr) + { + } +} + + + +/** + * Validates file as defined by RFC 1630 and RFC 1738. + */ +class HTMLPurifier_URIScheme_file extends HTMLPurifier_URIScheme +{ + /** + * Generally file:// URLs are not accessible from most + * machines, so placing them as an img src is incorrect. + * @type bool + */ + public $browsable = false; + + /** + * Basically the *only* URI scheme for which this is true, since + * accessing files on the local machine is very common. In fact, + * browsers on some operating systems don't understand the + * authority, though I hear it is used on Windows to refer to + * network shares. + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + // Authentication method is not supported + $uri->userinfo = null; + // file:// makes no provisions for accessing the resource + $uri->port = null; + // While it seems to work on Firefox, the querystring has + // no possible effect and is thus stripped. + $uri->query = null; + return true; + } +} + + + + + +/** + * Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738. + */ +class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 21; + + /** + * @type bool + */ + public $browsable = true; // usually + + /** + * @type bool + */ + public $hierarchical = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->query = null; + + // typecode check + $semicolon_pos = strrpos($uri->path, ';'); // reverse + if ($semicolon_pos !== false) { + $type = substr($uri->path, $semicolon_pos + 1); // no semicolon + $uri->path = substr($uri->path, 0, $semicolon_pos); + $type_ret = ''; + if (strpos($type, '=') !== false) { + // figure out whether or not the declaration is correct + list($key, $typecode) = explode('=', $type, 2); + if ($key !== 'type') { + // invalid key, tack it back on encoded + $uri->path .= '%3B' . $type; + } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') { + $type_ret = ";type=$typecode"; + } + } else { + $uri->path .= '%3B' . $type; + } + $uri->path = str_replace(';', '%3B', $uri->path); + $uri->path .= $type_ret; + } + return true; + } +} + + + + + +/** + * Validates http (HyperText Transfer Protocol) as defined by RFC 2616 + */ +class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 80; + + /** + * @type bool + */ + public $browsable = true; + + /** + * @type bool + */ + public $hierarchical = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + return true; + } +} + + + + + +/** + * Validates https (Secure HTTP) according to http scheme. + */ +class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http +{ + /** + * @type int + */ + public $default_port = 443; + /** + * @type bool + */ + public $secure = true; +} + + + + + +// VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the +// email is valid, but be careful! + +/** + * Validates mailto (for E-mail) according to RFC 2368 + * @todo Validate the email address + * @todo Filter allowed query parameters + */ + +class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = false; + + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + // we need to validate path against RFC 2368's addr-spec + return true; + } +} + + + + + +/** + * Validates news (Usenet) as defined by generic RFC 1738 + */ +class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme +{ + /** + * @type bool + */ + public $browsable = false; + + /** + * @type bool + */ + public $may_omit_host = true; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->query = null; + // typecode check needed on path + return true; + } +} + + + + + +/** + * Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738 + */ +class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme +{ + /** + * @type int + */ + public $default_port = 119; + + /** + * @type bool + */ + public $browsable = false; + + /** + * @param HTMLPurifier_URI $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function doValidate(&$uri, $config, $context) + { + $uri->userinfo = null; + $uri->query = null; + return true; + } +} + + + + + +/** + * Performs safe variable parsing based on types which can be used by + * users. This may not be able to represent all possible data inputs, + * however. + */ +class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser +{ + /** + * @param mixed $var + * @param int $type + * @param bool $allow_null + * @return array|bool|float|int|mixed|null|string + * @throws HTMLPurifier_VarParserException + */ + protected function parseImplementation($var, $type, $allow_null) + { + if ($allow_null && $var === null) { + return null; + } + switch ($type) { + // Note: if code "breaks" from the switch, it triggers a generic + // exception to be thrown. Specific errors can be specifically + // done here. + case self::MIXED: + case self::ISTRING: + case self::STRING: + case self::TEXT: + case self::ITEXT: + return $var; + case self::INT: + if (is_string($var) && ctype_digit($var)) { + $var = (int)$var; + } + return $var; + case self::FLOAT: + if ((is_string($var) && is_numeric($var)) || is_int($var)) { + $var = (float)$var; + } + return $var; + case self::BOOL: + if (is_int($var) && ($var === 0 || $var === 1)) { + $var = (bool)$var; + } elseif (is_string($var)) { + if ($var == 'on' || $var == 'true' || $var == '1') { + $var = true; + } elseif ($var == 'off' || $var == 'false' || $var == '0') { + $var = false; + } else { + throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type"); + } + } + return $var; + case self::ALIST: + case self::HASH: + case self::LOOKUP: + if (is_string($var)) { + // special case: technically, this is an array with + // a single empty string item, but having an empty + // array is more intuitive + if ($var == '') { + return array(); + } + if (strpos($var, "\n") === false && strpos($var, "\r") === false) { + // simplistic string to array method that only works + // for simple lists of tag names or alphanumeric characters + $var = explode(',', $var); + } else { + $var = preg_split('/(,|[\n\r]+)/', $var); + } + // remove spaces + foreach ($var as $i => $j) { + $var[$i] = trim($j); + } + if ($type === self::HASH) { + // key:value,key2:value2 + $nvar = array(); + foreach ($var as $keypair) { + $c = explode(':', $keypair, 2); + if (!isset($c[1])) { + continue; + } + $nvar[trim($c[0])] = trim($c[1]); + } + $var = $nvar; + } + } + if (!is_array($var)) { + break; + } + $keys = array_keys($var); + if ($keys === array_keys($keys)) { + if ($type == self::ALIST) { + return $var; + } elseif ($type == self::LOOKUP) { + $new = array(); + foreach ($var as $key) { + $new[$key] = true; + } + return $new; + } else { + break; + } + } + if ($type === self::ALIST) { + trigger_error("Array list did not have consecutive integer indexes", E_USER_WARNING); + return array_values($var); + } + if ($type === self::LOOKUP) { + foreach ($var as $key => $value) { + if ($value !== true) { + trigger_error( + "Lookup array has non-true value at key '$key'; " . + "maybe your input array was not indexed numerically", + E_USER_WARNING + ); + } + $var[$key] = true; + } + } + return $var; + default: + $this->errorInconsistent(__CLASS__, $type); + } + $this->errorGeneric($var, $type); + } +} + + + + + +/** + * This variable parser uses PHP's internal code engine. Because it does + * this, it can represent all inputs; however, it is dangerous and cannot + * be used by users. + */ +class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser +{ + + /** + * @param mixed $var + * @param int $type + * @param bool $allow_null + * @return null|string + */ + protected function parseImplementation($var, $type, $allow_null) + { + return $this->evalExpression($var); + } + + /** + * @param string $expr + * @return mixed + * @throws HTMLPurifier_VarParserException + */ + protected function evalExpression($expr) + { + $var = null; + $result = eval("\$var = $expr;"); + if ($result === false) { + throw new HTMLPurifier_VarParserException("Fatal error in evaluated code"); + } + return $var; + } +} + + + diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php new file mode 100644 index 0000000..d5906cd --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php @@ -0,0 +1,48 @@ +directives as $d) { + $schema->add( + $d->id->key, + $d->default, + $d->type, + $d->typeAllowsNull + ); + if ($d->allowed !== null) { + $schema->addAllowedValues( + $d->id->key, + $d->allowed + ); + } + foreach ($d->aliases as $alias) { + $schema->addAlias( + $alias->key, + $d->id->key + ); + } + if ($d->valueAliases !== null) { + $schema->addValueAliases( + $d->id->key, + $d->valueAliases + ); + } + } + $schema->postProcess(); + return $schema; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/Xml.php b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/Xml.php new file mode 100644 index 0000000..5fa56f7 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Builder/Xml.php @@ -0,0 +1,144 @@ +startElement('div'); + + $purifier = HTMLPurifier::getInstance(); + $html = $purifier->purify($html); + $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $this->writeRaw($html); + + $this->endElement(); // div + } + + /** + * @param mixed $var + * @return string + */ + protected function export($var) + { + if ($var === array()) { + return 'array()'; + } + return var_export($var, true); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + */ + public function build($interchange) + { + // global access, only use as last resort + $this->interchange = $interchange; + + $this->setIndent(true); + $this->startDocument('1.0', 'UTF-8'); + $this->startElement('configdoc'); + $this->writeElement('title', $interchange->name); + + foreach ($interchange->directives as $directive) { + $this->buildDirective($directive); + } + + if ($this->namespace) { + $this->endElement(); + } // namespace + + $this->endElement(); // configdoc + $this->flush(); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + */ + public function buildDirective($directive) + { + // Kludge, although I suppose having a notion of a "root namespace" + // certainly makes things look nicer when documentation is built. + // Depends on things being sorted. + if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { + if ($this->namespace) { + $this->endElement(); + } // namespace + $this->namespace = $directive->id->getRootNamespace(); + $this->startElement('namespace'); + $this->writeAttribute('id', $this->namespace); + $this->writeElement('name', $this->namespace); + } + + $this->startElement('directive'); + $this->writeAttribute('id', $directive->id->toString()); + + $this->writeElement('name', $directive->id->getDirective()); + + $this->startElement('aliases'); + foreach ($directive->aliases as $alias) { + $this->writeElement('alias', $alias->toString()); + } + $this->endElement(); // aliases + + $this->startElement('constraints'); + if ($directive->version) { + $this->writeElement('version', $directive->version); + } + $this->startElement('type'); + if ($directive->typeAllowsNull) { + $this->writeAttribute('allow-null', 'yes'); + } + $this->text($directive->type); + $this->endElement(); // type + if ($directive->allowed) { + $this->startElement('allowed'); + foreach ($directive->allowed as $value => $x) { + $this->writeElement('value', $value); + } + $this->endElement(); // allowed + } + $this->writeElement('default', $this->export($directive->default)); + $this->writeAttribute('xml:space', 'preserve'); + if ($directive->external) { + $this->startElement('external'); + foreach ($directive->external as $project) { + $this->writeElement('project', $project); + } + $this->endElement(); + } + $this->endElement(); // constraints + + if ($directive->deprecatedVersion) { + $this->startElement('deprecated'); + $this->writeElement('version', $directive->deprecatedVersion); + $this->writeElement('use', $directive->deprecatedUse->toString()); + $this->endElement(); // deprecated + } + + $this->startElement('description'); + $this->writeHTMLDiv($directive->description); + $this->endElement(); // description + + $this->endElement(); // directive + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Exception.php b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Exception.php new file mode 100644 index 0000000..2671516 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Exception.php @@ -0,0 +1,11 @@ + array(directive info) + * @type HTMLPurifier_ConfigSchema_Interchange_Directive[] + */ + public $directives = array(); + + /** + * Adds a directive array to $directives + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function addDirective($directive) + { + if (isset($this->directives[$i = $directive->id->toString()])) { + throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); + } + $this->directives[$i] = $directive; + } + + /** + * Convenience function to perform standard validation. Throws exception + * on failed validation. + */ + public function validate() + { + $validator = new HTMLPurifier_ConfigSchema_Validator(); + return $validator->validate($this); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Directive.php new file mode 100644 index 0000000..127a39a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Directive.php @@ -0,0 +1,89 @@ + true). + * Null if all values are allowed. + * @type array + */ + public $allowed; + + /** + * List of aliases for the directive. + * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). + * @type HTMLPurifier_ConfigSchema_Interchange_Id[] + */ + public $aliases = array(); + + /** + * Hash of value aliases, e.g. array('alt' => 'real'). Null if value + * aliasing is disabled (necessary for non-scalar types). + * @type array + */ + public $valueAliases; + + /** + * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. + * Null if the directive has always existed. + * @type string + */ + public $version; + + /** + * ID of directive that supercedes this old directive. + * Null if not deprecated. + * @type HTMLPurifier_ConfigSchema_Interchange_Id + */ + public $deprecatedUse; + + /** + * Version of HTML Purifier this directive was deprecated. Null if not + * deprecated. + * @type string + */ + public $deprecatedVersion; + + /** + * List of external projects this directive depends on, e.g. array('CSSTidy'). + * @type array + */ + public $external = array(); +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Id.php b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Id.php new file mode 100644 index 0000000..126f09d --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Interchange/Id.php @@ -0,0 +1,58 @@ +key = $key; + } + + /** + * @return string + * @warning This is NOT magic, to ensure that people don't abuse SPL and + * cause problems for PHP 5.0 support. + */ + public function toString() + { + return $this->key; + } + + /** + * @return string + */ + public function getRootNamespace() + { + return substr($this->key, 0, strpos($this->key, ".")); + } + + /** + * @return string + */ + public function getDirective() + { + return substr($this->key, strpos($this->key, ".") + 1); + } + + /** + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + public static function make($id) + { + return new HTMLPurifier_ConfigSchema_Interchange_Id($id); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/InterchangeBuilder.php new file mode 100644 index 0000000..655e6dd --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/InterchangeBuilder.php @@ -0,0 +1,226 @@ +varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); + } + + /** + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public static function buildFromDirectory($dir = null) + { + $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); + $interchange = new HTMLPurifier_ConfigSchema_Interchange(); + return $builder->buildDir($interchange, $dir); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public function buildDir($interchange, $dir = null) + { + if (!$dir) { + $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; + } + if (file_exists($dir . '/info.ini')) { + $info = parse_ini_file($dir . '/info.ini'); + $interchange->name = $info['name']; + } + + $files = array(); + $dh = opendir($dir); + while (false !== ($file = readdir($dh))) { + if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { + continue; + } + $files[] = $file; + } + closedir($dh); + + sort($files); + foreach ($files as $file) { + $this->buildFile($interchange, $dir . '/' . $file); + } + return $interchange; + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $file + */ + public function buildFile($interchange, $file) + { + $parser = new HTMLPurifier_StringHashParser(); + $this->build( + $interchange, + new HTMLPurifier_StringHash($parser->parseFile($file)) + ); + } + + /** + * Builds an interchange object based on a hash. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build + * @param HTMLPurifier_StringHash $hash source data + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function build($interchange, $hash) + { + if (!$hash instanceof HTMLPurifier_StringHash) { + $hash = new HTMLPurifier_StringHash($hash); + } + if (!isset($hash['ID'])) { + throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); + } + if (strpos($hash['ID'], '.') === false) { + if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { + $hash->offsetGet('DESCRIPTION'); // prevent complaining + } else { + throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); + } + } else { + $this->buildDirective($interchange, $hash); + } + $this->_findUnused($hash); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param HTMLPurifier_StringHash $hash + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function buildDirective($interchange, $hash) + { + $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); + + // These are required elements: + $directive->id = $this->id($hash->offsetGet('ID')); + $id = $directive->id->toString(); // convenience + + if (isset($hash['TYPE'])) { + $type = explode('/', $hash->offsetGet('TYPE')); + if (isset($type[1])) { + $directive->typeAllowsNull = true; + } + $directive->type = $type[0]; + } else { + throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); + } + + if (isset($hash['DEFAULT'])) { + try { + $directive->default = $this->varParser->parse( + $hash->offsetGet('DEFAULT'), + $directive->type, + $directive->typeAllowsNull + ); + } catch (HTMLPurifier_VarParserException $e) { + throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); + } + } + + if (isset($hash['DESCRIPTION'])) { + $directive->description = $hash->offsetGet('DESCRIPTION'); + } + + if (isset($hash['ALLOWED'])) { + $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); + } + + if (isset($hash['VALUE-ALIASES'])) { + $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); + } + + if (isset($hash['ALIASES'])) { + $raw_aliases = trim($hash->offsetGet('ALIASES')); + $aliases = preg_split('/\s*,\s*/', $raw_aliases); + foreach ($aliases as $alias) { + $directive->aliases[] = $this->id($alias); + } + } + + if (isset($hash['VERSION'])) { + $directive->version = $hash->offsetGet('VERSION'); + } + + if (isset($hash['DEPRECATED-USE'])) { + $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); + } + + if (isset($hash['DEPRECATED-VERSION'])) { + $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); + } + + if (isset($hash['EXTERNAL'])) { + $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); + } + + $interchange->addDirective($directive); + } + + /** + * Evaluates an array PHP code string without array() wrapper + * @param string $contents + */ + protected function evalArray($contents) + { + return eval('return array(' . $contents . ');'); + } + + /** + * Converts an array list into a lookup array. + * @param array $array + * @return array + */ + protected function lookup($array) + { + $ret = array(); + foreach ($array as $val) { + $ret[$val] = true; + } + return $ret; + } + + /** + * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id + * object based on a string Id. + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + protected function id($id) + { + return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); + } + + /** + * Triggers errors for any unused keys passed in the hash; such keys + * may indicate typos, missing values, etc. + * @param HTMLPurifier_StringHash $hash Hash to check. + */ + protected function _findUnused($hash) + { + $accessed = $hash->getAccessed(); + foreach ($hash as $k => $v) { + if (!isset($accessed[$k])) { + trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Validator.php b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Validator.php new file mode 100644 index 0000000..fb31277 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/Validator.php @@ -0,0 +1,248 @@ +parser = new HTMLPurifier_VarParser(); + } + + /** + * Validates a fully-formed interchange object. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @return bool + */ + public function validate($interchange) + { + $this->interchange = $interchange; + $this->aliases = array(); + // PHP is a bit lax with integer <=> string conversions in + // arrays, so we don't use the identical !== comparison + foreach ($interchange->directives as $i => $directive) { + $id = $directive->id->toString(); + if ($i != $id) { + $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); + } + $this->validateDirective($directive); + } + return true; + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. + * @param HTMLPurifier_ConfigSchema_Interchange_Id $id + */ + public function validateId($id) + { + $id_string = $id->toString(); + $this->context[] = "id '$id_string'"; + if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { + // handled by InterchangeBuilder + $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); + } + // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) + // we probably should check that it has at least one namespace + $this->with($id, 'key') + ->assertNotEmpty() + ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder + array_pop($this->context); + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirective($d) + { + $id = $d->id->toString(); + $this->context[] = "directive '$id'"; + $this->validateId($d->id); + + $this->with($d, 'description') + ->assertNotEmpty(); + + // BEGIN - handled by InterchangeBuilder + $this->with($d, 'type') + ->assertNotEmpty(); + $this->with($d, 'typeAllowsNull') + ->assertIsBool(); + try { + // This also tests validity of $d->type + $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); + } catch (HTMLPurifier_VarParserException $e) { + $this->error('default', 'had error: ' . $e->getMessage()); + } + // END - handled by InterchangeBuilder + + if (!is_null($d->allowed) || !empty($d->valueAliases)) { + // allowed and valueAliases require that we be dealing with + // strings, so check for that early. + $d_int = HTMLPurifier_VarParser::$types[$d->type]; + if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { + $this->error('type', 'must be a string type when used with allowed or value aliases'); + } + } + + $this->validateDirectiveAllowed($d); + $this->validateDirectiveValueAliases($d); + $this->validateDirectiveAliases($d); + + array_pop($this->context); + } + + /** + * Extra validation if $allowed member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAllowed($d) + { + if (is_null($d->allowed)) { + return; + } + $this->with($d, 'allowed') + ->assertNotEmpty() + ->assertIsLookup(); // handled by InterchangeBuilder + if (is_string($d->default) && !isset($d->allowed[$d->default])) { + $this->error('default', 'must be an allowed value'); + } + $this->context[] = 'allowed'; + foreach ($d->allowed as $val => $x) { + if (!is_string($val)) { + $this->error("value $val", 'must be a string'); + } + } + array_pop($this->context); + } + + /** + * Extra validation if $valueAliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveValueAliases($d) + { + if (is_null($d->valueAliases)) { + return; + } + $this->with($d, 'valueAliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'valueAliases'; + foreach ($d->valueAliases as $alias => $real) { + if (!is_string($alias)) { + $this->error("alias $alias", 'must be a string'); + } + if (!is_string($real)) { + $this->error("alias target $real from alias '$alias'", 'must be a string'); + } + if ($alias === $real) { + $this->error("alias '$alias'", "must not be an alias to itself"); + } + } + if (!is_null($d->allowed)) { + foreach ($d->valueAliases as $alias => $real) { + if (isset($d->allowed[$alias])) { + $this->error("alias '$alias'", 'must not be an allowed value'); + } elseif (!isset($d->allowed[$real])) { + $this->error("alias '$alias'", 'must be an alias to an allowed value'); + } + } + } + array_pop($this->context); + } + + /** + * Extra validation if $aliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAliases($d) + { + $this->with($d, 'aliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'aliases'; + foreach ($d->aliases as $alias) { + $this->validateId($alias); + $s = $alias->toString(); + if (isset($this->interchange->directives[$s])) { + $this->error("alias '$s'", 'collides with another directive'); + } + if (isset($this->aliases[$s])) { + $other_directive = $this->aliases[$s]; + $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); + } + $this->aliases[$s] = $d->id->toString(); + } + array_pop($this->context); + } + + // protected helper functions + + /** + * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom + * for validating simple member variables of objects. + * @param $obj + * @param $member + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + protected function with($obj, $member) + { + return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); + } + + /** + * Emits an error, providing helpful context. + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($target, $msg) + { + if ($target !== false) { + $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); + } else { + $prefix = ucfirst($this->getFormattedContext()); + } + throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); + } + + /** + * Returns a formatted context string. + * @return string + */ + protected function getFormattedContext() + { + return implode(' in ', array_reverse($this->context)); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/ValidatorAtom.php new file mode 100644 index 0000000..c9aa364 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/ValidatorAtom.php @@ -0,0 +1,130 @@ +context = $context; + $this->obj = $obj; + $this->member = $member; + $this->contents =& $obj->$member; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsString() + { + if (!is_string($this->contents)) { + $this->error('must be a string'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsBool() + { + if (!is_bool($this->contents)) { + $this->error('must be a boolean'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsArray() + { + if (!is_array($this->contents)) { + $this->error('must be an array'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotNull() + { + if ($this->contents === null) { + $this->error('must not be null'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertAlnum() + { + $this->assertIsString(); + if (!ctype_alnum($this->contents)) { + $this->error('must be alphanumeric'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotEmpty() + { + if (empty($this->contents)) { + $this->error('must not be empty'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsLookup() + { + $this->assertIsArray(); + foreach ($this->contents as $v) { + if ($v !== true) { + $this->error('must be a lookup array'); + } + } + return $this; + } + + /** + * @param string $msg + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($msg) + { + throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema.ser b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema.ser new file mode 100644 index 0000000000000000000000000000000000000000..22ea32185db63b19d525f509ebe431f593e92271 GIT binary patch literal 15000 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 QaEIMR#+=+~=6(D2AIX)%zyJUM literal 0 HcmV?d00001 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt new file mode 100644 index 0000000..0517fed --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt @@ -0,0 +1,8 @@ +Attr.AllowedClasses +TYPE: lookup/null +VERSION: 4.0.0 +DEFAULT: null +--DESCRIPTION-- +List of allowed class values in the class attribute. By default, this is null, +which means all classes are allowed. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt new file mode 100644 index 0000000..249edd6 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt @@ -0,0 +1,12 @@ +Attr.AllowedFrameTargets +TYPE: lookup +DEFAULT: array() +--DESCRIPTION-- +Lookup table of all allowed link frame targets. Some commonly used link +targets include _blank, _self, _parent and _top. Values should be +lowercase, as validation will be done in a case-sensitive manner despite +W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute +so this directive will have no effect in that doctype. XHTML 1.1 does not +enable the Target module by default, you will have to manually enable it +(see the module documentation for more details.) +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt new file mode 100644 index 0000000..9a8fa6a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt @@ -0,0 +1,9 @@ +Attr.AllowedRel +TYPE: lookup +VERSION: 1.6.0 +DEFAULT: array() +--DESCRIPTION-- +List of allowed forward document relationships in the rel attribute. Common +values may be nofollow or print. By default, this is empty, meaning that no +document relationships are allowed. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt new file mode 100644 index 0000000..b017883 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt @@ -0,0 +1,9 @@ +Attr.AllowedRev +TYPE: lookup +VERSION: 1.6.0 +DEFAULT: array() +--DESCRIPTION-- +List of allowed reverse document relationships in the rev attribute. This +attribute is a bit of an edge-case; if you don't know what it is for, stay +away. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt new file mode 100644 index 0000000..e774b82 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt @@ -0,0 +1,19 @@ +Attr.ClassUseCDATA +TYPE: bool/null +DEFAULT: null +VERSION: 4.0.0 +--DESCRIPTION-- +If null, class will auto-detect the doctype and, if matching XHTML 1.1 or +XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise, +it will use a relaxed CDATA definition. If true, the relaxed CDATA definition +is forced; if false, the NMTOKENS definition is forced. To get behavior +of HTML Purifier prior to 4.0.0, set this directive to false. + +Some rational behind the auto-detection: +in previous versions of HTML Purifier, it was assumed that the form of +class was NMTOKENS, as specified by the XHTML Modularization (representing +XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however +specify class as CDATA. HTML 5 effectively defines it as CDATA, but +with the additional constraint that each name should be unique (this is not +explicitly outlined in previous specifications). +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt new file mode 100644 index 0000000..533165e --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt @@ -0,0 +1,11 @@ +Attr.DefaultImageAlt +TYPE: string/null +DEFAULT: null +VERSION: 3.2.0 +--DESCRIPTION-- +This is the content of the alt tag of an image if the user had not +previously specified an alt attribute. This applies to all images without +a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which +only applies to invalid images, and overrides in the case of an invalid image. +Default behavior with null is to use the basename of the src tag for the alt. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt new file mode 100644 index 0000000..9eb7e38 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt @@ -0,0 +1,9 @@ +Attr.DefaultInvalidImage +TYPE: string +DEFAULT: '' +--DESCRIPTION-- +This is the default image an img tag will be pointed to if it does not have +a valid src attribute. In future versions, we may allow the image tag to +be removed completely, but due to design issues, this is not possible right +now. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt new file mode 100644 index 0000000..2f17bf4 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt @@ -0,0 +1,8 @@ +Attr.DefaultInvalidImageAlt +TYPE: string +DEFAULT: 'Invalid image' +--DESCRIPTION-- +This is the content of the alt tag of an invalid image if the user had not +previously specified an alt attribute. It has no effect when the image is +valid but there was no alt attribute present. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt new file mode 100644 index 0000000..52654b5 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt @@ -0,0 +1,10 @@ +Attr.DefaultTextDir +TYPE: string +DEFAULT: 'ltr' +--DESCRIPTION-- +Defines the default text direction (ltr or rtl) of the document being +parsed. This generally is the same as the value of the dir attribute in +HTML, or ltr if that is not specified. +--ALLOWED-- +'ltr', 'rtl' +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt new file mode 100644 index 0000000..6440d21 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt @@ -0,0 +1,16 @@ +Attr.EnableID +TYPE: bool +DEFAULT: false +VERSION: 1.2.0 +--DESCRIPTION-- +Allows the ID attribute in HTML. This is disabled by default due to the +fact that without proper configuration user input can easily break the +validation of a webpage by specifying an ID that is already on the +surrounding HTML. If you don't mind throwing caution to the wind, enable +this directive, but I strongly recommend you also consider blacklisting IDs +you use (%Attr.IDBlacklist) or prefixing all user supplied IDs +(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of +pre-1.2.0 versions. +--ALIASES-- +HTML.EnableAttrID +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt new file mode 100644 index 0000000..f31d226 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt @@ -0,0 +1,8 @@ +Attr.ForbiddenClasses +TYPE: lookup +VERSION: 4.0.0 +DEFAULT: array() +--DESCRIPTION-- +List of forbidden class values in the class attribute. By default, this is +empty, which means that no classes are forbidden. See also %Attr.AllowedClasses. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt new file mode 100644 index 0000000..5f2b5e3 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt @@ -0,0 +1,5 @@ +Attr.IDBlacklist +TYPE: list +DEFAULT: array() +DESCRIPTION: Array of IDs not allowed in the document. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt new file mode 100644 index 0000000..6f58245 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt @@ -0,0 +1,9 @@ +Attr.IDBlacklistRegexp +TYPE: string/null +VERSION: 1.6.0 +DEFAULT: NULL +--DESCRIPTION-- +PCRE regular expression to be matched against all IDs. If the expression is +matches, the ID is rejected. Use this with care: may cause significant +degradation. ID matching is done after all other validation. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt new file mode 100644 index 0000000..cc49d43 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt @@ -0,0 +1,12 @@ +Attr.IDPrefix +TYPE: string +VERSION: 1.2.0 +DEFAULT: '' +--DESCRIPTION-- +String to prefix to IDs. If you have no idea what IDs your pages may use, +you may opt to simply add a prefix to all user-submitted ID attributes so +that they are still usable, but will not conflict with core page IDs. +Example: setting the directive to 'user_' will result in a user submitted +'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true +before using this. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt new file mode 100644 index 0000000..2c5924a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt @@ -0,0 +1,14 @@ +Attr.IDPrefixLocal +TYPE: string +VERSION: 1.2.0 +DEFAULT: '' +--DESCRIPTION-- +Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you +need to allow multiple sets of user content on web page, you may need to +have a seperate prefix that changes with each iteration. This way, +seperately submitted user content displayed on the same page doesn't +clobber each other. Ideal values are unique identifiers for the content it +represents (i.e. the id of the row in the database). Be sure to add a +seperator (like an underscore) at the end. Warning: this directive will +not work unless %Attr.IDPrefix is set to a non-empty value! +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt new file mode 100644 index 0000000..d5caa1b --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt @@ -0,0 +1,31 @@ +AutoFormat.AutoParagraph +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

        + This directive turns on auto-paragraphing, where double newlines are + converted in to paragraphs whenever possible. Auto-paragraphing: +

        +
          +
        • Always applies to inline elements or text in the root node,
        • +
        • Applies to inline elements or text with double newlines in nodes + that allow paragraph tags,
        • +
        • Applies to double newlines in paragraph tags
        • +
        +

        + p tags must be allowed for this directive to take effect. + We do not use br tags for paragraphing, as that is + semantically incorrect. +

        +

        + To prevent auto-paragraphing as a content-producer, refrain from using + double-newlines except to specify a new paragraph or in contexts where + it has special meaning (whitespace usually has no meaning except in + tags like pre, so this should not be difficult.) To prevent + the paragraphing of inline text adjacent to block elements, wrap them + in div tags (the behavior is slightly different outside of + the root node.) +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt new file mode 100644 index 0000000..2a47648 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt @@ -0,0 +1,12 @@ +AutoFormat.Custom +TYPE: list +VERSION: 2.0.1 +DEFAULT: array() +--DESCRIPTION-- + +

        + This directive can be used to add custom auto-format injectors. + Specify an array of injector names (class name minus the prefix) + or concrete implementations. Injector class must exist. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt new file mode 100644 index 0000000..663064a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt @@ -0,0 +1,11 @@ +AutoFormat.DisplayLinkURI +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

        + This directive turns on the in-text display of URIs in <a> tags, and disables + those links. For example, example becomes + example (http://example.com). +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt new file mode 100644 index 0000000..3a48ba9 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt @@ -0,0 +1,12 @@ +AutoFormat.Linkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

        + This directive turns on linkification, auto-linking http, ftp and + https URLs. a tags with the href attribute + must be allowed. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt new file mode 100644 index 0000000..db58b13 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt @@ -0,0 +1,12 @@ +AutoFormat.PurifierLinkify.DocURL +TYPE: string +VERSION: 2.0.1 +DEFAULT: '#%s' +ALIASES: AutoFormatParam.PurifierLinkifyDocURL +--DESCRIPTION-- +

        + Location of configuration documentation to link to, let %s substitute + into the configuration's namespace and directive names sans the percent + sign. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt new file mode 100644 index 0000000..7996488 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt @@ -0,0 +1,12 @@ +AutoFormat.PurifierLinkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

        + Internal auto-formatter that converts configuration directives in + syntax %Namespace.Directive to links. a tags + with the href attribute must be allowed. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt new file mode 100644 index 0000000..35c393b --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt @@ -0,0 +1,11 @@ +AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions +TYPE: lookup +VERSION: 4.0.0 +DEFAULT: array('td' => true, 'th' => true) +--DESCRIPTION-- +

        + When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp + are enabled, this directive defines what HTML elements should not be + removede if they have only a non-breaking space in them. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt new file mode 100644 index 0000000..ca17eb1 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt @@ -0,0 +1,15 @@ +AutoFormat.RemoveEmpty.RemoveNbsp +TYPE: bool +VERSION: 4.0.0 +DEFAULT: false +--DESCRIPTION-- +

        + When enabled, HTML Purifier will treat any elements that contain only + non-breaking spaces as well as regular whitespace as empty, and remove + them when %AutoForamt.RemoveEmpty is enabled. +

        +

        + See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements + that don't have this behavior applied to them. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt new file mode 100644 index 0000000..34657ba --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt @@ -0,0 +1,46 @@ +AutoFormat.RemoveEmpty +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

        + When enabled, HTML Purifier will attempt to remove empty elements that + contribute no semantic information to the document. The following types + of nodes will be removed: +

        +
        • + Tags with no attributes and no content, and that are not empty + elements (remove <a></a> but not + <br />), and +
        • +
        • + Tags with no content, except for:
            +
          • The colgroup element, or
          • +
          • + Elements with the id or name attribute, + when those attributes are permitted on those elements. +
          • +
        • +
        +

        + Please be very careful when using this functionality; while it may not + seem that empty elements contain useful information, they can alter the + layout of a document given appropriate styling. This directive is most + useful when you are processing machine-generated HTML, please avoid using + it on regular user HTML. +

        +

        + Elements that contain only whitespace will be treated as empty. Non-breaking + spaces, however, do not count as whitespace. See + %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior. +

        +

        + This algorithm is not perfect; you may still notice some empty tags, + particularly if a node had elements, but those elements were later removed + because they were not permitted in that context, or tags that, after + being auto-closed by another tag, where empty. This is for safety reasons + to prevent clever code from breaking validation. The general rule of thumb: + if a tag looked empty on the way in, it will get removed; if HTML Purifier + made it empty, it will stay. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt new file mode 100644 index 0000000..dde990a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt @@ -0,0 +1,11 @@ +AutoFormat.RemoveSpansWithoutAttributes +TYPE: bool +VERSION: 4.0.1 +DEFAULT: false +--DESCRIPTION-- +

        + This directive causes span tags without any attributes + to be removed. It will also remove spans that had all attributes + removed during processing. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt new file mode 100644 index 0000000..b324608 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt @@ -0,0 +1,8 @@ +CSS.AllowImportant +TYPE: bool +DEFAULT: false +VERSION: 3.1.0 +--DESCRIPTION-- +This parameter determines whether or not !important cascade modifiers should +be allowed in user CSS. If false, !important will stripped. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt new file mode 100644 index 0000000..748be0e --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt @@ -0,0 +1,11 @@ +CSS.AllowTricky +TYPE: bool +DEFAULT: false +VERSION: 3.1.0 +--DESCRIPTION-- +This parameter determines whether or not to allow "tricky" CSS properties and +values. Tricky CSS properties/values can drastically modify page layout or +be used for deceptive practices but do not directly constitute a security risk. +For example, display:none; is considered a tricky property that +will only be allowed if this directive is set to true. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt new file mode 100644 index 0000000..3fd4654 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt @@ -0,0 +1,12 @@ +CSS.AllowedFonts +TYPE: lookup/null +VERSION: 4.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

        + Allows you to manually specify a set of allowed fonts. If + NULL, all fonts are allowed. This directive + affects generic names (serif, sans-serif, monospace, cursive, + fantasy) as well as specific font families. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt new file mode 100644 index 0000000..460112e --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt @@ -0,0 +1,18 @@ +CSS.AllowedProperties +TYPE: lookup/null +VERSION: 3.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + If HTML Purifier's style attributes set is unsatisfactory for your needs, + you can overload it with your own list of tags to allow. Note that this + method is subtractive: it does its job by taking away from HTML Purifier + usual feature set, so you cannot add an attribute that HTML Purifier never + supported in the first place. +

        +

        + Warning: If another directive conflicts with the + elements here, that directive will win and override. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt new file mode 100644 index 0000000..5cb7dda --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt @@ -0,0 +1,11 @@ +CSS.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

        + Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt new file mode 100644 index 0000000..f1f5c5f --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt @@ -0,0 +1,13 @@ +CSS.ForbiddenProperties +TYPE: lookup +VERSION: 4.2.0 +DEFAULT: array() +--DESCRIPTION-- +

        + This is the logical inverse of %CSS.AllowedProperties, and it will + override that directive or any other directive. If possible, + %CSS.AllowedProperties is recommended over this directive, + because it can sometimes be difficult to tell whether or not you've + forbidden all of the CSS properties you truly would like to disallow. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt new file mode 100644 index 0000000..7a32914 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt @@ -0,0 +1,16 @@ +CSS.MaxImgLength +TYPE: string/null +DEFAULT: '1200px' +VERSION: 3.1.1 +--DESCRIPTION-- +

        + This parameter sets the maximum allowed length on img tags, + effectively the width and height properties. + Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is + in place to prevent imagecrash attacks, disable with null at your own risk. + This directive is similar to %HTML.MaxImgLength, and both should be + concurrently edited, although there are + subtle differences in the input format (the CSS max is a number with + a unit). +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt new file mode 100644 index 0000000..148eedb --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt @@ -0,0 +1,10 @@ +CSS.Proprietary +TYPE: bool +VERSION: 3.0.0 +DEFAULT: false +--DESCRIPTION-- + +

        + Whether or not to allow safe, proprietary CSS values. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt new file mode 100644 index 0000000..e733a61 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt @@ -0,0 +1,9 @@ +CSS.Trusted +TYPE: bool +VERSION: 4.2.1 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user's CSS input is trusted or not. If the +input is trusted, a more expansive set of allowed properties. See +also %HTML.Trusted. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt new file mode 100644 index 0000000..c486724 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt @@ -0,0 +1,14 @@ +Cache.DefinitionImpl +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: 'Serializer' +--DESCRIPTION-- + +This directive defines which method to use when caching definitions, +the complex data-type that makes HTML Purifier tick. Set to null +to disable caching (not recommended, as you will see a definite +performance degradation). + +--ALIASES-- +Core.DefinitionCache +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt new file mode 100644 index 0000000..5403650 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt @@ -0,0 +1,13 @@ +Cache.SerializerPath +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + Absolute path with no trailing slash to store serialized definitions in. + Default is within the + HTML Purifier library inside DefinitionCache/Serializer. This + path must be writable by the webserver. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt new file mode 100644 index 0000000..b2b83d9 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt @@ -0,0 +1,11 @@ +Cache.SerializerPermissions +TYPE: int +VERSION: 4.3.0 +DEFAULT: 0755 +--DESCRIPTION-- + +

        + Directory permissions of the files and directories created inside + the DefinitionCache/Serializer or other custom serializer path. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt new file mode 100644 index 0000000..568cbf3 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt @@ -0,0 +1,18 @@ +Core.AggressivelyFixLt +TYPE: bool +VERSION: 2.1.0 +DEFAULT: true +--DESCRIPTION-- +

        + This directive enables aggressive pre-filter fixes HTML Purifier can + perform in order to ensure that open angled-brackets do not get killed + during parsing stage. Enabling this will result in two preg_replace_callback + calls and at least two preg_replace calls for every HTML document parsed; + if your users make very well-formed HTML, you can set this directive false. + This has no effect when DirectLex is used. +

        +

        + Notice: This directive's default turned from false to true + in HTML Purifier 3.2.0. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt new file mode 100644 index 0000000..2c910cc --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt @@ -0,0 +1,16 @@ +Core.AllowHostnameUnderscore +TYPE: bool +VERSION: 4.6.0 +DEFAULT: false +--DESCRIPTION-- +

        + By RFC 1123, underscores are not permitted in host names. + (This is in contrast to the specification for DNS, RFC + 2181, which allows underscores.) + However, most browsers do the right thing when faced with + an underscore in the host name, and so some poorly written + websites are written with the expectation this should work. + Setting this parameter to true relaxes our allowed character + check so that underscores are permitted. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt new file mode 100644 index 0000000..d731791 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt @@ -0,0 +1,12 @@ +Core.CollectErrors +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- + +Whether or not to collect errors found while filtering the document. This +is a useful way to give feedback to your users. Warning: +Currently this feature is very patchy and experimental, with lots of +possible error messages not yet implemented. It will not cause any +problems, but it may not help your users either. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt new file mode 100644 index 0000000..c572c14 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt @@ -0,0 +1,29 @@ +Core.ColorKeywords +TYPE: hash +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'maroon' => '#800000', + 'red' => '#FF0000', + 'orange' => '#FFA500', + 'yellow' => '#FFFF00', + 'olive' => '#808000', + 'purple' => '#800080', + 'fuchsia' => '#FF00FF', + 'white' => '#FFFFFF', + 'lime' => '#00FF00', + 'green' => '#008000', + 'navy' => '#000080', + 'blue' => '#0000FF', + 'aqua' => '#00FFFF', + 'teal' => '#008080', + 'black' => '#000000', + 'silver' => '#C0C0C0', + 'gray' => '#808080', +) +--DESCRIPTION-- + +Lookup array of color names to six digit hexadecimal number corresponding +to color, with preceding hash mark. Used when parsing colors. The lookup +is done in a case-insensitive manner. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt new file mode 100644 index 0000000..64b114f --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt @@ -0,0 +1,14 @@ +Core.ConvertDocumentToFragment +TYPE: bool +DEFAULT: true +--DESCRIPTION-- + +This parameter determines whether or not the filter should convert +input that is a full document with html and body tags to a fragment +of just the contents of a body tag. This parameter is simply something +HTML Purifier can do during an edge-case: for most inputs, this +processing is not necessary. + +--ALIASES-- +Core.AcceptFullDocuments +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt new file mode 100644 index 0000000..36f16e0 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt @@ -0,0 +1,17 @@ +Core.DirectLexLineNumberSyncInterval +TYPE: int +VERSION: 2.0.0 +DEFAULT: 0 +--DESCRIPTION-- + +

        + Specifies the number of tokens the DirectLex line number tracking + implementations should process before attempting to resyncronize the + current line count by manually counting all previous new-lines. When + at 0, this functionality is disabled. Lower values will decrease + performance, and this is only strictly necessary if the counting + algorithm is buggy (in which case you should report it as a bug). + This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is + not being used. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt new file mode 100644 index 0000000..1cd4c2c --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt @@ -0,0 +1,14 @@ +Core.DisableExcludes +TYPE: bool +DEFAULT: false +VERSION: 4.5.0 +--DESCRIPTION-- +

        + This directive disables SGML-style exclusions, e.g. the exclusion of + <object> in any descendant of a + <pre> tag. Disabling excludes will allow some + invalid documents to pass through HTML Purifier, but HTML Purifier + will also be less likely to accidentally remove large documents during + processing. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt new file mode 100644 index 0000000..ce243c3 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt @@ -0,0 +1,9 @@ +Core.EnableIDNA +TYPE: bool +DEFAULT: false +VERSION: 4.4.0 +--DESCRIPTION-- +Allows international domain names in URLs. This configuration option +requires the PEAR Net_IDNA2 module to be installed. It operates by +punycoding any internationalized host names for maximum portability. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt new file mode 100644 index 0000000..8bfb47c --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt @@ -0,0 +1,15 @@ +Core.Encoding +TYPE: istring +DEFAULT: 'utf-8' +--DESCRIPTION-- +If for some reason you are unable to convert all webpages to UTF-8, you can +use this directive as a stop-gap compatibility change to let HTML Purifier +deal with non UTF-8 input. This technique has notable deficiencies: +absolutely no characters outside of the selected character encoding will be +preserved, not even the ones that have been ampersand escaped (this is due +to a UTF-8 specific feature that automatically resolves all +entities), making it pretty useless for anything except the most I18N-blind +applications, although %Core.EscapeNonASCIICharacters offers fixes this +trouble with another tradeoff. This directive only accepts ISO-8859-1 if +iconv is not enabled. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt new file mode 100644 index 0000000..a3881be --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt @@ -0,0 +1,12 @@ +Core.EscapeInvalidChildren +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +

        Warning: this configuration option is no longer does anything as of 4.6.0.

        + +

        When true, a child is found that is not allowed in the context of the +parent element will be transformed into text as if it were ASCII. When +false, that element and all internal tags will be dropped, though text will +be preserved. There is no option for dropping the element but preserving +child nodes.

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt new file mode 100644 index 0000000..a7a5b24 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt @@ -0,0 +1,7 @@ +Core.EscapeInvalidTags +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When true, invalid tags will be written back to the document as plain text. +Otherwise, they are silently dropped. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt new file mode 100644 index 0000000..abb4999 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt @@ -0,0 +1,13 @@ +Core.EscapeNonASCIICharacters +TYPE: bool +VERSION: 1.4.0 +DEFAULT: false +--DESCRIPTION-- +This directive overcomes a deficiency in %Core.Encoding by blindly +converting all non-ASCII characters into decimal numeric entities before +converting it to its native encoding. This means that even characters that +can be expressed in the non-UTF-8 encoding will be entity-ized, which can +be a real downer for encodings like Big5. It also assumes that the ASCII +repetoire is available, although this is the case for almost all encodings. +Anyway, use UTF-8! +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt new file mode 100644 index 0000000..915391e --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt @@ -0,0 +1,19 @@ +Core.HiddenElements +TYPE: lookup +--DEFAULT-- +array ( + 'script' => true, + 'style' => true, +) +--DESCRIPTION-- + +

        + This directive is a lookup array of elements which should have their + contents removed when they are not allowed by the HTML definition. + For example, the contents of a script tag are not + normally shown in a document, so if script tags are to be removed, + their contents should be removed to. This is opposed to a b + tag, which defines some presentational changes but does not hide its + contents. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.Language.txt new file mode 100644 index 0000000..233fca1 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.Language.txt @@ -0,0 +1,10 @@ +Core.Language +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'en' +--DESCRIPTION-- + +ISO 639 language code for localizable things in HTML Purifier to use, +which is mainly error reporting. There is currently only an English (en) +translation, so this directive is currently useless. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt new file mode 100644 index 0000000..8983e2c --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt @@ -0,0 +1,34 @@ +Core.LexerImpl +TYPE: mixed/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + This parameter determines what lexer implementation can be used. The + valid values are: +

        +
        +
        null
        +
        + Recommended, the lexer implementation will be auto-detected based on + your PHP-version and configuration. +
        +
        string lexer identifier
        +
        + This is a slim way of manually overridding the implementation. + Currently recognized values are: DOMLex (the default PHP5 +implementation) + and DirectLex (the default PHP4 implementation). Only use this if + you know what you are doing: usually, the auto-detection will + manage things for cases you aren't even aware of. +
        +
        object lexer instance
        +
        + Super-advanced: you can specify your own, custom, implementation that + implements the interface defined by HTMLPurifier_Lexer. + I may remove this option simply because I don't expect anyone + to use it. +
        +
        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt new file mode 100644 index 0000000..eb841a7 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt @@ -0,0 +1,16 @@ +Core.MaintainLineNumbers +TYPE: bool/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + If true, HTML Purifier will add line number information to all tokens. + This is useful when error reporting is turned on, but can result in + significant performance degradation and should not be used when + unnecessary. This directive must be used with the DirectLex lexer, + as the DOMLex lexer does not (yet) support this functionality. + If the value is null, an appropriate value will be selected based + on other configuration. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt new file mode 100644 index 0000000..d77f536 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt @@ -0,0 +1,11 @@ +Core.NormalizeNewlines +TYPE: bool +VERSION: 4.2.0 +DEFAULT: true +--DESCRIPTION-- +

        + Whether or not to normalize newlines to the operating + system default. When false, HTML Purifier + will attempt to preserve mixed newline files. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt new file mode 100644 index 0000000..4070c2a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt @@ -0,0 +1,12 @@ +Core.RemoveInvalidImg +TYPE: bool +DEFAULT: true +VERSION: 1.3.0 +--DESCRIPTION-- + +

        + This directive enables pre-emptive URI checking in img + tags, as the attribute validation strategy is not authorized to + remove elements from the document. Revert to pre-1.3.0 behavior by setting to false. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt new file mode 100644 index 0000000..3397d9f --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt @@ -0,0 +1,11 @@ +Core.RemoveProcessingInstructions +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +Instead of escaping processing instructions in the form <? ... +?>, remove it out-right. This may be useful if the HTML +you are validating contains XML processing instruction gunk, however, +it can also be user-unfriendly for people attempting to post PHP +snippets. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt new file mode 100644 index 0000000..a4cd966 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt @@ -0,0 +1,12 @@ +Core.RemoveScriptContents +TYPE: bool/null +DEFAULT: NULL +VERSION: 2.0.0 +DEPRECATED-VERSION: 2.1.0 +DEPRECATED-USE: Core.HiddenElements +--DESCRIPTION-- +

        + This directive enables HTML Purifier to remove not only script tags + but all of their contents. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt new file mode 100644 index 0000000..3db50ef --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt @@ -0,0 +1,11 @@ +Filter.Custom +TYPE: list +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

        + This directive can be used to add custom filters; it is nearly the + equivalent of the now deprecated HTMLPurifier->addFilter() + method. Specify an array of concrete implementations. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt new file mode 100644 index 0000000..16829bc --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt @@ -0,0 +1,14 @@ +Filter.ExtractStyleBlocks.Escaping +TYPE: bool +VERSION: 3.0.0 +DEFAULT: true +ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping +--DESCRIPTION-- + +

        + Whether or not to escape the dangerous characters <, > and & + as \3C, \3E and \26, respectively. This is can be safely set to false + if the contents of StyleBlocks will be placed in an external stylesheet, + where there is no risk of it being interpreted as HTML. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt new file mode 100644 index 0000000..7f95f54 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt @@ -0,0 +1,29 @@ +Filter.ExtractStyleBlocks.Scope +TYPE: string/null +VERSION: 3.0.0 +DEFAULT: NULL +ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope +--DESCRIPTION-- + +

        + If you would like users to be able to define external stylesheets, but + only allow them to specify CSS declarations for a specific node and + prevent them from fiddling with other elements, use this directive. + It accepts any valid CSS selector, and will prepend this to any + CSS declaration extracted from the document. For example, if this + directive is set to #user-content and a user uses the + selector a:hover, the final selector will be + #user-content a:hover. +

        +

        + The comma shorthand may be used; consider the above example, with + #user-content, #user-content2, the final selector will + be #user-content a:hover, #user-content2 a:hover. +

        +

        + Warning: It is possible for users to bypass this measure + using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML + Purifier, and I am working to get it fixed. Until then, HTML Purifier + performs a basic check to prevent this. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt new file mode 100644 index 0000000..6c231b2 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt @@ -0,0 +1,16 @@ +Filter.ExtractStyleBlocks.TidyImpl +TYPE: mixed/null +VERSION: 3.1.0 +DEFAULT: NULL +ALIASES: FilterParam.ExtractStyleBlocksTidyImpl +--DESCRIPTION-- +

        + If left NULL, HTML Purifier will attempt to instantiate a csstidy + class to use for internal cleaning. This will usually be good enough. +

        +

        + However, for trusted user input, you can set this to false to + disable cleaning. In addition, you can supply your own concrete implementation + of Tidy's interface to use, although I don't know why you'd want to do that. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt new file mode 100644 index 0000000..078d087 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt @@ -0,0 +1,74 @@ +Filter.ExtractStyleBlocks +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +EXTERNAL: CSSTidy +--DESCRIPTION-- +

        + This directive turns on the style block extraction filter, which removes + style blocks from input HTML, cleans them up with CSSTidy, + and places them in the StyleBlocks context variable, for further + use by you, usually to be placed in an external stylesheet, or a + style block in the head of your document. +

        +

        + Sample usage: +

        +
        ';
        +?>
        +
        +
        +
        +  Filter.ExtractStyleBlocks
        +body {color:#F00;} Some text';
        +
        +    $config = HTMLPurifier_Config::createDefault();
        +    $config->set('Filter', 'ExtractStyleBlocks', true);
        +    $purifier = new HTMLPurifier($config);
        +
        +    $html = $purifier->purify($dirty);
        +
        +    // This implementation writes the stylesheets to the styles/ directory.
        +    // You can also echo the styles inside the document, but it's a bit
        +    // more difficult to make sure they get interpreted properly by
        +    // browsers; try the usual CSS armoring techniques.
        +    $styles = $purifier->context->get('StyleBlocks');
        +    $dir = 'styles/';
        +    if (!is_dir($dir)) mkdir($dir);
        +    $hash = sha1($_GET['html']);
        +    foreach ($styles as $i => $style) {
        +        file_put_contents($name = $dir . $hash . "_$i");
        +        echo '';
        +    }
        +?>
        +
        +
        +  
        + +
        + + +]]>
        +

        + Warning: It is possible for a user to mount an + imagecrash attack using this CSS. Counter-measures are difficult; + it is not simply enough to limit the range of CSS lengths (using + relative lengths with many nesting levels allows for large values + to be attained without actually specifying them in the stylesheet), + and the flexible nature of selectors makes it difficult to selectively + disable lengths on image tags (HTML Purifier, however, does disable + CSS width and height in inline styling). There are probably two effective + counter measures: an explicit width and height set to auto in all + images in your document (unlikely) or the disabling of width and + height (somewhat reasonable). Whether or not these measures should be + used is left to the reader. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt new file mode 100644 index 0000000..321eaa2 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt @@ -0,0 +1,16 @@ +Filter.YouTube +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

        + Warning: Deprecated in favor of %HTML.SafeObject and + %Output.FlashCompat (turn both on to allow YouTube videos and other + Flash content). +

        +

        + This directive enables YouTube video embedding in HTML Purifier. Check + this document + on embedding videos for more information on what this filter does. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt new file mode 100644 index 0000000..0b2c106 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt @@ -0,0 +1,25 @@ +HTML.Allowed +TYPE: itext/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + This is a preferred convenience directive that combines + %HTML.AllowedElements and %HTML.AllowedAttributes. + Specify elements and attributes that are allowed using: + element1[attr1|attr2],element2.... For example, + if you would like to only allow paragraphs and links, specify + a[href],p. You can specify attributes that apply + to all elements using an asterisk, e.g. *[lang]. + You can also use newlines instead of commas to separate elements. +

        +

        + Warning: + All of the constraints on the component directives are still enforced. + The syntax is a subset of TinyMCE's valid_elements + whitelist: directly copy-pasting it here will probably result in + broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes + are set, this directive has no effect. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt new file mode 100644 index 0000000..fcf093f --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt @@ -0,0 +1,19 @@ +HTML.AllowedAttributes +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + If HTML Purifier's attribute set is unsatisfactory, overload it! + The syntax is "tag.attr" or "*.attr" for the global attributes + (style, id, class, dir, lang, xml:lang). +

        +

        + Warning: If another directive conflicts with the + elements here, that directive will win and override. For + example, %HTML.EnableAttrID will take precedence over *.id in this + directive. You must set that directive to true before you can use + IDs at all. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt new file mode 100644 index 0000000..140e214 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt @@ -0,0 +1,10 @@ +HTML.AllowedComments +TYPE: lookup +VERSION: 4.4.0 +DEFAULT: array() +--DESCRIPTION-- +A whitelist which indicates what explicit comment bodies should be +allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp +(these directives are union'ed together, so a comment is considered +valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt new file mode 100644 index 0000000..f22e977 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt @@ -0,0 +1,15 @@ +HTML.AllowedCommentsRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +A regexp, which if it matches the body of a comment, indicates that +it should be allowed. Trailing and leading spaces are removed prior +to running this regular expression. +Warning: Make sure you specify +correct anchor metacharacters ^regex$, otherwise you may accept +comments that you did not mean to! In particular, the regex /foo|bar/ +is probably not sufficiently strict, since it also allows foobar. +See also %HTML.AllowedComments (these directives are union'ed together, +so a comment is considered valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt new file mode 100644 index 0000000..1d3fa79 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt @@ -0,0 +1,23 @@ +HTML.AllowedElements +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

        + If HTML Purifier's tag set is unsatisfactory for your needs, you can + overload it with your own list of tags to allow. If you change + this, you probably also want to change %HTML.AllowedAttributes; see + also %HTML.Allowed which lets you set allowed elements and + attributes at the same time. +

        +

        + If you attempt to allow an element that HTML Purifier does not know + about, HTML Purifier will raise an error. You will need to manually + tell HTML Purifier about this element by using the + advanced customization features. +

        +

        + Warning: If another directive conflicts with the + elements here, that directive will win and override. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt new file mode 100644 index 0000000..5a59a55 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt @@ -0,0 +1,20 @@ +HTML.AllowedModules +TYPE: lookup/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + A doctype comes with a set of usual modules to use. Without having + to mucking about with the doctypes, you can quickly activate or + disable these modules by specifying which modules you wish to allow + with this directive. This is most useful for unit testing specific + modules, although end users may find it useful for their own ends. +

        +

        + If you specify a module that does not exist, the manager will silently + fail to use it, so be careful! User-defined modules are not affected + by this directive. Modules defined in %HTML.CoreModules are not + affected by this directive. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt new file mode 100644 index 0000000..151fb7b --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt @@ -0,0 +1,11 @@ +HTML.Attr.Name.UseCDATA +TYPE: bool +DEFAULT: false +VERSION: 4.0.0 +--DESCRIPTION-- +The W3C specification DTD defines the name attribute to be CDATA, not ID, due +to limitations of DTD. In certain documents, this relaxed behavior is desired, +whether it is to specify duplicate names, or to specify names that would be +illegal IDs (for example, names that begin with a digit.) Set this configuration +directive to true to use the relaxed parsing rules. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt new file mode 100644 index 0000000..45ae469 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt @@ -0,0 +1,18 @@ +HTML.BlockWrapper +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'p' +--DESCRIPTION-- + +

        + String name of element to wrap inline elements that are inside a block + context. This only occurs in the children of blockquote in strict mode. +

        +

        + Example: by default value, + <blockquote>Foo</blockquote> would become + <blockquote><p>Foo</p></blockquote>. + The <p> tags can be replaced with whatever you desire, + as long as it is a block level element. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt new file mode 100644 index 0000000..5246188 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt @@ -0,0 +1,23 @@ +HTML.CoreModules +TYPE: lookup +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'Structure' => true, + 'Text' => true, + 'Hypertext' => true, + 'List' => true, + 'NonXMLCommonAttributes' => true, + 'XMLCommonAttributes' => true, + 'CommonAttributes' => true, +) +--DESCRIPTION-- + +

        + Certain modularized doctypes (XHTML, namely), have certain modules + that must be included for the doctype to be an conforming document + type: put those modules here. By default, XHTML's core modules + are used. You can set this to a blank array to disable core module + protection, but this is not recommended. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt new file mode 100644 index 0000000..a64e3d7 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt @@ -0,0 +1,9 @@ +HTML.CustomDoctype +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +A custom doctype for power-users who defined there own document +type. This directive only applies when %HTML.Doctype is blank. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt new file mode 100644 index 0000000..103db75 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt @@ -0,0 +1,33 @@ +HTML.DefinitionID +TYPE: string/null +DEFAULT: NULL +VERSION: 2.0.0 +--DESCRIPTION-- + +

        + Unique identifier for a custom-built HTML definition. If you edit + the raw version of the HTMLDefinition, introducing changes that the + configuration object does not reflect, you must specify this variable. + If you change your custom edits, you should change this directive, or + clear your cache. Example: +

        +
        +$config = HTMLPurifier_Config::createDefault();
        +$config->set('HTML', 'DefinitionID', '1');
        +$def = $config->getHTMLDefinition();
        +$def->addAttribute('a', 'tabindex', 'Number');
        +
        +

        + In the above example, the configuration is still at the defaults, but + using the advanced API, an extra attribute has been added. The + configuration object normally has no way of knowing that this change + has taken place, so it needs an extra directive: %HTML.DefinitionID. + If someone else attempts to use the default configuration, these two + pieces of code will not clobber each other in the cache, since one has + an extra directive attached to it. +

        +

        + You must specify a value to this directive to use the + advanced API features. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt new file mode 100644 index 0000000..229ae02 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt @@ -0,0 +1,16 @@ +HTML.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

        + Revision identifier for your custom definition specified in + %HTML.DefinitionID. This serves the same purpose: uniquely identifying + your custom definition, but this one does so in a chronological + context: revision 3 is more up-to-date then revision 2. Thus, when + this gets incremented, the cache handling is smart enough to clean + up any older revisions of your definition as well as flush the + cache. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt new file mode 100644 index 0000000..9dab497 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt @@ -0,0 +1,11 @@ +HTML.Doctype +TYPE: string/null +DEFAULT: NULL +--DESCRIPTION-- +Doctype to use during filtering. Technically speaking this is not actually +a doctype (as it does not identify a corresponding DTD), but we are using +this name for sake of simplicity. When non-blank, this will override any +older directives like %HTML.XHTML or %HTML.Strict. +--ALLOWED-- +'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1' +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt new file mode 100644 index 0000000..7878dc0 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt @@ -0,0 +1,11 @@ +HTML.FlashAllowFullScreen +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

        + Whether or not to permit embedded Flash content from + %HTML.SafeObject to expand to the full screen. Corresponds to + the allowFullScreen parameter. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt new file mode 100644 index 0000000..57358f9 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt @@ -0,0 +1,21 @@ +HTML.ForbiddenAttributes +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

        + While this directive is similar to %HTML.AllowedAttributes, for + forwards-compatibility with XML, this attribute has a different syntax. Instead of + tag.attr, use tag@attr. To disallow href + attributes in a tags, set this directive to + a@href. You can also disallow an attribute globally with + attr or *@attr (either syntax is fine; the latter + is provided for consistency with %HTML.AllowedAttributes). +

        +

        + Warning: This directive complements %HTML.ForbiddenElements, + accordingly, check + out that directive for a discussion of why you + should think twice before using this directive. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt new file mode 100644 index 0000000..93a53e1 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt @@ -0,0 +1,20 @@ +HTML.ForbiddenElements +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

        + This was, perhaps, the most requested feature ever in HTML + Purifier. Please don't abuse it! This is the logical inverse of + %HTML.AllowedElements, and it will override that directive, or any + other directive. +

        +

        + If possible, %HTML.Allowed is recommended over this directive, because it + can sometimes be difficult to tell whether or not you've forbidden all of + the behavior you would like to disallow. If you forbid img + with the expectation of preventing images on your site, you'll be in for + a nasty surprise when people start using the background-image + CSS property. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt new file mode 100644 index 0000000..e424c38 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt @@ -0,0 +1,14 @@ +HTML.MaxImgLength +TYPE: int/null +DEFAULT: 1200 +VERSION: 3.1.1 +--DESCRIPTION-- +

        + This directive controls the maximum number of pixels in the width and + height attributes in img tags. This is + in place to prevent imagecrash attacks, disable with null at your own risk. + This directive is similar to %CSS.MaxImgLength, and both should be + concurrently edited, although there are + subtle differences in the input format (the HTML max is an integer). +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt new file mode 100644 index 0000000..700b309 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt @@ -0,0 +1,7 @@ +HTML.Nofollow +TYPE: bool +VERSION: 4.3.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, nofollow rel attributes are added to all outgoing links. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt new file mode 100644 index 0000000..62e8e16 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt @@ -0,0 +1,12 @@ +HTML.Parent +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'div' +--DESCRIPTION-- + +

        + String name of element that HTML fragment passed to library will be + inserted in. An interesting variation would be using span as the + parent element, meaning that only inline tags would be allowed. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt new file mode 100644 index 0000000..dfb7204 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt @@ -0,0 +1,12 @@ +HTML.Proprietary +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

        + Whether or not to allow proprietary elements and attributes in your + documents, as per HTMLPurifier_HTMLModule_Proprietary. + Warning: This can cause your documents to stop + validating! +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt new file mode 100644 index 0000000..cdda09a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt @@ -0,0 +1,13 @@ +HTML.SafeEmbed +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

        + Whether or not to permit embed tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to embed tags. Embed is a proprietary + element and will cause your website to stop validating; you should + see if you can use %Output.FlashCompat with %HTML.SafeObject instead + first.

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt new file mode 100644 index 0000000..5eb6ec2 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt @@ -0,0 +1,13 @@ +HTML.SafeIframe +TYPE: bool +VERSION: 4.4.0 +DEFAULT: false +--DESCRIPTION-- +

        + Whether or not to permit iframe tags in untrusted documents. This + directive must be accompanied by a whitelist of permitted iframes, + such as %URI.SafeIframeRegexp, otherwise it will fatally error. + This directive has no effect on strict doctypes, as iframes are not + valid. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt new file mode 100644 index 0000000..ceb342e --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt @@ -0,0 +1,13 @@ +HTML.SafeObject +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

        + Whether or not to permit object tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to object tags. You should also enable + %Output.FlashCompat in order to generate Internet Explorer + compatibility code for your object tags. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt new file mode 100644 index 0000000..5ebc7a1 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt @@ -0,0 +1,10 @@ +HTML.SafeScripting +TYPE: lookup +VERSION: 4.5.0 +DEFAULT: array() +--DESCRIPTION-- +

        + Whether or not to permit script tags to external scripts in documents. + Inline scripting is not allowed, and the script must match an explicit whitelist. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt new file mode 100644 index 0000000..a8b1de5 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt @@ -0,0 +1,9 @@ +HTML.Strict +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not to use Transitional (loose) or Strict rulesets. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt new file mode 100644 index 0000000..587a167 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt @@ -0,0 +1,8 @@ +HTML.TargetBlank +TYPE: bool +VERSION: 4.4.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, target=blank attributes are added to all outgoing links. +(This includes links from an HTTPS version of a page to an HTTP version.) +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt new file mode 100644 index 0000000..b4c271b --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt @@ -0,0 +1,8 @@ +HTML.TidyAdd +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to add to the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt new file mode 100644 index 0000000..4186ccd --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt @@ -0,0 +1,24 @@ +HTML.TidyLevel +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'medium' +--DESCRIPTION-- + +

        General level of cleanliness the Tidy module should enforce. +There are four allowed values:

        +
        +
        none
        +
        No extra tidying should be done
        +
        light
        +
        Only fix elements that would be discarded otherwise due to + lack of support in doctype
        +
        medium
        +
        Enforce best practices
        +
        heavy
        +
        Transform all deprecated elements and attributes to standards + compliant equivalents
        +
        + +--ALLOWED-- +'none', 'light', 'medium', 'heavy' +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt new file mode 100644 index 0000000..996762b --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt @@ -0,0 +1,8 @@ +HTML.TidyRemove +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to remove from the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt new file mode 100644 index 0000000..1db9237 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt @@ -0,0 +1,9 @@ +HTML.Trusted +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user input is trusted or not. If the input is +trusted, a more expansive set of allowed tags and attributes will be used. +See also %CSS.Trusted. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt new file mode 100644 index 0000000..2a47e38 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt @@ -0,0 +1,11 @@ +HTML.XHTML +TYPE: bool +DEFAULT: true +VERSION: 1.1.0 +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. +--ALIASES-- +Core.XHTML +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt new file mode 100644 index 0000000..08921fd --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt @@ -0,0 +1,10 @@ +Output.CommentScriptContents +TYPE: bool +VERSION: 2.0.0 +DEFAULT: true +--DESCRIPTION-- +Determines whether or not HTML Purifier should attempt to fix up the +contents of script tags for legacy browsers with comments. +--ALIASES-- +Core.CommentScriptContents +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt new file mode 100644 index 0000000..d6f0d9f --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt @@ -0,0 +1,15 @@ +Output.FixInnerHTML +TYPE: bool +VERSION: 4.3.0 +DEFAULT: true +--DESCRIPTION-- +

        + If true, HTML Purifier will protect against Internet Explorer's + mishandling of the innerHTML attribute by appending + a space to any attribute that does not contain angled brackets, spaces + or quotes, but contains a backtick. This slightly changes the + semantics of any given attribute, so if this is unacceptable and + you do not use innerHTML on any of your pages, you can + turn this directive off. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt new file mode 100644 index 0000000..93398e8 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt @@ -0,0 +1,11 @@ +Output.FlashCompat +TYPE: bool +VERSION: 4.1.0 +DEFAULT: false +--DESCRIPTION-- +

        + If true, HTML Purifier will generate Internet Explorer compatibility + code for all object code. This is highly recommended if you enable + %HTML.SafeObject. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt new file mode 100644 index 0000000..79f8ad8 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt @@ -0,0 +1,13 @@ +Output.Newline +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +

        + Newline string to format final output with. If left null, HTML Purifier + will auto-detect the default newline type of the system and use that; + you can manually override it here. Remember, \r\n is Windows, \r + is Mac, and \n is Unix. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt new file mode 100644 index 0000000..232b023 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt @@ -0,0 +1,14 @@ +Output.SortAttr +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

        + If true, HTML Purifier will sort attributes by name before writing them back + to the document, converting a tag like: <el b="" a="" c="" /> + to <el a="" b="" c="" />. This is a workaround for + a bug in FCKeditor which causes it to swap attributes order, adding noise + to text diffs. If you're not seeing this bug, chances are, you don't need + this directive. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt new file mode 100644 index 0000000..06bab00 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt @@ -0,0 +1,25 @@ +Output.TidyFormat +TYPE: bool +VERSION: 1.1.1 +DEFAULT: false +--DESCRIPTION-- +

        + Determines whether or not to run Tidy on the final output for pretty + formatting reasons, such as indentation and wrap. +

        +

        + This can greatly improve readability for editors who are hand-editing + the HTML, but is by no means necessary as HTML Purifier has already + fixed all major errors the HTML may have had. Tidy is a non-default + extension, and this directive will silently fail if Tidy is not + available. +

        +

        + If you are looking to make the overall look of your page's source + better, I recommend running Tidy on the entire page rather than just + user-content (after all, the indentation relative to the containing + blocks will be incorrect). +

        +--ALIASES-- +Core.TidyFormat +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt new file mode 100644 index 0000000..071bc02 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt @@ -0,0 +1,7 @@ +Test.ForceNoIconv +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When set to true, HTMLPurifier_Encoder will act as if iconv does not exist +and use only pure PHP implementations. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt new file mode 100644 index 0000000..666635a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt @@ -0,0 +1,17 @@ +URI.AllowedSchemes +TYPE: lookup +--DEFAULT-- +array ( + 'http' => true, + 'https' => true, + 'mailto' => true, + 'ftp' => true, + 'nntp' => true, + 'news' => true, +) +--DESCRIPTION-- +Whitelist that defines the schemes that a URI is allowed to have. This +prevents XSS attacks from using pseudo-schemes like javascript or mocha. +There is also support for the data and file +URI schemes, but they are not enabled by default. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Base.txt new file mode 100644 index 0000000..876f068 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Base.txt @@ -0,0 +1,17 @@ +URI.Base +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + The base URI is the URI of the document this purified HTML will be + inserted into. This information is important if HTML Purifier needs + to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute + is on. You may use a non-absolute URI for this value, but behavior + may vary (%URI.MakeAbsolute deals nicely with both absolute and + relative paths, but forwards-compatibility is not guaranteed). + Warning: If set, the scheme on this URI + overrides the one specified by %URI.DefaultScheme. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt new file mode 100644 index 0000000..728e378 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt @@ -0,0 +1,10 @@ +URI.DefaultScheme +TYPE: string +DEFAULT: 'http' +--DESCRIPTION-- + +

        + Defines through what scheme the output will be served, in order to + select the proper object validator when no scheme information is present. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt new file mode 100644 index 0000000..f05312b --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt @@ -0,0 +1,11 @@ +URI.DefinitionID +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + Unique identifier for a custom-built URI definition. If you want + to add custom URIFilters, you must specify this value. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt new file mode 100644 index 0000000..80cfea9 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt @@ -0,0 +1,11 @@ +URI.DefinitionRev +TYPE: int +VERSION: 2.1.0 +DEFAULT: 1 +--DESCRIPTION-- + +

        + Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt new file mode 100644 index 0000000..71ce025 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt @@ -0,0 +1,14 @@ +URI.Disable +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- + +

        + Disables all URIs in all forms. Not sure why you'd want to do that + (after all, the Internet's founded on the notion of a hyperlink). +

        + +--ALIASES-- +Attr.DisableURI +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt new file mode 100644 index 0000000..13c122c --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt @@ -0,0 +1,11 @@ +URI.DisableExternal +TYPE: bool +VERSION: 1.2.0 +DEFAULT: false +--DESCRIPTION-- +Disables links to external websites. This is a highly effective anti-spam +and anti-pagerank-leech measure, but comes at a hefty price: nolinks or +images outside of your domain will be allowed. Non-linkified URIs will +still be preserved. If you want to be able to link to subdomains or use +absolute URIs, specify %URI.Host for your website. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt new file mode 100644 index 0000000..abcc1ef --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt @@ -0,0 +1,13 @@ +URI.DisableExternalResources +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- +Disables the embedding of external resources, preventing users from +embedding things like images from other hosts. This prevents access +tracking (good for email viewers), bandwidth leeching, cross-site request +forging, goatse.cx posting, and other nasties, but also results in a loss +of end-user functionality (they can't directly post a pic they posted from +Flickr anymore). Use it if you don't have a robust user-content moderation +team. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt new file mode 100644 index 0000000..f891de4 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt @@ -0,0 +1,15 @@ +URI.DisableResources +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

        + Disables embedding resources, essentially meaning no pictures. You can + still link to them though. See %URI.DisableExternalResources for why + this might be a good idea. +

        +

        + Note: While this directive has been available since 1.3.0, + it didn't actually start doing anything until 4.2.0. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Host.txt new file mode 100644 index 0000000..ee83b12 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Host.txt @@ -0,0 +1,19 @@ +URI.Host +TYPE: string/null +VERSION: 1.2.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + Defines the domain name of the server, so we can determine whether or + an absolute URI is from your website or not. Not strictly necessary, + as users should be using relative URIs to reference resources on your + website. It will, however, let you use absolute URIs to link to + subdomains of the domain you post here: i.e. example.com will allow + sub.example.com. However, higher up domains will still be excluded: + if you set %URI.Host to sub.example.com, example.com will be blocked. + Note: This directive overrides %URI.Base because + a given page may be on a sub-domain, but you wish HTML Purifier to be + more relaxed and allow some of the parent domains too. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt new file mode 100644 index 0000000..0b6df76 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt @@ -0,0 +1,9 @@ +URI.HostBlacklist +TYPE: list +VERSION: 1.3.0 +DEFAULT: array() +--DESCRIPTION-- +List of strings that are forbidden in the host of any URI. Use it to kill +domain names of spam, etc. Note that it will catch anything in the domain, +so moo.com will catch moo.com.example.com. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt new file mode 100644 index 0000000..4214900 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt @@ -0,0 +1,13 @@ +URI.MakeAbsolute +TYPE: bool +VERSION: 2.1.0 +DEFAULT: false +--DESCRIPTION-- + +

        + Converts all URIs into absolute forms. This is useful when the HTML + being filtered assumes a specific base path, but will actually be + viewed in a different context (and setting an alternate base URI is + not possible). %URI.Base must be set for this directive to work. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt new file mode 100644 index 0000000..58c81dc --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -0,0 +1,83 @@ +URI.Munge +TYPE: string/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

        + Munges all browsable (usually http, https and ftp) + absolute URIs into another URI, usually a URI redirection service. + This directive accepts a URI, formatted with a %s where + the url-encoded original URI should be inserted (sample: + http://www.google.com/url?q=%s). +

        +

        + Uses for this directive: +

        +
          +
        • + Prevent PageRank leaks, while being fairly transparent + to users (you may also want to add some client side JavaScript to + override the text in the statusbar). Notice: + Many security experts believe that this form of protection does not deter spam-bots. +
        • +
        • + Redirect users to a splash page telling them they are leaving your + website. While this is poor usability practice, it is often mandated + in corporate environments. +
        • +
        +

        + Prior to HTML Purifier 3.1.1, this directive also enabled the munging + of browsable external resources, which could break things if your redirection + script was a splash page or used meta tags. To revert to + previous behavior, please use %URI.MungeResources. +

        +

        + You may want to also use %URI.MungeSecretKey along with this directive + in order to enforce what URIs your redirector script allows. Open + redirector scripts can be a security risk and negatively affect the + reputation of your domain name. +

        +

        + Starting with HTML Purifier 3.1.1, there is also these substitutions: +

        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        KeyDescriptionExample <a href="">
        %r1 - The URI embeds a resource
        (blank) - The URI is merely a link
        %nThe name of the tag this URI came froma
        %mThe name of the attribute this URI came fromhref
        %pThe name of the CSS property this URI came from, or blank if irrelevant
        +

        + Admittedly, these letters are somewhat arbitrary; the only stipulation + was that they couldn't be a through f. r is for resource (I would have preferred + e, but you take what you can get), n is for name, m + was picked because it came after n (and I couldn't use a), p is for + property. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt new file mode 100644 index 0000000..6fce0fd --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt @@ -0,0 +1,17 @@ +URI.MungeResources +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

        + If true, any URI munging directives like %URI.Munge + will also apply to embedded resources, such as <img src="">. + Be careful enabling this directive if you have a redirector script + that does not use the Location HTTP header; all of your images + and other embedded resources will break. +

        +

        + Warning: It is strongly advised you use this in conjunction + %URI.MungeSecretKey to mitigate the security risk of an open redirector. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt new file mode 100644 index 0000000..1e17c1d --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt @@ -0,0 +1,30 @@ +URI.MungeSecretKey +TYPE: string/null +VERSION: 3.1.1 +DEFAULT: NULL +--DESCRIPTION-- +

        + This directive enables secure checksum generation along with %URI.Munge. + It should be set to a secure key that is not shared with anyone else. + The checksum can be placed in the URI using %t. Use of this checksum + affords an additional level of protection by allowing a redirector + to check if a URI has passed through HTML Purifier with this line: +

        + +
        $checksum === hash_hmac("sha256", $url, $secret_key)
        + +

        + If the output is TRUE, the redirector script should accept the URI. +

        + +

        + Please note that it would still be possible for an attacker to procure + secure hashes en-mass by abusing your website's Preview feature or the + like, but this service affords an additional level of protection + that should be combined with website blacklisting. +

        + +

        + Remember this has no effect if %URI.Munge is not on. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt new file mode 100644 index 0000000..23331a4 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt @@ -0,0 +1,9 @@ +URI.OverrideAllowedSchemes +TYPE: bool +DEFAULT: true +--DESCRIPTION-- +If this is set to true (which it is by default), you can override +%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the +registry. If false, you will also have to update that directive in order +to add more schemes. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt new file mode 100644 index 0000000..7908483 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt @@ -0,0 +1,22 @@ +URI.SafeIframeRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +

        + A PCRE regular expression that will be matched against an iframe URI. This is + a relatively inflexible scheme, but works well enough for the most common + use-case of iframes: embedded video. This directive only has an effect if + %HTML.SafeIframe is enabled. Here are some example values: +

        +
          +
        • %^http://www.youtube.com/embed/% - Allow YouTube videos
        • +
        • %^http://player.vimeo.com/video/% - Allow Vimeo videos
        • +
        • %^http://(www.youtube.com/embed/|player.vimeo.com/video/)% - Allow both
        • +
        +

        + Note that this directive does not give you enough granularity to, say, disable + all autoplay videos. Pipe up on the HTML Purifier forums if this + is a capability you want. +

        +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/info.ini b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/info.ini new file mode 100644 index 0000000..5de4505 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/ConfigSchema/schema/info.ini @@ -0,0 +1,3 @@ +name = "HTML Purifier" + +; vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/gitignore b/lib/htmlpurifier/standalone/HTMLPurifier/DefinitionCache/Serializer/gitignore new file mode 100644 index 0000000..e69de29 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/EntityLookup/entities.ser b/lib/htmlpurifier/standalone/HTMLPurifier/EntityLookup/entities.ser new file mode 100644 index 0000000..e8b0812 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/EntityLookup/entities.ser @@ -0,0 +1 @@ +a:253:{s:4:"fnof";s:2:"ƒ";s:5:"Alpha";s:2:"Α";s:4:"Beta";s:2:"Β";s:5:"Gamma";s:2:"Γ";s:5:"Delta";s:2:"Δ";s:7:"Epsilon";s:2:"Ε";s:4:"Zeta";s:2:"Ζ";s:3:"Eta";s:2:"Η";s:5:"Theta";s:2:"Θ";s:4:"Iota";s:2:"Ι";s:5:"Kappa";s:2:"Κ";s:6:"Lambda";s:2:"Λ";s:2:"Mu";s:2:"Μ";s:2:"Nu";s:2:"Ν";s:2:"Xi";s:2:"Ξ";s:7:"Omicron";s:2:"Ο";s:2:"Pi";s:2:"Π";s:3:"Rho";s:2:"Ρ";s:5:"Sigma";s:2:"Σ";s:3:"Tau";s:2:"Τ";s:7:"Upsilon";s:2:"Υ";s:3:"Phi";s:2:"Φ";s:3:"Chi";s:2:"Χ";s:3:"Psi";s:2:"Ψ";s:5:"Omega";s:2:"Ω";s:5:"alpha";s:2:"α";s:4:"beta";s:2:"β";s:5:"gamma";s:2:"γ";s:5:"delta";s:2:"δ";s:7:"epsilon";s:2:"ε";s:4:"zeta";s:2:"ζ";s:3:"eta";s:2:"η";s:5:"theta";s:2:"θ";s:4:"iota";s:2:"ι";s:5:"kappa";s:2:"κ";s:6:"lambda";s:2:"λ";s:2:"mu";s:2:"μ";s:2:"nu";s:2:"ν";s:2:"xi";s:2:"ξ";s:7:"omicron";s:2:"ο";s:2:"pi";s:2:"π";s:3:"rho";s:2:"ρ";s:6:"sigmaf";s:2:"ς";s:5:"sigma";s:2:"σ";s:3:"tau";s:2:"τ";s:7:"upsilon";s:2:"υ";s:3:"phi";s:2:"φ";s:3:"chi";s:2:"χ";s:3:"psi";s:2:"ψ";s:5:"omega";s:2:"ω";s:8:"thetasym";s:2:"ϑ";s:5:"upsih";s:2:"ϒ";s:3:"piv";s:2:"ϖ";s:4:"bull";s:3:"•";s:6:"hellip";s:3:"…";s:5:"prime";s:3:"′";s:5:"Prime";s:3:"″";s:5:"oline";s:3:"‾";s:5:"frasl";s:3:"⁄";s:6:"weierp";s:3:"℘";s:5:"image";s:3:"ℑ";s:4:"real";s:3:"ℜ";s:5:"trade";s:3:"™";s:7:"alefsym";s:3:"ℵ";s:4:"larr";s:3:"←";s:4:"uarr";s:3:"↑";s:4:"rarr";s:3:"→";s:4:"darr";s:3:"↓";s:4:"harr";s:3:"↔";s:5:"crarr";s:3:"↵";s:4:"lArr";s:3:"⇐";s:4:"uArr";s:3:"⇑";s:4:"rArr";s:3:"⇒";s:4:"dArr";s:3:"⇓";s:4:"hArr";s:3:"⇔";s:6:"forall";s:3:"∀";s:4:"part";s:3:"∂";s:5:"exist";s:3:"∃";s:5:"empty";s:3:"∅";s:5:"nabla";s:3:"∇";s:4:"isin";s:3:"∈";s:5:"notin";s:3:"∉";s:2:"ni";s:3:"∋";s:4:"prod";s:3:"∏";s:3:"sum";s:3:"∑";s:5:"minus";s:3:"−";s:6:"lowast";s:3:"∗";s:5:"radic";s:3:"√";s:4:"prop";s:3:"∝";s:5:"infin";s:3:"∞";s:3:"ang";s:3:"∠";s:3:"and";s:3:"∧";s:2:"or";s:3:"∨";s:3:"cap";s:3:"∩";s:3:"cup";s:3:"∪";s:3:"int";s:3:"∫";s:6:"there4";s:3:"∴";s:3:"sim";s:3:"∼";s:4:"cong";s:3:"≅";s:5:"asymp";s:3:"≈";s:2:"ne";s:3:"≠";s:5:"equiv";s:3:"≡";s:2:"le";s:3:"≤";s:2:"ge";s:3:"≥";s:3:"sub";s:3:"⊂";s:3:"sup";s:3:"⊃";s:4:"nsub";s:3:"⊄";s:4:"sube";s:3:"⊆";s:4:"supe";s:3:"⊇";s:5:"oplus";s:3:"⊕";s:6:"otimes";s:3:"⊗";s:4:"perp";s:3:"⊥";s:4:"sdot";s:3:"⋅";s:5:"lceil";s:3:"⌈";s:5:"rceil";s:3:"⌉";s:6:"lfloor";s:3:"⌊";s:6:"rfloor";s:3:"⌋";s:4:"lang";s:3:"〈";s:4:"rang";s:3:"〉";s:3:"loz";s:3:"◊";s:6:"spades";s:3:"♠";s:5:"clubs";s:3:"♣";s:6:"hearts";s:3:"♥";s:5:"diams";s:3:"♦";s:4:"quot";s:1:""";s:3:"amp";s:1:"&";s:2:"lt";s:1:"<";s:2:"gt";s:1:">";s:4:"apos";s:1:"'";s:5:"OElig";s:2:"Œ";s:5:"oelig";s:2:"œ";s:6:"Scaron";s:2:"Š";s:6:"scaron";s:2:"š";s:4:"Yuml";s:2:"Ÿ";s:4:"circ";s:2:"ˆ";s:5:"tilde";s:2:"˜";s:4:"ensp";s:3:" ";s:4:"emsp";s:3:" ";s:6:"thinsp";s:3:" ";s:4:"zwnj";s:3:"‌";s:3:"zwj";s:3:"‍";s:3:"lrm";s:3:"‎";s:3:"rlm";s:3:"‏";s:5:"ndash";s:3:"–";s:5:"mdash";s:3:"—";s:5:"lsquo";s:3:"‘";s:5:"rsquo";s:3:"’";s:5:"sbquo";s:3:"‚";s:5:"ldquo";s:3:"“";s:5:"rdquo";s:3:"”";s:5:"bdquo";s:3:"„";s:6:"dagger";s:3:"†";s:6:"Dagger";s:3:"‡";s:6:"permil";s:3:"‰";s:6:"lsaquo";s:3:"‹";s:6:"rsaquo";s:3:"›";s:4:"euro";s:3:"€";s:4:"nbsp";s:2:" ";s:5:"iexcl";s:2:"¡";s:4:"cent";s:2:"¢";s:5:"pound";s:2:"£";s:6:"curren";s:2:"¤";s:3:"yen";s:2:"¥";s:6:"brvbar";s:2:"¦";s:4:"sect";s:2:"§";s:3:"uml";s:2:"¨";s:4:"copy";s:2:"©";s:4:"ordf";s:2:"ª";s:5:"laquo";s:2:"«";s:3:"not";s:2:"¬";s:3:"shy";s:2:"­";s:3:"reg";s:2:"®";s:4:"macr";s:2:"¯";s:3:"deg";s:2:"°";s:6:"plusmn";s:2:"±";s:4:"sup2";s:2:"²";s:4:"sup3";s:2:"³";s:5:"acute";s:2:"´";s:5:"micro";s:2:"µ";s:4:"para";s:2:"¶";s:6:"middot";s:2:"·";s:5:"cedil";s:2:"¸";s:4:"sup1";s:2:"¹";s:4:"ordm";s:2:"º";s:5:"raquo";s:2:"»";s:6:"frac14";s:2:"¼";s:6:"frac12";s:2:"½";s:6:"frac34";s:2:"¾";s:6:"iquest";s:2:"¿";s:6:"Agrave";s:2:"À";s:6:"Aacute";s:2:"Á";s:5:"Acirc";s:2:"Â";s:6:"Atilde";s:2:"Ã";s:4:"Auml";s:2:"Ä";s:5:"Aring";s:2:"Å";s:5:"AElig";s:2:"Æ";s:6:"Ccedil";s:2:"Ç";s:6:"Egrave";s:2:"È";s:6:"Eacute";s:2:"É";s:5:"Ecirc";s:2:"Ê";s:4:"Euml";s:2:"Ë";s:6:"Igrave";s:2:"Ì";s:6:"Iacute";s:2:"Í";s:5:"Icirc";s:2:"Î";s:4:"Iuml";s:2:"Ï";s:3:"ETH";s:2:"Ð";s:6:"Ntilde";s:2:"Ñ";s:6:"Ograve";s:2:"Ò";s:6:"Oacute";s:2:"Ó";s:5:"Ocirc";s:2:"Ô";s:6:"Otilde";s:2:"Õ";s:4:"Ouml";s:2:"Ö";s:5:"times";s:2:"×";s:6:"Oslash";s:2:"Ø";s:6:"Ugrave";s:2:"Ù";s:6:"Uacute";s:2:"Ú";s:5:"Ucirc";s:2:"Û";s:4:"Uuml";s:2:"Ü";s:6:"Yacute";s:2:"Ý";s:5:"THORN";s:2:"Þ";s:5:"szlig";s:2:"ß";s:6:"agrave";s:2:"à";s:6:"aacute";s:2:"á";s:5:"acirc";s:2:"â";s:6:"atilde";s:2:"ã";s:4:"auml";s:2:"ä";s:5:"aring";s:2:"å";s:5:"aelig";s:2:"æ";s:6:"ccedil";s:2:"ç";s:6:"egrave";s:2:"è";s:6:"eacute";s:2:"é";s:5:"ecirc";s:2:"ê";s:4:"euml";s:2:"ë";s:6:"igrave";s:2:"ì";s:6:"iacute";s:2:"í";s:5:"icirc";s:2:"î";s:4:"iuml";s:2:"ï";s:3:"eth";s:2:"ð";s:6:"ntilde";s:2:"ñ";s:6:"ograve";s:2:"ò";s:6:"oacute";s:2:"ó";s:5:"ocirc";s:2:"ô";s:6:"otilde";s:2:"õ";s:4:"ouml";s:2:"ö";s:6:"divide";s:2:"÷";s:6:"oslash";s:2:"ø";s:6:"ugrave";s:2:"ù";s:6:"uacute";s:2:"ú";s:5:"ucirc";s:2:"û";s:4:"uuml";s:2:"ü";s:6:"yacute";s:2:"ý";s:5:"thorn";s:2:"þ";s:4:"yuml";s:2:"ÿ";} \ No newline at end of file diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Filter/ExtractStyleBlocks.php b/lib/htmlpurifier/standalone/HTMLPurifier/Filter/ExtractStyleBlocks.php new file mode 100644 index 0000000..08e62c1 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -0,0 +1,338 @@ + blocks from input HTML, cleans them up + * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') + * so they can be used elsewhere in the document. + * + * @note + * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for + * sample usage. + * + * @note + * This filter can also be used on stylesheets not included in the + * document--something purists would probably prefer. Just directly + * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() + */ +class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter +{ + /** + * @type string + */ + public $name = 'ExtractStyleBlocks'; + + /** + * @type array + */ + private $_styleMatches = array(); + + /** + * @type csstidy + */ + private $_tidy; + + /** + * @type HTMLPurifier_AttrDef_HTML_ID + */ + private $_id_attrdef; + + /** + * @type HTMLPurifier_AttrDef_CSS_Ident + */ + private $_class_attrdef; + + /** + * @type HTMLPurifier_AttrDef_Enum + */ + private $_enum_attrdef; + + public function __construct() + { + $this->_tidy = new csstidy(); + $this->_tidy->set_cfg('lowercase_s', false); + $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); + $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); + $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'first-child', + 'link', + 'visited', + 'active', + 'hover', + 'focus' + ) + ); + } + + /** + * Save the contents of CSS blocks to style matches + * @param array $matches preg_replace style $matches array + */ + protected function styleCallback($matches) + { + $this->_styleMatches[] = $matches[1]; + } + + /** + * Removes inline #isU', array($this, 'styleCallback'), $html); + $style_blocks = $this->_styleMatches; + $this->_styleMatches = array(); // reset + $context->register('StyleBlocks', $style_blocks); // $context must not be reused + if ($this->_tidy) { + foreach ($style_blocks as &$style) { + $style = $this->cleanCSS($style, $config, $context); + } + } + return $html; + } + + /** + * Takes CSS (the stuff found in in a font-family prop). + if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { + $css = str_replace( + array('<', '>', '&'), + array('\3C ', '\3E ', '\26 '), + $css + ); + } + return $css; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Filter/YouTube.php b/lib/htmlpurifier/standalone/HTMLPurifier/Filter/YouTube.php new file mode 100644 index 0000000..411519a --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Filter/YouTube.php @@ -0,0 +1,65 @@ +]+>.+?' . + 'http://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; + $pre_replace = '\1'; + return preg_replace($pre_regex, $pre_replace, $html); + } + + /** + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; + return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); + } + + /** + * @param $url + * @return string + */ + protected function armorUrl($url) + { + return str_replace('--', '--', $url); + } + + /** + * @param array $matches + * @return string + */ + protected function postFilterCallback($matches) + { + $url = $this->armorUrl($matches[1]); + return '' . + '' . + '' . + ''; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Language/classes/en-x-test.php b/lib/htmlpurifier/standalone/HTMLPurifier/Language/classes/en-x-test.php new file mode 100644 index 0000000..8828f5c --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Language/classes/en-x-test.php @@ -0,0 +1,9 @@ + 'HTML Purifier X' +); + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-testmini.php b/lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-testmini.php new file mode 100644 index 0000000..806c83f --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en-x-testmini.php @@ -0,0 +1,12 @@ + 'HTML Purifier XNone' +); + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en.php b/lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en.php new file mode 100644 index 0000000..c7f197e --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Language/messages/en.php @@ -0,0 +1,55 @@ + 'HTML Purifier', +// for unit testing purposes + 'LanguageFactoryTest: Pizza' => 'Pizza', + 'LanguageTest: List' => '$1', + 'LanguageTest: Hash' => '$1.Keys; $1.Values', + 'Item separator' => ', ', + 'Item separator last' => ' and ', // non-Harvard style + + 'ErrorCollector: No errors' => 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.', + 'ErrorCollector: At line' => ' at line $line', + 'ErrorCollector: Incidental errors' => 'Incidental errors', + 'Lexer: Unclosed comment' => 'Unclosed comment', + 'Lexer: Unescaped lt' => 'Unescaped less-than sign (<) should be <', + 'Lexer: Missing gt' => 'Missing greater-than sign (>), previous less-than sign (<) should be escaped', + 'Lexer: Missing attribute key' => 'Attribute declaration has no key', + 'Lexer: Missing end quote' => 'Attribute declaration has no end quote', + 'Lexer: Extracted body' => 'Removed document metadata tags', + 'Strategy_RemoveForeignElements: Tag transform' => '<$1> element transformed into $CurrentToken.Serialized', + 'Strategy_RemoveForeignElements: Missing required attribute' => '$CurrentToken.Compact element missing required attribute $1', + 'Strategy_RemoveForeignElements: Foreign element to text' => 'Unrecognized $CurrentToken.Serialized tag converted to text', + 'Strategy_RemoveForeignElements: Foreign element removed' => 'Unrecognized $CurrentToken.Serialized tag removed', + 'Strategy_RemoveForeignElements: Comment removed' => 'Comment containing "$CurrentToken.Data" removed', + 'Strategy_RemoveForeignElements: Foreign meta element removed' => 'Unrecognized $CurrentToken.Serialized meta tag and all descendants removed', + 'Strategy_RemoveForeignElements: Token removed to end' => 'Tags and text starting from $1 element where removed to end', + 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' => 'Trailing hyphen(s) in comment removed', + 'Strategy_RemoveForeignElements: Hyphens in comment collapsed' => 'Double hyphens in comments are not allowed, and were collapsed into single hyphens', + 'Strategy_MakeWellFormed: Unnecessary end tag removed' => 'Unnecessary $CurrentToken.Serialized tag removed', + 'Strategy_MakeWellFormed: Unnecessary end tag to text' => 'Unnecessary $CurrentToken.Serialized tag converted to text', + 'Strategy_MakeWellFormed: Tag auto closed' => '$1.Compact started on line $1.Line auto-closed by $CurrentToken.Compact', + 'Strategy_MakeWellFormed: Tag carryover' => '$1.Compact started on line $1.Line auto-continued into $CurrentToken.Compact', + 'Strategy_MakeWellFormed: Stray end tag removed' => 'Stray $CurrentToken.Serialized tag removed', + 'Strategy_MakeWellFormed: Stray end tag to text' => 'Stray $CurrentToken.Serialized tag converted to text', + 'Strategy_MakeWellFormed: Tag closed by element end' => '$1.Compact tag started on line $1.Line closed by end of $CurrentToken.Serialized', + 'Strategy_MakeWellFormed: Tag closed by document end' => '$1.Compact tag started on line $1.Line closed by end of document', + 'Strategy_FixNesting: Node removed' => '$CurrentToken.Compact node removed', + 'Strategy_FixNesting: Node excluded' => '$CurrentToken.Compact node removed due to descendant exclusion by ancestor element', + 'Strategy_FixNesting: Node reorganized' => 'Contents of $CurrentToken.Compact node reorganized to enforce its content model', + 'Strategy_FixNesting: Node contents removed' => 'Contents of $CurrentToken.Compact node removed', + 'AttrValidator: Attributes transformed' => 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys', + 'AttrValidator: Attribute removed' => '$CurrentAttr.Name attribute on $CurrentToken.Compact removed', +); + +$errorNames = array( + E_ERROR => 'Error', + E_WARNING => 'Warning', + E_NOTICE => 'Notice' +); + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Lexer/PH5P.php b/lib/htmlpurifier/standalone/HTMLPurifier/Lexer/PH5P.php new file mode 100644 index 0000000..a4587e4 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Lexer/PH5P.php @@ -0,0 +1,4788 @@ +normalize($html, $config, $context); + $new_html = $this->wrapHTML($new_html, $config, $context); + try { + $parser = new HTML5($new_html); + $doc = $parser->save(); + } catch (DOMException $e) { + // Uh oh, it failed. Punt to DirectLex. + $lexer = new HTMLPurifier_Lexer_DirectLex(); + $context->register('PH5PError', $e); // save the error, so we can detect it + return $lexer->tokenizeHTML($html, $config, $context); // use original HTML + } + $tokens = array(); + $this->tokenizeDOM( + $doc->getElementsByTagName('html')->item(0)-> // + getElementsByTagName('body')->item(0)-> // + getElementsByTagName('div')->item(0) //
        + , + $tokens + ); + return $tokens; + } +} + +/* + +Copyright 2007 Jeroen van der Meer + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +class HTML5 +{ + private $data; + private $char; + private $EOF; + private $state; + private $tree; + private $token; + private $content_model; + private $escape = false; + private $entities = array( + 'AElig;', + 'AElig', + 'AMP;', + 'AMP', + 'Aacute;', + 'Aacute', + 'Acirc;', + 'Acirc', + 'Agrave;', + 'Agrave', + 'Alpha;', + 'Aring;', + 'Aring', + 'Atilde;', + 'Atilde', + 'Auml;', + 'Auml', + 'Beta;', + 'COPY;', + 'COPY', + 'Ccedil;', + 'Ccedil', + 'Chi;', + 'Dagger;', + 'Delta;', + 'ETH;', + 'ETH', + 'Eacute;', + 'Eacute', + 'Ecirc;', + 'Ecirc', + 'Egrave;', + 'Egrave', + 'Epsilon;', + 'Eta;', + 'Euml;', + 'Euml', + 'GT;', + 'GT', + 'Gamma;', + 'Iacute;', + 'Iacute', + 'Icirc;', + 'Icirc', + 'Igrave;', + 'Igrave', + 'Iota;', + 'Iuml;', + 'Iuml', + 'Kappa;', + 'LT;', + 'LT', + 'Lambda;', + 'Mu;', + 'Ntilde;', + 'Ntilde', + 'Nu;', + 'OElig;', + 'Oacute;', + 'Oacute', + 'Ocirc;', + 'Ocirc', + 'Ograve;', + 'Ograve', + 'Omega;', + 'Omicron;', + 'Oslash;', + 'Oslash', + 'Otilde;', + 'Otilde', + 'Ouml;', + 'Ouml', + 'Phi;', + 'Pi;', + 'Prime;', + 'Psi;', + 'QUOT;', + 'QUOT', + 'REG;', + 'REG', + 'Rho;', + 'Scaron;', + 'Sigma;', + 'THORN;', + 'THORN', + 'TRADE;', + 'Tau;', + 'Theta;', + 'Uacute;', + 'Uacute', + 'Ucirc;', + 'Ucirc', + 'Ugrave;', + 'Ugrave', + 'Upsilon;', + 'Uuml;', + 'Uuml', + 'Xi;', + 'Yacute;', + 'Yacute', + 'Yuml;', + 'Zeta;', + 'aacute;', + 'aacute', + 'acirc;', + 'acirc', + 'acute;', + 'acute', + 'aelig;', + 'aelig', + 'agrave;', + 'agrave', + 'alefsym;', + 'alpha;', + 'amp;', + 'amp', + 'and;', + 'ang;', + 'apos;', + 'aring;', + 'aring', + 'asymp;', + 'atilde;', + 'atilde', + 'auml;', + 'auml', + 'bdquo;', + 'beta;', + 'brvbar;', + 'brvbar', + 'bull;', + 'cap;', + 'ccedil;', + 'ccedil', + 'cedil;', + 'cedil', + 'cent;', + 'cent', + 'chi;', + 'circ;', + 'clubs;', + 'cong;', + 'copy;', + 'copy', + 'crarr;', + 'cup;', + 'curren;', + 'curren', + 'dArr;', + 'dagger;', + 'darr;', + 'deg;', + 'deg', + 'delta;', + 'diams;', + 'divide;', + 'divide', + 'eacute;', + 'eacute', + 'ecirc;', + 'ecirc', + 'egrave;', + 'egrave', + 'empty;', + 'emsp;', + 'ensp;', + 'epsilon;', + 'equiv;', + 'eta;', + 'eth;', + 'eth', + 'euml;', + 'euml', + 'euro;', + 'exist;', + 'fnof;', + 'forall;', + 'frac12;', + 'frac12', + 'frac14;', + 'frac14', + 'frac34;', + 'frac34', + 'frasl;', + 'gamma;', + 'ge;', + 'gt;', + 'gt', + 'hArr;', + 'harr;', + 'hearts;', + 'hellip;', + 'iacute;', + 'iacute', + 'icirc;', + 'icirc', + 'iexcl;', + 'iexcl', + 'igrave;', + 'igrave', + 'image;', + 'infin;', + 'int;', + 'iota;', + 'iquest;', + 'iquest', + 'isin;', + 'iuml;', + 'iuml', + 'kappa;', + 'lArr;', + 'lambda;', + 'lang;', + 'laquo;', + 'laquo', + 'larr;', + 'lceil;', + 'ldquo;', + 'le;', + 'lfloor;', + 'lowast;', + 'loz;', + 'lrm;', + 'lsaquo;', + 'lsquo;', + 'lt;', + 'lt', + 'macr;', + 'macr', + 'mdash;', + 'micro;', + 'micro', + 'middot;', + 'middot', + 'minus;', + 'mu;', + 'nabla;', + 'nbsp;', + 'nbsp', + 'ndash;', + 'ne;', + 'ni;', + 'not;', + 'not', + 'notin;', + 'nsub;', + 'ntilde;', + 'ntilde', + 'nu;', + 'oacute;', + 'oacute', + 'ocirc;', + 'ocirc', + 'oelig;', + 'ograve;', + 'ograve', + 'oline;', + 'omega;', + 'omicron;', + 'oplus;', + 'or;', + 'ordf;', + 'ordf', + 'ordm;', + 'ordm', + 'oslash;', + 'oslash', + 'otilde;', + 'otilde', + 'otimes;', + 'ouml;', + 'ouml', + 'para;', + 'para', + 'part;', + 'permil;', + 'perp;', + 'phi;', + 'pi;', + 'piv;', + 'plusmn;', + 'plusmn', + 'pound;', + 'pound', + 'prime;', + 'prod;', + 'prop;', + 'psi;', + 'quot;', + 'quot', + 'rArr;', + 'radic;', + 'rang;', + 'raquo;', + 'raquo', + 'rarr;', + 'rceil;', + 'rdquo;', + 'real;', + 'reg;', + 'reg', + 'rfloor;', + 'rho;', + 'rlm;', + 'rsaquo;', + 'rsquo;', + 'sbquo;', + 'scaron;', + 'sdot;', + 'sect;', + 'sect', + 'shy;', + 'shy', + 'sigma;', + 'sigmaf;', + 'sim;', + 'spades;', + 'sub;', + 'sube;', + 'sum;', + 'sup1;', + 'sup1', + 'sup2;', + 'sup2', + 'sup3;', + 'sup3', + 'sup;', + 'supe;', + 'szlig;', + 'szlig', + 'tau;', + 'there4;', + 'theta;', + 'thetasym;', + 'thinsp;', + 'thorn;', + 'thorn', + 'tilde;', + 'times;', + 'times', + 'trade;', + 'uArr;', + 'uacute;', + 'uacute', + 'uarr;', + 'ucirc;', + 'ucirc', + 'ugrave;', + 'ugrave', + 'uml;', + 'uml', + 'upsih;', + 'upsilon;', + 'uuml;', + 'uuml', + 'weierp;', + 'xi;', + 'yacute;', + 'yacute', + 'yen;', + 'yen', + 'yuml;', + 'yuml', + 'zeta;', + 'zwj;', + 'zwnj;' + ); + + const PCDATA = 0; + const RCDATA = 1; + const CDATA = 2; + const PLAINTEXT = 3; + + const DOCTYPE = 0; + const STARTTAG = 1; + const ENDTAG = 2; + const COMMENT = 3; + const CHARACTR = 4; + const EOF = 5; + + public function __construct($data) + { + $this->data = $data; + $this->char = -1; + $this->EOF = strlen($data); + $this->tree = new HTML5TreeConstructer; + $this->content_model = self::PCDATA; + + $this->state = 'data'; + + while ($this->state !== null) { + $this->{$this->state . 'State'}(); + } + } + + public function save() + { + return $this->tree->save(); + } + + private function char() + { + return ($this->char < $this->EOF) + ? $this->data[$this->char] + : false; + } + + private function character($s, $l = 0) + { + if ($s + $l < $this->EOF) { + if ($l === 0) { + return $this->data[$s]; + } else { + return substr($this->data, $s, $l); + } + } + } + + private function characters($char_class, $start) + { + return preg_replace('#^([' . $char_class . ']+).*#s', '\\1', substr($this->data, $start)); + } + + private function dataState() + { + // Consume the next input character + $this->char++; + $char = $this->char(); + + if ($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { + /* U+0026 AMPERSAND (&) + When the content model flag is set to one of the PCDATA or RCDATA + states: switch to the entity data state. Otherwise: treat it as per + the "anything else" entry below. */ + $this->state = 'entityData'; + + } elseif ($char === '-') { + /* If the content model flag is set to either the RCDATA state or + the CDATA state, and the escape flag is false, and there are at + least three characters before this one in the input stream, and the + last four characters in the input stream, including this one, are + U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, + and U+002D HYPHEN-MINUS (""), + set the escape flag to false. */ + if (($this->content_model === self::RCDATA || + $this->content_model === self::CDATA) && $this->escape === true && + $this->character($this->char, 3) === '-->' + ) { + $this->escape = false; + } + + /* In any case, emit the input character as a character token. + Stay in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + } elseif ($this->char === $this->EOF) { + /* EOF + Emit an end-of-file token. */ + $this->EOF(); + + } elseif ($this->content_model === self::PLAINTEXT) { + /* When the content model flag is set to the PLAINTEXT state + THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of + the text and emit it as a character token. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => substr($this->data, $this->char) + ) + ); + + $this->EOF(); + + } else { + /* Anything else + THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that + otherwise would also be treated as a character token and emit it + as a single character token. Stay in the data state. */ + $len = strcspn($this->data, '<&', $this->char); + $char = substr($this->data, $this->char, $len); + $this->char += $len - 1; + + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + $this->state = 'data'; + } + } + + private function entityDataState() + { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, emit a U+0026 AMPERSAND character token. + // Otherwise, emit the character token that was returned. + $char = (!$entity) ? '&' : $entity; + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + // Finally, switch to the data state. + $this->state = 'data'; + } + + private function tagOpenState() + { + switch ($this->content_model) { + case self::RCDATA: + case self::CDATA: + /* If the next input character is a U+002F SOLIDUS (/) character, + consume it and switch to the close tag open state. If the next + input character is not a U+002F SOLIDUS (/) character, emit a + U+003C LESS-THAN SIGN character token and switch to the data + state to process the next input character. */ + if ($this->character($this->char + 1) === '/') { + $this->char++; + $this->state = 'closeTagOpen'; + + } else { + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<' + ) + ); + + $this->state = 'data'; + } + break; + + case self::PCDATA: + // If the content model flag is set to the PCDATA state + // Consume the next input character: + $this->char++; + $char = $this->char(); + + if ($char === '!') { + /* U+0021 EXCLAMATION MARK (!) + Switch to the markup declaration open state. */ + $this->state = 'markupDeclarationOpen'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the close tag open state. */ + $this->state = 'closeTagOpen'; + + } elseif (preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new start tag token, set its tag name to the lowercase + version of the input character (add 0x0020 to the character's code + point), then switch to the tag name state. (Don't emit the token + yet; further details will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::STARTTAG, + 'attr' => array() + ); + + $this->state = 'tagName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit a U+003C LESS-THAN SIGN character token and a + U+003E GREATER-THAN SIGN character token. Switch to the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<>' + ) + ); + + $this->state = 'data'; + + } elseif ($char === '?') { + /* U+003F QUESTION MARK (?) + Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + + } else { + /* Anything else + Parse error. Emit a U+003C LESS-THAN SIGN character token and + reconsume the current input character in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<' + ) + ); + + $this->char--; + $this->state = 'data'; + } + break; + } + } + + private function closeTagOpenState() + { + $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); + $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; + + if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && + (!$the_same || ($the_same && (!preg_match( + '/[\t\n\x0b\x0c >\/]/', + $this->character($this->char + 1 + strlen($next_node)) + ) || $this->EOF === $this->char))) + ) { + /* If the content model flag is set to the RCDATA or CDATA states then + examine the next few characters. If they do not match the tag name of + the last start tag token emitted (case insensitively), or if they do but + they are not immediately followed by one of the following characters: + * U+0009 CHARACTER TABULATION + * U+000A LINE FEED (LF) + * U+000B LINE TABULATION + * U+000C FORM FEED (FF) + * U+0020 SPACE + * U+003E GREATER-THAN SIGN (>) + * U+002F SOLIDUS (/) + * EOF + ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character + token, a U+002F SOLIDUS character token, and switch to the data state + to process the next input character. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => 'state = 'data'; + + } else { + /* Otherwise, if the content model flag is set to the PCDATA state, + or if the next few characters do match that tag name, consume the + next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new end tag token, set its tag name to the lowercase version + of the input character (add 0x0020 to the character's code point), then + switch to the tag name state. (Don't emit the token yet; further details + will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::ENDTAG + ); + + $this->state = 'tagName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Switch to the data state. */ + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F + SOLIDUS character token. Reconsume the EOF character in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => 'char--; + $this->state = 'data'; + + } else { + /* Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + } + } + } + + private function tagNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } else { + /* Anything else + Append the current input character to the current tag token's tag name. + Stay in the tag name state. */ + $this->token['name'] .= strtolower($char); + $this->state = 'tagName'; + } + } + + private function beforeAttributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Stay in the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function attributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif ($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's name. + Stay in the attribute name state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['name'] .= strtolower($char); + + $this->state = 'attributeName'; + } + } + + private function afterAttributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif ($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the + before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function beforeAttributeValueState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the attribute value (double-quoted) state. */ + $this->state = 'attributeValueDoubleQuoted'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the attribute value (unquoted) state and reconsume + this input character. */ + $this->char--; + $this->state = 'attributeValueUnquoted'; + + } elseif ($char === '\'') { + /* U+0027 APOSTROPHE (') + Switch to the attribute value (single-quoted) state. */ + $this->state = 'attributeValueSingleQuoted'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Switch to the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function attributeValueDoubleQuotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('double'); + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (double-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueDoubleQuoted'; + } + } + + private function attributeValueSingleQuotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if ($char === '\'') { + /* U+0022 QUOTATION MARK (') + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('single'); + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (single-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueSingleQuoted'; + } + } + + private function attributeValueUnquotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState(); + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function entityInAttributeValueState() + { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, append a U+0026 AMPERSAND character to the + // current attribute's value. Otherwise, emit the character token that + // was returned. + $char = (!$entity) + ? '&' + : $entity; + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + } + + private function bogusCommentState() + { + /* Consume every character up to the first U+003E GREATER-THAN SIGN + character (>) or the end of the file (EOF), whichever comes first. Emit + a comment token whose data is the concatenation of all the characters + starting from and including the character that caused the state machine + to switch into the bogus comment state, up to and including the last + consumed character before the U+003E character, if any, or up to the + end of the file otherwise. (If the comment was started by the end of + the file (EOF), the token is empty.) */ + $data = $this->characters('^>', $this->char); + $this->emitToken( + array( + 'data' => $data, + 'type' => self::COMMENT + ) + ); + + $this->char += strlen($data); + + /* Switch to the data state. */ + $this->state = 'data'; + + /* If the end of the file was reached, reconsume the EOF character. */ + if ($this->char === $this->EOF) { + $this->char = $this->EOF - 1; + } + } + + private function markupDeclarationOpenState() + { + /* If the next two characters are both U+002D HYPHEN-MINUS (-) + characters, consume those two characters, create a comment token whose + data is the empty string, and switch to the comment state. */ + if ($this->character($this->char + 1, 2) === '--') { + $this->char += 2; + $this->state = 'comment'; + $this->token = array( + 'data' => null, + 'type' => self::COMMENT + ); + + /* Otherwise if the next seven chacacters are a case-insensitive match + for the word "DOCTYPE", then consume those characters and switch to the + DOCTYPE state. */ + } elseif (strtolower($this->character($this->char + 1, 7)) === 'doctype') { + $this->char += 7; + $this->state = 'doctype'; + + /* Otherwise, is is a parse error. Switch to the bogus comment state. + The next character that is consumed, if any, is the first character + that will be in the comment. */ + } else { + $this->char++; + $this->state = 'bogusComment'; + } + } + + private function commentState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if ($char === '-') { + /* Switch to the comment dash state */ + $this->state = 'commentDash'; + + /* EOF */ + } elseif ($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append the input character to the comment token's data. Stay in + the comment state. */ + $this->token['data'] .= $char; + } + } + + private function commentDashState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if ($char === '-') { + /* Switch to the comment end state */ + $this->state = 'commentEnd'; + + /* EOF */ + } elseif ($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append a U+002D HYPHEN-MINUS (-) character and the input + character to the comment token's data. Switch to the comment state. */ + $this->token['data'] .= '-' . $char; + $this->state = 'comment'; + } + } + + private function commentEndState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '-') { + $this->token['data'] .= '-'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['data'] .= '--' . $char; + $this->state = 'comment'; + } + } + + private function doctypeState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'beforeDoctypeName'; + + } else { + $this->char--; + $this->state = 'beforeDoctypeName'; + } + } + + private function beforeDoctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the before DOCTYPE name state. + + } elseif (preg_match('/^[a-z]$/', $char)) { + $this->token = array( + 'name' => strtoupper($char), + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + + } elseif ($char === '>') { + $this->emitToken( + array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + ) + ); + + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken( + array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + ) + ); + + $this->char--; + $this->state = 'data'; + + } else { + $this->token = array( + 'name' => $char, + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + } + } + + private function doctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'AfterDoctypeName'; + + } elseif ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif (preg_match('/^[a-z]$/', $char)) { + $this->token['name'] .= strtoupper($char); + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['name'] .= $char; + } + + $this->token['error'] = ($this->token['name'] === 'HTML') + ? false + : true; + } + + private function afterDoctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the DOCTYPE name state. + + } elseif ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['error'] = true; + $this->state = 'bogusDoctype'; + } + } + + private function bogusDoctypeState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + // Stay in the bogus DOCTYPE state. + } + } + + private function entity() + { + $start = $this->char; + + // This section defines how to consume an entity. This definition is + // used when parsing entities in text and in attributes. + + // The behaviour depends on the identity of the next character (the + // one immediately after the U+0026 AMPERSAND character): + + switch ($this->character($this->char + 1)) { + // U+0023 NUMBER SIGN (#) + case '#': + + // The behaviour further depends on the character after the + // U+0023 NUMBER SIGN: + switch ($this->character($this->char + 1)) { + // U+0078 LATIN SMALL LETTER X + // U+0058 LATIN CAPITAL LETTER X + case 'x': + case 'X': + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 + // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER + // A, through to U+0046 LATIN CAPITAL LETTER F (in other + // words, 0-9, A-F, a-f). + $char = 1; + $char_class = '0-9A-Fa-f'; + break; + + // Anything else + default: + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE (i.e. just 0-9). + $char = 0; + $char_class = '0-9'; + break; + } + + // Consume as many characters as match the range of characters + // given above. + $this->char++; + $e_name = $this->characters($char_class, $this->char + $char + 1); + $entity = $this->character($start, $this->char); + $cond = strlen($e_name) > 0; + + // The rest of the parsing happens bellow. + break; + + // Anything else + default: + // Consume the maximum number of characters possible, with the + // consumed characters case-sensitively matching one of the + // identifiers in the first column of the entities table. + $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); + $len = strlen($e_name); + + for ($c = 1; $c <= $len; $c++) { + $id = substr($e_name, 0, $c); + $this->char++; + + if (in_array($id, $this->entities)) { + if ($e_name[$c - 1] !== ';') { + if ($c < $len && $e_name[$c] == ';') { + $this->char++; // consume extra semicolon + } + } + $entity = $id; + break; + } + } + + $cond = isset($entity); + // The rest of the parsing happens bellow. + break; + } + + if (!$cond) { + // If no match can be made, then this is a parse error. No + // characters are consumed, and nothing is returned. + $this->char = $start; + return false; + } + + // Return a character token for the character corresponding to the + // entity name (as given by the second column of the entities table). + return html_entity_decode('&' . $entity . ';', ENT_QUOTES, 'UTF-8'); + } + + private function emitToken($token) + { + $emit = $this->tree->emitToken($token); + + if (is_int($emit)) { + $this->content_model = $emit; + + } elseif ($token['type'] === self::ENDTAG) { + $this->content_model = self::PCDATA; + } + } + + private function EOF() + { + $this->state = null; + $this->tree->emitToken( + array( + 'type' => self::EOF + ) + ); + } +} + +class HTML5TreeConstructer +{ + public $stack = array(); + + private $phase; + private $mode; + private $dom; + private $foster_parent = null; + private $a_formatting = array(); + + private $head_pointer = null; + private $form_pointer = null; + + private $scoping = array('button', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'th'); + private $formatting = array( + 'a', + 'b', + 'big', + 'em', + 'font', + 'i', + 'nobr', + 's', + 'small', + 'strike', + 'strong', + 'tt', + 'u' + ); + private $special = array( + 'address', + 'area', + 'base', + 'basefont', + 'bgsound', + 'blockquote', + 'body', + 'br', + 'center', + 'col', + 'colgroup', + 'dd', + 'dir', + 'div', + 'dl', + 'dt', + 'embed', + 'fieldset', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'hr', + 'iframe', + 'image', + 'img', + 'input', + 'isindex', + 'li', + 'link', + 'listing', + 'menu', + 'meta', + 'noembed', + 'noframes', + 'noscript', + 'ol', + 'optgroup', + 'option', + 'p', + 'param', + 'plaintext', + 'pre', + 'script', + 'select', + 'spacer', + 'style', + 'tbody', + 'textarea', + 'tfoot', + 'thead', + 'title', + 'tr', + 'ul', + 'wbr' + ); + + // The different phases. + const INIT_PHASE = 0; + const ROOT_PHASE = 1; + const MAIN_PHASE = 2; + const END_PHASE = 3; + + // The different insertion modes for the main phase. + const BEFOR_HEAD = 0; + const IN_HEAD = 1; + const AFTER_HEAD = 2; + const IN_BODY = 3; + const IN_TABLE = 4; + const IN_CAPTION = 5; + const IN_CGROUP = 6; + const IN_TBODY = 7; + const IN_ROW = 8; + const IN_CELL = 9; + const IN_SELECT = 10; + const AFTER_BODY = 11; + const IN_FRAME = 12; + const AFTR_FRAME = 13; + + // The different types of elements. + const SPECIAL = 0; + const SCOPING = 1; + const FORMATTING = 2; + const PHRASING = 3; + + const MARKER = 0; + + public function __construct() + { + $this->phase = self::INIT_PHASE; + $this->mode = self::BEFOR_HEAD; + $this->dom = new DOMDocument; + + $this->dom->encoding = 'UTF-8'; + $this->dom->preserveWhiteSpace = true; + $this->dom->substituteEntities = true; + $this->dom->strictErrorChecking = false; + } + + // Process tag tokens + public function emitToken($token) + { + switch ($this->phase) { + case self::INIT_PHASE: + return $this->initPhase($token); + break; + case self::ROOT_PHASE: + return $this->rootElementPhase($token); + break; + case self::MAIN_PHASE: + return $this->mainPhase($token); + break; + case self::END_PHASE : + return $this->trailingEndPhase($token); + break; + } + } + + private function initPhase($token) + { + /* Initially, the tree construction stage must handle each token + emitted from the tokenisation stage as follows: */ + + /* A DOCTYPE token that is marked as being in error + A comment token + A start tag token + An end tag token + A character token that is not one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE + An end-of-file token */ + if ((isset($token['error']) && $token['error']) || + $token['type'] === HTML5::COMMENT || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF || + ($token['type'] === HTML5::CHARACTR && isset($token['data']) && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) + ) { + /* This specification does not define how to handle this case. In + particular, user agents may ignore the entirety of this specification + altogether for such documents, and instead invoke special parse modes + with a greater emphasis on backwards compatibility. */ + + $this->phase = self::ROOT_PHASE; + return $this->rootElementPhase($token); + + /* A DOCTYPE token marked as being correct */ + } elseif (isset($token['error']) && !$token['error']) { + /* Append a DocumentType node to the Document node, with the name + attribute set to the name given in the DOCTYPE token (which will be + "HTML"), and the other attributes specific to DocumentType objects + set to null, empty lists, or the empty string as appropriate. */ + $doctype = new DOMDocumentType(null, null, 'HTML'); + + /* Then, switch to the root element phase of the tree construction + stage. */ + $this->phase = self::ROOT_PHASE; + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif (isset($token['data']) && preg_match( + '/^[\t\n\x0b\x0c ]+$/', + $token['data'] + ) + ) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + } + } + + private function rootElementPhase($token) + { + /* After the initial phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED + (FF), or U+0020 SPACE + A start tag token + An end tag token + An end-of-file token */ + } elseif (($token['type'] === HTML5::CHARACTR && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF + ) { + /* Create an HTMLElement node with the tag name html, in the HTML + namespace. Append it to the Document object. Switch to the main + phase and reprocess the current token. */ + $html = $this->dom->createElement('html'); + $this->dom->appendChild($html); + $this->stack[] = $html; + + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + } + } + + private function mainPhase($token) + { + /* Tokens in the main phase must be handled as follows: */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A start tag token with the tag name "html" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { + /* If this start tag token was not the first start tag token, then + it is a parse error. */ + + /* For each attribute on the token, check to see if the attribute + is already present on the top element of the stack of open elements. + If it is not, add the attribute and its corresponding value to that + element. */ + foreach ($token['attr'] as $attr) { + if (!$this->stack[0]->hasAttribute($attr['name'])) { + $this->stack[0]->setAttribute($attr['name'], $attr['value']); + } + } + + /* An end-of-file token */ + } elseif ($token['type'] === HTML5::EOF) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Anything else. */ + } else { + /* Depends on the insertion mode: */ + switch ($this->mode) { + case self::BEFOR_HEAD: + return $this->beforeHead($token); + break; + case self::IN_HEAD: + return $this->inHead($token); + break; + case self::AFTER_HEAD: + return $this->afterHead($token); + break; + case self::IN_BODY: + return $this->inBody($token); + break; + case self::IN_TABLE: + return $this->inTable($token); + break; + case self::IN_CAPTION: + return $this->inCaption($token); + break; + case self::IN_CGROUP: + return $this->inColumnGroup($token); + break; + case self::IN_TBODY: + return $this->inTableBody($token); + break; + case self::IN_ROW: + return $this->inRow($token); + break; + case self::IN_CELL: + return $this->inCell($token); + break; + case self::IN_SELECT: + return $this->inSelect($token); + break; + case self::AFTER_BODY: + return $this->afterBody($token); + break; + case self::IN_FRAME: + return $this->inFrameset($token); + break; + case self::AFTR_FRAME: + return $this->afterFrameset($token); + break; + case self::END_PHASE: + return $this->trailingEndPhase($token); + break; + } + } + } + + private function beforeHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "head" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { + /* Create an element for the token, append the new element to the + current node and push it onto the stack of open elements. */ + $element = $this->insertElement($token); + + /* Set the head element pointer to this new element node. */ + $this->head_pointer = $element; + + /* Change the insertion mode to "in head". */ + $this->mode = self::IN_HEAD; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title". Or an end tag with the tag name "html". + Or a character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or any other start tag token */ + } elseif ($token['type'] === HTML5::STARTTAG || + ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || + ($token['type'] === HTML5::CHARACTR && !preg_match( + '/^[\t\n\x0b\x0c ]$/', + $token['data'] + )) + ) { + /* Act as if a start tag token with the tag name "head" and no + attributes had been seen, then reprocess the current token. */ + $this->beforeHead( + array( + 'name' => 'head', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inHead($token); + + /* Any other end tag */ + } elseif ($token['type'] === HTML5::ENDTAG) { + /* Parse error. Ignore the token. */ + } + } + + private function inHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. + + THIS DIFFERS FROM THE SPEC: If the current node is either a title, style + or script element, append the character to the current node regardless + of its content. */ + if (($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( + $token['type'] === HTML5::CHARACTR && in_array( + end($this->stack)->nodeName, + array('title', 'style', 'script') + )) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('title', 'style', 'script')) + ) { + array_pop($this->stack); + return HTML5::PCDATA; + + /* A start tag with the tag name "title" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $element = $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the RCDATA state. */ + return HTML5::RCDATA; + + /* A start tag with the tag name "style" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "script" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { + /* Create an element for the token. */ + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "base", "link", or "meta" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('base', 'link', 'meta') + ) + ) { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + array_pop($this->stack); + + } else { + $this->insertElement($token); + } + + /* An end tag with the tag name "head" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { + /* If the current node is a head element, pop the current node off + the stack of open elements. */ + if ($this->head_pointer->isSameNode(end($this->stack))) { + array_pop($this->stack); + + /* Otherwise, this is a parse error. */ + } else { + // k + } + + /* Change the insertion mode to "after head". */ + $this->mode = self::AFTER_HEAD; + + /* A start tag with the tag name "head" or an end tag except "html". */ + } elseif (($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || + ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html') + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* If the current node is a head element, act as if an end tag + token with the tag name "head" had been seen. */ + if ($this->head_pointer->isSameNode(end($this->stack))) { + $this->inHead( + array( + 'name' => 'head', + 'type' => HTML5::ENDTAG + ) + ); + + /* Otherwise, change the insertion mode to "after head". */ + } else { + $this->mode = self::AFTER_HEAD; + } + + /* Then, reprocess the current token. */ + return $this->afterHead($token); + } + } + + private function afterHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "body" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { + /* Insert a body element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in body". */ + $this->mode = self::IN_BODY; + + /* A start tag token with the tag name "frameset" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { + /* Insert a frameset element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in frameset". */ + $this->mode = self::IN_FRAME; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('base', 'link', 'meta', 'script', 'style', 'title') + ) + ) { + /* Parse error. Switch the insertion mode back to "in head" and + reprocess the token. */ + $this->mode = self::IN_HEAD; + return $this->inHead($token); + + /* Anything else */ + } else { + /* Act as if a start tag token with the tag name "body" and no + attributes had been seen, and then reprocess the current token. */ + $this->afterHead( + array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inBody($token); + } + } + + private function inBody($token) + { + /* Handle the token as follows: */ + + switch ($token['type']) { + /* A character token */ + case HTML5::CHARACTR: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + break; + + /* A comment token */ + case HTML5::COMMENT: + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + break; + + case HTML5::STARTTAG: + switch ($token['name']) { + /* A start tag token whose tag name is one of: "script", + "style" */ + case 'script': + case 'style': + /* Process the token as if the insertion mode had been "in + head". */ + return $this->inHead($token); + break; + + /* A start tag token whose tag name is one of: "base", "link", + "meta", "title" */ + case 'base': + case 'link': + case 'meta': + case 'title': + /* Parse error. Process the token as if the insertion mode + had been "in head". */ + return $this->inHead($token); + break; + + /* A start tag token with the tag name "body" */ + case 'body': + /* Parse error. If the second element on the stack of open + elements is not a body element, or, if the stack of open + elements has only one node on it, then ignore the token. + (innerHTML case) */ + if (count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { + // Ignore + + /* Otherwise, for each attribute on the token, check to see + if the attribute is already present on the body element (the + second element) on the stack of open elements. If it is not, + add the attribute and its corresponding value to that + element. */ + } else { + foreach ($token['attr'] as $attr) { + if (!$this->stack[1]->hasAttribute($attr['name'])) { + $this->stack[1]->setAttribute($attr['name'], $attr['value']); + } + } + } + break; + + /* A start tag whose tag name is one of: "address", + "blockquote", "center", "dir", "div", "dl", "fieldset", + "listing", "menu", "ol", "p", "ul" */ + case 'address': + case 'blockquote': + case 'center': + case 'dir': + case 'div': + case 'dl': + case 'fieldset': + case 'listing': + case 'menu': + case 'ol': + case 'p': + case 'ul': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "form" */ + case 'form': + /* If the form element pointer is not null, ignore the + token with a parse error. */ + if ($this->form_pointer !== null) { + // Ignore. + + /* Otherwise: */ + } else { + /* If the stack of open elements has a p element in + scope, then act as if an end tag with the tag name p + had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token, and set the + form element pointer to point to the element created. */ + $element = $this->insertElement($token); + $this->form_pointer = $element; + } + break; + + /* A start tag whose tag name is "li", "dd" or "dt" */ + case 'li': + case 'dd': + case 'dt': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + $stack_length = count($this->stack) - 1; + + for ($n = $stack_length; 0 <= $n; $n--) { + /* 1. Initialise node to be the current node (the + bottommost node of the stack). */ + $stop = false; + $node = $this->stack[$n]; + $cat = $this->getElementCategory($node->tagName); + + /* 2. If node is an li, dd or dt element, then pop all + the nodes from the current node up to node, including + node, then stop this algorithm. */ + if ($token['name'] === $node->tagName || ($token['name'] !== 'li' + && ($node->tagName === 'dd' || $node->tagName === 'dt')) + ) { + for ($x = $stack_length; $x >= $n; $x--) { + array_pop($this->stack); + } + + break; + } + + /* 3. If node is not in the formatting category, and is + not in the phrasing category, and is not an address or + div element, then stop this algorithm. */ + if ($cat !== self::FORMATTING && $cat !== self::PHRASING && + $node->tagName !== 'address' && $node->tagName !== 'div' + ) { + break; + } + } + + /* Finally, insert an HTML element with the same tag + name as the token's. */ + $this->insertElement($token); + break; + + /* A start tag token whose tag name is "plaintext" */ + case 'plaintext': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + return HTML5::PLAINTEXT; + break; + + /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + this is a parse error; pop elements from the stack until an + element with one of those tag names has been popped from the + stack. */ + while ($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { + array_pop($this->stack); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "a" */ + case 'a': + /* If the list of active formatting elements contains + an element whose tag name is "a" between the end of the + list and the last marker on the list (or the start of + the list if there is no marker on the list), then this + is a parse error; act as if an end tag with the tag name + "a" had been seen, then remove that element from the list + of active formatting elements and the stack of open + elements if the end tag didn't already remove it (it + might not have if the element is not in table scope). */ + $leng = count($this->a_formatting); + + for ($n = $leng - 1; $n >= 0; $n--) { + if ($this->a_formatting[$n] === self::MARKER) { + break; + + } elseif ($this->a_formatting[$n]->nodeName === 'a') { + $this->emitToken( + array( + 'name' => 'a', + 'type' => HTML5::ENDTAG + ) + ); + break; + } + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag whose tag name is one of: "b", "big", "em", "font", + "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'b': + case 'big': + case 'em': + case 'font': + case 'i': + case 'nobr': + case 's': + case 'small': + case 'strike': + case 'strong': + case 'tt': + case 'u': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag token whose tag name is "button" */ + case 'button': + /* If the stack of open elements has a button element in scope, + then this is a parse error; act as if an end tag with the tag + name "button" had been seen, then reprocess the token. (We don't + do that. Unnecessary.) */ + if ($this->elementInScope('button')) { + $this->inBody( + array( + 'name' => 'button', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is one of: "marquee", "object" */ + case 'marquee': + case 'object': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is "xmp" */ + case 'xmp': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Switch the content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "table" */ + case 'table': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + break; + + /* A start tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ + case 'area': + case 'basefont': + case 'bgsound': + case 'br': + case 'embed': + case 'img': + case 'param': + case 'spacer': + case 'wbr': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "hr" */ + case 'hr': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "image" */ + case 'image': + /* Parse error. Change the token's tag name to "img" and + reprocess it. (Don't ask.) */ + $token['name'] = 'img'; + return $this->inBody($token); + break; + + /* A start tag whose tag name is "input" */ + case 'input': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an input element for the token. */ + $element = $this->insertElement($token, false); + + /* If the form element pointer is not null, then associate the + input element with the form element pointed to by the form + element pointer. */ + $this->form_pointer !== null + ? $this->form_pointer->appendChild($element) + : end($this->stack)->appendChild($element); + + /* Pop that input element off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "isindex" */ + case 'isindex': + /* Parse error. */ + // w/e + + /* If the form element pointer is not null, + then ignore the token. */ + if ($this->form_pointer === null) { + /* Act as if a start tag token with the tag name "form" had + been seen. */ + $this->inBody( + array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody( + array( + 'name' => 'hr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "p" had + been seen. */ + $this->inBody( + array( + 'name' => 'p', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "label" + had been seen. */ + $this->inBody( + array( + 'name' => 'label', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a stream of character tokens had been seen. */ + $this->insertText( + 'This is a searchable index. ' . + 'Insert your search keywords here: ' + ); + + /* Act as if a start tag token with the tag name "input" + had been seen, with all the attributes from the "isindex" + token, except with the "name" attribute set to the value + "isindex" (ignoring any explicit "name" attribute). */ + $attr = $token['attr']; + $attr[] = array('name' => 'name', 'value' => 'isindex'); + + $this->inBody( + array( + 'name' => 'input', + 'type' => HTML5::STARTTAG, + 'attr' => $attr + ) + ); + + /* Act as if a stream of character tokens had been seen + (see below for what they should say). */ + $this->insertText( + 'This is a searchable index. ' . + 'Insert your search keywords here: ' + ); + + /* Act as if an end tag token with the tag name "label" + had been seen. */ + $this->inBody( + array( + 'name' => 'label', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if an end tag token with the tag name "p" had + been seen. */ + $this->inBody( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody( + array( + 'name' => 'hr', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if an end tag token with the tag name "form" had + been seen. */ + $this->inBody( + array( + 'name' => 'form', + 'type' => HTML5::ENDTAG + ) + ); + } + break; + + /* A start tag whose tag name is "textarea" */ + case 'textarea': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the + RCDATA state. */ + return HTML5::RCDATA; + break; + + /* A start tag whose tag name is one of: "iframe", "noembed", + "noframes" */ + case 'iframe': + case 'noembed': + case 'noframes': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "select" */ + case 'select': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in select". */ + $this->mode = self::IN_SELECT; + break; + + /* A start or end tag whose tag name is one of: "caption", "col", + "colgroup", "frame", "frameset", "head", "option", "optgroup", + "tbody", "td", "tfoot", "th", "thead", "tr". */ + case 'caption': + case 'col': + case 'colgroup': + case 'frame': + case 'frameset': + case 'head': + case 'option': + case 'optgroup': + case 'tbody': + case 'td': + case 'tfoot': + case 'th': + case 'thead': + case 'tr': + // Parse error. Ignore the token. + break; + + /* A start or end tag whose tag name is one of: "event-source", + "section", "nav", "article", "aside", "header", "footer", + "datagrid", "command" */ + case 'event-source': + case 'section': + case 'nav': + case 'article': + case 'aside': + case 'header': + case 'footer': + case 'datagrid': + case 'command': + // Work in progress! + break; + + /* A start tag token not covered by the previous entries */ + default: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + $this->insertElement($token, true, true); + break; + } + break; + + case HTML5::ENDTAG: + switch ($token['name']) { + /* An end tag with the tag name "body" */ + case 'body': + /* If the second element in the stack of open elements is + not a body element, this is a parse error. Ignore the token. + (innerHTML case) */ + if (count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { + // Ignore. + + /* If the current node is not the body element, then this + is a parse error. */ + } elseif (end($this->stack)->nodeName !== 'body') { + // Parse error. + } + + /* Change the insertion mode to "after body". */ + $this->mode = self::AFTER_BODY; + break; + + /* An end tag with the tag name "html" */ + case 'html': + /* Act as if an end tag with tag name "body" had been seen, + then, if that token wasn't ignored, reprocess the current + token. */ + $this->inBody( + array( + 'name' => 'body', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->afterBody($token); + break; + + /* An end tag whose tag name is one of: "address", "blockquote", + "center", "dir", "div", "dl", "fieldset", "listing", "menu", + "ol", "pre", "ul" */ + case 'address': + case 'blockquote': + case 'center': + case 'dir': + case 'div': + case 'dl': + case 'fieldset': + case 'listing': + case 'menu': + case 'ol': + case 'pre': + case 'ul': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with + the same tag name as that of the token, then this + is a parse error. */ + // w/e + + /* If the stack of open elements has an element in + scope with the same tag name as that of the token, + then pop elements from this stack until an element + with that tag name has been popped from the stack. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is "form" */ + case 'form': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + } + + if (end($this->stack)->nodeName !== $token['name']) { + /* Now, if the current node is not an element with the + same tag name as that of the token, then this is a parse + error. */ + // w/e + + } else { + /* Otherwise, if the current node is an element with + the same tag name as that of the token pop that element + from the stack. */ + array_pop($this->stack); + } + + /* In any case, set the form element pointer to null. */ + $this->form_pointer = null; + break; + + /* An end tag whose tag name is "p" */ + case 'p': + /* If the stack of open elements has a p element in scope, + then generate implied end tags, except for p elements. */ + if ($this->elementInScope('p')) { + $this->generateImpliedEndTags(array('p')); + + /* If the current node is not a p element, then this is + a parse error. */ + // k + + /* If the stack of open elements has a p element in + scope, then pop elements from this stack until the stack + no longer has a p element in scope. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->elementInScope('p')) { + array_pop($this->stack); + + } else { + break; + } + } + } + break; + + /* An end tag whose tag name is "dd", "dt", or "li" */ + case 'dd': + case 'dt': + case 'li': + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + generate implied end tags, except for elements with the + same tag name as the token. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(array($token['name'])); + + /* If the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + pop elements from this stack until an element with that + tag name has been popped from the stack. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + generate implied end tags. */ + if ($this->elementInScope($elements)) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as that of the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has in scope an element + whose tag name is one of "h1", "h2", "h3", "h4", "h5", or + "h6", then pop elements from the stack until an element + with one of those tag names has been popped from the stack. */ + while ($this->elementInScope($elements)) { + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "a", "b", "big", "em", + "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'a': + case 'b': + case 'big': + case 'em': + case 'font': + case 'i': + case 'nobr': + case 's': + case 'small': + case 'strike': + case 'strong': + case 'tt': + case 'u': + /* 1. Let the formatting element be the last element in + the list of active formatting elements that: + * is between the end of the list and the last scope + marker in the list, if any, or the start of the list + otherwise, and + * has the same tag name as the token. + */ + while (true) { + for ($a = count($this->a_formatting) - 1; $a >= 0; $a--) { + if ($this->a_formatting[$a] === self::MARKER) { + break; + + } elseif ($this->a_formatting[$a]->tagName === $token['name']) { + $formatting_element = $this->a_formatting[$a]; + $in_stack = in_array($formatting_element, $this->stack, true); + $fe_af_pos = $a; + break; + } + } + + /* If there is no such node, or, if that node is + also in the stack of open elements but the element + is not in scope, then this is a parse error. Abort + these steps. The token is ignored. */ + if (!isset($formatting_element) || ($in_stack && + !$this->elementInScope($token['name'])) + ) { + break; + + /* Otherwise, if there is such a node, but that node + is not in the stack of open elements, then this is a + parse error; remove the element from the list, and + abort these steps. */ + } elseif (isset($formatting_element) && !$in_stack) { + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 2. Let the furthest block be the topmost node in the + stack of open elements that is lower in the stack + than the formatting element, and is not an element in + the phrasing or formatting categories. There might + not be one. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $length = count($this->stack); + + for ($s = $fe_s_pos + 1; $s < $length; $s++) { + $category = $this->getElementCategory($this->stack[$s]->nodeName); + + if ($category !== self::PHRASING && $category !== self::FORMATTING) { + $furthest_block = $this->stack[$s]; + } + } + + /* 3. If there is no furthest block, then the UA must + skip the subsequent steps and instead just pop all + the nodes from the bottom of the stack of open + elements, from the current node up to the formatting + element, and remove the formatting element from the + list of active formatting elements. */ + if (!isset($furthest_block)) { + for ($n = $length - 1; $n >= $fe_s_pos; $n--) { + array_pop($this->stack); + } + + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 4. Let the common ancestor be the element + immediately above the formatting element in the stack + of open elements. */ + $common_ancestor = $this->stack[$fe_s_pos - 1]; + + /* 5. If the furthest block has a parent node, then + remove the furthest block from its parent node. */ + if ($furthest_block->parentNode !== null) { + $furthest_block->parentNode->removeChild($furthest_block); + } + + /* 6. Let a bookmark note the position of the + formatting element in the list of active formatting + elements relative to the elements on either side + of it in the list. */ + $bookmark = $fe_af_pos; + + /* 7. Let node and last node be the furthest block. + Follow these steps: */ + $node = $furthest_block; + $last_node = $furthest_block; + + while (true) { + for ($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { + /* 7.1 Let node be the element immediately + prior to node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 7.2 If node is not in the list of active + formatting elements, then remove node from + the stack of open elements and then go back + to step 1. */ + if (!in_array($node, $this->a_formatting, true)) { + unset($this->stack[$n]); + $this->stack = array_merge($this->stack); + + } else { + break; + } + } + + /* 7.3 Otherwise, if node is the formatting + element, then go to the next step in the overall + algorithm. */ + if ($node === $formatting_element) { + break; + + /* 7.4 Otherwise, if last node is the furthest + block, then move the aforementioned bookmark to + be immediately after the node in the list of + active formatting elements. */ + } elseif ($last_node === $furthest_block) { + $bookmark = array_search($node, $this->a_formatting, true) + 1; + } + + /* 7.5 If node has any children, perform a + shallow clone of node, replace the entry for + node in the list of active formatting elements + with an entry for the clone, replace the entry + for node in the stack of open elements with an + entry for the clone, and let node be the clone. */ + if ($node->hasChildNodes()) { + $clone = $node->cloneNode(); + $s_pos = array_search($node, $this->stack, true); + $a_pos = array_search($node, $this->a_formatting, true); + + $this->stack[$s_pos] = $clone; + $this->a_formatting[$a_pos] = $clone; + $node = $clone; + } + + /* 7.6 Insert last node into node, first removing + it from its previous parent node if any. */ + if ($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $node->appendChild($last_node); + + /* 7.7 Let last node be node. */ + $last_node = $node; + } + + /* 8. Insert whatever last node ended up being in + the previous step into the common ancestor node, + first removing it from its previous parent node if + any. */ + if ($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $common_ancestor->appendChild($last_node); + + /* 9. Perform a shallow clone of the formatting + element. */ + $clone = $formatting_element->cloneNode(); + + /* 10. Take all of the child nodes of the furthest + block and append them to the clone created in the + last step. */ + while ($furthest_block->hasChildNodes()) { + $child = $furthest_block->firstChild; + $furthest_block->removeChild($child); + $clone->appendChild($child); + } + + /* 11. Append that clone to the furthest block. */ + $furthest_block->appendChild($clone); + + /* 12. Remove the formatting element from the list + of active formatting elements, and insert the clone + into the list of active formatting elements at the + position of the aforementioned bookmark. */ + $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + + $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); + $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); + $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); + + /* 13. Remove the formatting element from the stack + of open elements, and insert the clone into the stack + of open elements immediately after (i.e. in a more + deeply nested position than) the position of the + furthest block in that stack. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $fb_s_pos = array_search($furthest_block, $this->stack, true); + unset($this->stack[$fe_s_pos]); + + $s_part1 = array_slice($this->stack, 0, $fb_s_pos); + $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); + $this->stack = array_merge($s_part1, array($clone), $s_part2); + + /* 14. Jump back to step 1 in this series of steps. */ + unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); + } + break; + + /* An end tag token whose tag name is one of: "button", + "marquee", "object" */ + case 'button': + case 'marquee': + case 'object': + /* If the stack of open elements has an element in scope whose + tag name matches the tag name of the token, then generate implied + tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // k + + /* Now, if the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then pop + elements from the stack until that element has been popped from + the stack, and clear the list of active formatting elements up + to the last marker. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + + $marker = end(array_keys($this->a_formatting, self::MARKER, true)); + + for ($n = count($this->a_formatting) - 1; $n > $marker; $n--) { + array_pop($this->a_formatting); + } + } + break; + + /* Or an end tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "hr", "iframe", "image", "img", + "input", "isindex", "noembed", "noframes", "param", "select", + "spacer", "table", "textarea", "wbr" */ + case 'area': + case 'basefont': + case 'bgsound': + case 'br': + case 'embed': + case 'hr': + case 'iframe': + case 'image': + case 'img': + case 'input': + case 'isindex': + case 'noembed': + case 'noframes': + case 'param': + case 'select': + case 'spacer': + case 'table': + case 'textarea': + case 'wbr': + // Parse error. Ignore the token. + break; + + /* An end tag token not covered by the previous entries */ + default: + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + /* Initialise node to be the current node (the bottommost + node of the stack). */ + $node = end($this->stack); + + /* If node has the same tag name as the end tag token, + then: */ + if ($token['name'] === $node->nodeName) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* If the tag name of the end tag token does not + match the tag name of the current node, this is a + parse error. */ + // k + + /* Pop all the nodes from the current node up to + node, including node, then stop this algorithm. */ + for ($x = count($this->stack) - $n; $x >= $n; $x--) { + array_pop($this->stack); + } + + } else { + $category = $this->getElementCategory($node); + + if ($category !== self::SPECIAL && $category !== self::SCOPING) { + /* Otherwise, if node is in neither the formatting + category nor the phrasing category, then this is a + parse error. Stop this algorithm. The end tag token + is ignored. */ + return false; + } + } + } + break; + } + break; + } + } + + private function inTable($token) + { + $clear = array('html', 'table'); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "caption" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'caption' + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + + /* Insert an HTML element for the token, then switch the + insertion mode to "in caption". */ + $this->insertElement($token); + $this->mode = self::IN_CAPTION; + + /* A start tag whose tag name is "colgroup" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'colgroup' + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the + insertion mode to "in column group". */ + $this->insertElement($token); + $this->mode = self::IN_CGROUP; + + /* A start tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'col' + ) { + $this->inTable( + array( + 'name' => 'colgroup', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + $this->inColumnGroup($token); + + /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('tbody', 'tfoot', 'thead') + ) + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in table body". */ + $this->insertElement($token); + $this->mode = self::IN_TBODY; + + /* A start tag whose tag name is one of: "td", "th", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && + in_array($token['name'], array('td', 'th', 'tr')) + ) { + /* Act as if a start tag token with the tag name "tbody" had been + seen, then reprocess the current token. */ + $this->inTable( + array( + 'name' => 'tbody', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inTableBody($token); + + /* A start tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'table' + ) { + /* Parse error. Act as if an end tag token with the tag name "table" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inTable( + array( + 'name' => 'table', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->mainPhase($token); + + /* An end tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table' + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + return false; + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a table element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a table element has been + popped from the stack. */ + while (true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($current === 'table') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array( + 'body', + 'caption', + 'col', + 'colgroup', + 'html', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Parse error. Process the token as if the insertion mode was "in + body", with the following exception: */ + + /* If the current node is a table, tbody, tfoot, thead, or tr + element, then, whenever a node would be inserted into the current + node, it must instead be inserted into the foster parent element. */ + if (in_array( + end($this->stack)->nodeName, + array('table', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* The foster parent element is the parent element of the last + table element in the stack of open elements, if there is a + table element and it has such a parent element. If there is no + table element in the stack of open elements (innerHTML case), + then the foster parent element is the first element in the + stack of open elements (the html element). Otherwise, if there + is a table element in the stack of open elements, but the last + table element in the stack of open elements has no parent, or + its parent node is not an element, then the foster parent + element is the element before the last table element in the + stack of open elements. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === 'table') { + $table = $this->stack[$n]; + break; + } + } + + if (isset($table) && $table->parentNode !== null) { + $this->foster_parent = $table->parentNode; + + } elseif (!isset($table)) { + $this->foster_parent = $this->stack[0]; + + } elseif (isset($table) && ($table->parentNode === null || + $table->parentNode->nodeType !== XML_ELEMENT_NODE) + ) { + $this->foster_parent = $this->stack[$n - 1]; + } + } + + $this->inBody($token); + } + } + + private function inCaption($token) + { + /* An end tag whose tag name is "caption" */ + if ($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a caption element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a caption element has + been popped from the stack. */ + while (true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($node === 'caption') { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag + name is "table" */ + } elseif (($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + )) || ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table') + ) { + /* Parse error. Act as if an end tag with the tag name "caption" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inCaption( + array( + 'name' => 'caption', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inTable($token); + + /* An end tag whose tag name is one of: "body", "col", "colgroup", + "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array( + 'body', + 'col', + 'colgroup', + 'html', + 'tbody', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inColumnGroup($token) + { + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { + /* Insert a col element for the token. Immediately pop the current + node off the stack of open elements. */ + $this->insertElement($token); + array_pop($this->stack); + + /* An end tag whose tag name is "colgroup" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'colgroup' + ) { + /* If the current node is the root html element, then this is a + parse error, ignore the token. (innerHTML case) */ + if (end($this->stack)->nodeName === 'html') { + // Ignore + + /* Otherwise, pop the current node (which will be a colgroup + element) from the stack of open elements. Switch the insertion + mode to "in table". */ + } else { + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* An end tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Act as if an end tag with the tag name "colgroup" had been seen, + and then, if that token wasn't ignored, reprocess the current token. */ + $this->inColumnGroup( + array( + 'name' => 'colgroup', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inTable($token); + } + } + + private function inTableBody($token) + { + $clear = array('tbody', 'tfoot', 'thead', 'html'); + + /* A start tag whose tag name is "tr" */ + if ($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Insert a tr element for the token, then switch the insertion + mode to "in row". */ + $this->insertElement($token); + $this->mode = self::IN_ROW; + + /* A start tag whose tag name is one of: "th", "td" */ + } elseif ($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td') + ) { + /* Parse error. Act as if a start tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inTableBody( + array( + 'name' => 'tr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inRow($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead')) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node from the stack of open elements. Switch + the insertion mode to "in table". */ + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ + } elseif (($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead') + )) || + ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table') + ) { + /* If the stack of open elements does not have a tbody, thead, or + tfoot element in table scope, this is a parse error. Ignore the + token. (innerHTML case) */ + if (!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Act as if an end tag with the same tag name as the current + node ("tbody", "tfoot", or "thead") had been seen, then + reprocess the current token. */ + $this->inTableBody( + array( + 'name' => end($this->stack)->nodeName, + 'type' => HTML5::ENDTAG + ) + ); + + return $this->mainPhase($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') + ) + ) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inRow($token) + { + $clear = array('tr', 'html'); + + /* A start tag whose tag name is one of: "th", "td" */ + if ($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td') + ) { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in cell". */ + $this->insertElement($token); + $this->mode = self::IN_CELL; + + /* Insert a marker at the end of the list of active formatting + elements. */ + $this->a_formatting[] = self::MARKER; + + /* An end tag whose tag name is "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node (which will be a tr element) from the + stack of open elements. Switch the insertion mode to "in table + body". */ + array_pop($this->stack); + $this->mode = self::IN_TBODY; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* Act as if an end tag with the tag name "tr" had been seen, then, + if that token wasn't ignored, reprocess the current token. */ + $this->inRow( + array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inCell($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead')) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Otherwise, act as if an end tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inRow( + array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inCell($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') + ) + ) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inCell($token) + { + /* An end tag whose tag name is one of: "td", "th" */ + if ($token['type'] === HTML5::ENDTAG && + ($token['name'] === 'td' || $token['name'] === 'th') + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token, then this is a + parse error and the token must be ignored. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Generate implied end tags, except for elements with the same + tag name as the token. */ + $this->generateImpliedEndTags(array($token['name'])); + + /* Now, if the current node is not an element with the same tag + name as the token, then this is a parse error. */ + // k + + /* Pop elements from this stack until an element with the same + tag name as the token has been popped from the stack. */ + while (true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($node === $token['name']) { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in row". (The current node + will be a tr element at this point.) */ + $this->mode = self::IN_ROW; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if (!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if (!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html') + ) + ) { + /* Parse error. Ignore the token. */ + + /* An end tag whose tag name is one of: "table", "tbody", "tfoot", + "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('table', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token (which can only + happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), + then this is a parse error and the token must be ignored. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inSelect($token) + { + /* Handle the token as follows: */ + + /* A character token */ + if ($token['type'] === HTML5::CHARACTR) { + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token whose tag name is "option" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'option' + ) { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if (end($this->stack)->nodeName === 'option') { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* A start tag token whose tag name is "optgroup" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'optgroup' + ) { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if (end($this->stack)->nodeName === 'option') { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the current node is an optgroup element, act as if an end tag + with the tag name "optgroup" had been seen. */ + if (end($this->stack)->nodeName === 'optgroup') { + $this->inSelect( + array( + 'name' => 'optgroup', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* An end tag token whose tag name is "optgroup" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'optgroup' + ) { + /* First, if the current node is an option element, and the node + immediately before it in the stack of open elements is an optgroup + element, then act as if an end tag with the tag name "option" had + been seen. */ + $elements_in_stack = count($this->stack); + + if ($this->stack[$elements_in_stack - 1]->nodeName === 'option' && + $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup' + ) { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the current node is an optgroup element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if ($this->stack[$elements_in_stack - 1] === 'optgroup') { + array_pop($this->stack); + } + + /* An end tag token whose tag name is "option" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'option' + ) { + /* If the current node is an option element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if (end($this->stack)->nodeName === 'option') { + array_pop($this->stack); + } + + /* An end tag whose tag name is "select" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'select' + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // w/e + + /* Otherwise: */ + } else { + /* Pop elements from the stack of open elements until a select + element has been popped from the stack. */ + while (true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($current === 'select') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* A start tag whose tag name is "select" */ + } elseif ($token['name'] === 'select' && + $token['type'] === HTML5::STARTTAG + ) { + /* Parse error. Act as if the token had been an end tag with the + tag name "select" instead. */ + $this->inSelect( + array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + ) + ); + + /* An end tag whose tag name is one of: "caption", "table", "tbody", + "tfoot", "thead", "tr", "td", "th" */ + } elseif (in_array( + $token['name'], + array( + 'caption', + 'table', + 'tbody', + 'tfoot', + 'thead', + 'tr', + 'td', + 'th' + ) + ) && $token['type'] === HTML5::ENDTAG + ) { + /* Parse error. */ + // w/e + + /* If the stack of open elements has an element in table scope with + the same tag name as that of the token, then act as if an end tag + with the tag name "select" had been seen, and reprocess the token. + Otherwise, ignore the token. */ + if ($this->elementInScope($token['name'], true)) { + $this->inSelect( + array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + ) + ); + + $this->mainPhase($token); + } + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterBody($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Process the token as it would be processed if the insertion mode + was "in body". */ + $this->inBody($token); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the first element in the stack of open + elements (the html element), with the data attribute set to the + data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->stack[0]->appendChild($comment); + + /* An end tag with the tag name "html" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { + /* If the parser was originally created in order to handle the + setting of an element's innerHTML attribute, this is a parse error; + ignore the token. (The element will be an html element in this + case.) (innerHTML case) */ + + /* Otherwise, switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* Anything else */ + } else { + /* Parse error. Set the insertion mode to "in body" and reprocess + the token. */ + $this->mode = self::IN_BODY; + return $this->inBody($token); + } + } + + private function inFrameset($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag with the tag name "frameset" */ + } elseif ($token['name'] === 'frameset' && + $token['type'] === HTML5::STARTTAG + ) { + $this->insertElement($token); + + /* An end tag with the tag name "frameset" */ + } elseif ($token['name'] === 'frameset' && + $token['type'] === HTML5::ENDTAG + ) { + /* If the current node is the root html element, then this is a + parse error; ignore the token. (innerHTML case) */ + if (end($this->stack)->nodeName === 'html') { + // Ignore + + } else { + /* Otherwise, pop the current node from the stack of open + elements. */ + array_pop($this->stack); + + /* If the parser was not originally created in order to handle + the setting of an element's innerHTML attribute (innerHTML case), + and the current node is no longer a frameset element, then change + the insertion mode to "after frameset". */ + $this->mode = self::AFTR_FRAME; + } + + /* A start tag with the tag name "frame" */ + } elseif ($token['name'] === 'frame' && + $token['type'] === HTML5::STARTTAG + ) { + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + /* A start tag with the tag name "noframes" */ + } elseif ($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG + ) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterFrameset($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* An end tag with the tag name "html" */ + } elseif ($token['name'] === 'html' && + $token['type'] === HTML5::ENDTAG + ) { + /* Switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* A start tag with the tag name "noframes" */ + } elseif ($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG + ) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function trailingEndPhase($token) + { + /* After the main phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Process the token as it would be processed in the main phase. */ + $this->mainPhase($token); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or a start tag token. Or an end tag token. */ + } elseif (($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG + ) { + /* Parse error. Switch back to the main phase and reprocess the + token. */ + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + + /* An end-of-file token */ + } elseif ($token['type'] === HTML5::EOF) { + /* OMG DONE!! */ + } + } + + private function insertElement($token, $append = true, $check = false) + { + // Proprietary workaround for libxml2's limitations with tag names + if ($check) { + // Slightly modified HTML5 tag-name modification, + // removing anything that's not an ASCII letter, digit, or hyphen + $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); + // Remove leading hyphens and numbers + $token['name'] = ltrim($token['name'], '-0..9'); + // In theory, this should ever be needed, but just in case + if ($token['name'] === '') { + $token['name'] = 'span'; + } // arbitrary generic choice + } + + $el = $this->dom->createElement($token['name']); + + foreach ($token['attr'] as $attr) { + if (!$el->hasAttribute($attr['name'])) { + $el->setAttribute($attr['name'], $attr['value']); + } + } + + $this->appendToRealParent($el); + $this->stack[] = $el; + + return $el; + } + + private function insertText($data) + { + $text = $this->dom->createTextNode($data); + $this->appendToRealParent($text); + } + + private function insertComment($data) + { + $comment = $this->dom->createComment($data); + $this->appendToRealParent($comment); + } + + private function appendToRealParent($node) + { + if ($this->foster_parent === null) { + end($this->stack)->appendChild($node); + + } elseif ($this->foster_parent !== null) { + /* If the foster parent element is the parent element of the + last table element in the stack of open elements, then the new + node must be inserted immediately before the last table element + in the stack of open elements in the foster parent element; + otherwise, the new node must be appended to the foster parent + element. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === 'table' && + $this->stack[$n]->parentNode !== null + ) { + $table = $this->stack[$n]; + break; + } + } + + if (isset($table) && $this->foster_parent->isSameNode($table->parentNode)) { + $this->foster_parent->insertBefore($node, $table); + } else { + $this->foster_parent->appendChild($node); + } + + $this->foster_parent = null; + } + } + + private function elementInScope($el, $table = false) + { + if (is_array($el)) { + foreach ($el as $element) { + if ($this->elementInScope($element, $table)) { + return true; + } + } + + return false; + } + + $leng = count($this->stack); + + for ($n = 0; $n < $leng; $n++) { + /* 1. Initialise node to be the current node (the bottommost node of + the stack). */ + $node = $this->stack[$leng - 1 - $n]; + + if ($node->tagName === $el) { + /* 2. If node is the target node, terminate in a match state. */ + return true; + + } elseif ($node->tagName === 'table') { + /* 3. Otherwise, if node is a table element, terminate in a failure + state. */ + return false; + + } elseif ($table === true && in_array( + $node->tagName, + array( + 'caption', + 'td', + 'th', + 'button', + 'marquee', + 'object' + ) + ) + ) { + /* 4. Otherwise, if the algorithm is the "has an element in scope" + variant (rather than the "has an element in table scope" variant), + and node is one of the following, terminate in a failure state. */ + return false; + + } elseif ($node === $node->ownerDocument->documentElement) { + /* 5. Otherwise, if node is an html element (root element), terminate + in a failure state. (This can only happen if the node is the topmost + node of the stack of open elements, and prevents the next step from + being invoked if there are no more elements in the stack.) */ + return false; + } + + /* Otherwise, set node to the previous entry in the stack of open + elements and return to step 2. (This will never fail, since the loop + will always terminate in the previous step if the top of the stack + is reached.) */ + } + } + + private function reconstructActiveFormattingElements() + { + /* 1. If there are no entries in the list of active formatting elements, + then there is nothing to reconstruct; stop this algorithm. */ + $formatting_elements = count($this->a_formatting); + + if ($formatting_elements === 0) { + return false; + } + + /* 3. Let entry be the last (most recently added) element in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. If the last (most recently added) entry in the list of active + formatting elements is a marker, or if it is an element that is in the + stack of open elements, then there is nothing to reconstruct; stop this + algorithm. */ + if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { + return false; + } + + for ($a = $formatting_elements - 1; $a >= 0; true) { + /* 4. If there are no entries before entry in the list of active + formatting elements, then jump to step 8. */ + if ($a === 0) { + $step_seven = false; + break; + } + + /* 5. Let entry be the entry one earlier than entry in the list of + active formatting elements. */ + $a--; + $entry = $this->a_formatting[$a]; + + /* 6. If entry is neither a marker nor an element that is also in + thetack of open elements, go to step 4. */ + if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { + break; + } + } + + while (true) { + /* 7. Let entry be the element one later than entry in the list of + active formatting elements. */ + if (isset($step_seven) && $step_seven === true) { + $a++; + $entry = $this->a_formatting[$a]; + } + + /* 8. Perform a shallow clone of the element entry to obtain clone. */ + $clone = $entry->cloneNode(); + + /* 9. Append clone to the current node and push it onto the stack + of open elements so that it is the new current node. */ + end($this->stack)->appendChild($clone); + $this->stack[] = $clone; + + /* 10. Replace the entry for entry in the list with an entry for + clone. */ + $this->a_formatting[$a] = $clone; + + /* 11. If the entry for clone in the list of active formatting + elements is not the last entry in the list, return to step 7. */ + if (end($this->a_formatting) !== $clone) { + $step_seven = true; + } else { + break; + } + } + } + + private function clearTheActiveFormattingElementsUpToTheLastMarker() + { + /* When the steps below require the UA to clear the list of active + formatting elements up to the last marker, the UA must perform the + following steps: */ + + while (true) { + /* 1. Let entry be the last (most recently added) entry in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. Remove entry from the list of active formatting elements. */ + array_pop($this->a_formatting); + + /* 3. If entry was a marker, then stop the algorithm at this point. + The list has been cleared up to the last marker. */ + if ($entry === self::MARKER) { + break; + } + } + } + + private function generateImpliedEndTags($exclude = array()) + { + /* When the steps below require the UA to generate implied end tags, + then, if the current node is a dd element, a dt element, an li element, + a p element, a td element, a th element, or a tr element, the UA must + act as if an end tag with the respective tag name had been seen and + then generate implied end tags again. */ + $node = end($this->stack); + $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); + + while (in_array(end($this->stack)->nodeName, $elements)) { + array_pop($this->stack); + } + } + + private function getElementCategory($node) + { + $name = $node->tagName; + if (in_array($name, $this->special)) { + return self::SPECIAL; + } elseif (in_array($name, $this->scoping)) { + return self::SCOPING; + } elseif (in_array($name, $this->formatting)) { + return self::FORMATTING; + } else { + return self::PHRASING; + } + } + + private function clearStackToTableContext($elements) + { + /* When the steps above require the UA to clear the stack back to a + table context, it means that the UA must, while the current node is not + a table element or an html element, pop elements from the stack of open + elements. If this causes any elements to be popped from the stack, then + this is a parse error. */ + while (true) { + $node = end($this->stack)->nodeName; + + if (in_array($node, $elements)) { + break; + } else { + array_pop($this->stack); + } + } + } + + private function resetInsertionMode() + { + /* 1. Let last be false. */ + $last = false; + $leng = count($this->stack); + + for ($n = $leng - 1; $n >= 0; $n--) { + /* 2. Let node be the last node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 3. If node is the first node in the stack of open elements, then + set last to true. If the element whose innerHTML attribute is being + set is neither a td element nor a th element, then set node to the + element whose innerHTML attribute is being set. (innerHTML case) */ + if ($this->stack[0]->isSameNode($node)) { + $last = true; + } + + /* 4. If node is a select element, then switch the insertion mode to + "in select" and abort these steps. (innerHTML case) */ + if ($node->nodeName === 'select') { + $this->mode = self::IN_SELECT; + break; + + /* 5. If node is a td or th element, then switch the insertion mode + to "in cell" and abort these steps. */ + } elseif ($node->nodeName === 'td' || $node->nodeName === 'th') { + $this->mode = self::IN_CELL; + break; + + /* 6. If node is a tr element, then switch the insertion mode to + "in row" and abort these steps. */ + } elseif ($node->nodeName === 'tr') { + $this->mode = self::IN_ROW; + break; + + /* 7. If node is a tbody, thead, or tfoot element, then switch the + insertion mode to "in table body" and abort these steps. */ + } elseif (in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { + $this->mode = self::IN_TBODY; + break; + + /* 8. If node is a caption element, then switch the insertion mode + to "in caption" and abort these steps. */ + } elseif ($node->nodeName === 'caption') { + $this->mode = self::IN_CAPTION; + break; + + /* 9. If node is a colgroup element, then switch the insertion mode + to "in column group" and abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'colgroup') { + $this->mode = self::IN_CGROUP; + break; + + /* 10. If node is a table element, then switch the insertion mode + to "in table" and abort these steps. */ + } elseif ($node->nodeName === 'table') { + $this->mode = self::IN_TABLE; + break; + + /* 11. If node is a head element, then switch the insertion mode + to "in body" ("in body"! not "in head"!) and abort these steps. + (innerHTML case) */ + } elseif ($node->nodeName === 'head') { + $this->mode = self::IN_BODY; + break; + + /* 12. If node is a body element, then switch the insertion mode to + "in body" and abort these steps. */ + } elseif ($node->nodeName === 'body') { + $this->mode = self::IN_BODY; + break; + + /* 13. If node is a frameset element, then switch the insertion + mode to "in frameset" and abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'frameset') { + $this->mode = self::IN_FRAME; + break; + + /* 14. If node is an html element, then: if the head element + pointer is null, switch the insertion mode to "before head", + otherwise, switch the insertion mode to "after head". In either + case, abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'html') { + $this->mode = ($this->head_pointer === null) + ? self::BEFOR_HEAD + : self::AFTER_HEAD; + + break; + + /* 15. If last is true, then set the insertion mode to "in body" + and abort these steps. (innerHTML case) */ + } elseif ($last) { + $this->mode = self::IN_BODY; + break; + } + } + } + + private function closeCell() + { + /* If the stack of open elements has a td or th element in table scope, + then act as if an end tag token with that tag name had been seen. */ + foreach (array('td', 'th') as $cell) { + if ($this->elementInScope($cell, true)) { + $this->inCell( + array( + 'name' => $cell, + 'type' => HTML5::ENDTAG + ) + ); + + break; + } + } + } + + public function save() + { + return $this->dom; + } +} diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Printer.php b/lib/htmlpurifier/standalone/HTMLPurifier/Printer.php new file mode 100644 index 0000000..549e4ce --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Printer.php @@ -0,0 +1,218 @@ +getAll(); + $context = new HTMLPurifier_Context(); + $this->generator = new HTMLPurifier_Generator($config, $context); + } + + /** + * Main function that renders object or aspect of that object + * @note Parameters vary depending on printer + */ + // function render() {} + + /** + * Returns a start tag + * @param string $tag Tag name + * @param array $attr Attribute array + * @return string + */ + protected function start($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) + ); + } + + /** + * Returns an end tag + * @param string $tag Tag name + * @return string + */ + protected function end($tag) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_End($tag) + ); + } + + /** + * Prints a complete element with content inside + * @param string $tag Tag name + * @param string $contents Element contents + * @param array $attr Tag attributes + * @param bool $escape whether or not to escape contents + * @return string + */ + protected function element($tag, $contents, $attr = array(), $escape = true) + { + return $this->start($tag, $attr) . + ($escape ? $this->escape($contents) : $contents) . + $this->end($tag); + } + + /** + * @param string $tag + * @param array $attr + * @return string + */ + protected function elementEmpty($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Empty($tag, $attr) + ); + } + + /** + * @param string $text + * @return string + */ + protected function text($text) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Text($text) + ); + } + + /** + * Prints a simple key/value row in a table. + * @param string $name Key + * @param mixed $value Value + * @return string + */ + protected function row($name, $value) + { + if (is_bool($value)) { + $value = $value ? 'On' : 'Off'; + } + return + $this->start('tr') . "\n" . + $this->element('th', $name) . "\n" . + $this->element('td', $value) . "\n" . + $this->end('tr'); + } + + /** + * Escapes a string for HTML output. + * @param string $string String to escape + * @return string + */ + protected function escape($string) + { + $string = HTMLPurifier_Encoder::cleanUTF8($string); + $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); + return $string; + } + + /** + * Takes a list of strings and turns them into a single list + * @param string[] $array List of strings + * @param bool $polite Bool whether or not to add an end before the last + * @return string + */ + protected function listify($array, $polite = false) + { + if (empty($array)) { + return 'None'; + } + $ret = ''; + $i = count($array); + foreach ($array as $value) { + $i--; + $ret .= $value; + if ($i > 0 && !($polite && $i == 1)) { + $ret .= ', '; + } + if ($polite && $i == 1) { + $ret .= 'and '; + } + } + return $ret; + } + + /** + * Retrieves the class of an object without prefixes, as well as metadata + * @param object $obj Object to determine class of + * @param string $sec_prefix Further prefix to remove + * @return string + */ + protected function getClass($obj, $sec_prefix = '') + { + static $five = null; + if ($five === null) { + $five = version_compare(PHP_VERSION, '5', '>='); + } + $prefix = 'HTMLPurifier_' . $sec_prefix; + if (!$five) { + $prefix = strtolower($prefix); + } + $class = str_replace($prefix, '', get_class($obj)); + $lclass = strtolower($class); + $class .= '('; + switch ($lclass) { + case 'enum': + $values = array(); + foreach ($obj->valid_values as $value => $bool) { + $values[] = $value; + } + $class .= implode(', ', $values); + break; + case 'css_composite': + $values = array(); + foreach ($obj->defs as $def) { + $values[] = $this->getClass($def, $sec_prefix); + } + $class .= implode(', ', $values); + break; + case 'css_multiple': + $class .= $this->getClass($obj->single, $sec_prefix) . ', '; + $class .= $obj->max; + break; + case 'css_denyelementdecorator': + $class .= $this->getClass($obj->def, $sec_prefix) . ', '; + $class .= $obj->element; + break; + case 'css_importantdecorator': + $class .= $this->getClass($obj->def, $sec_prefix); + if ($obj->allow) { + $class .= ', !important'; + } + break; + } + $class .= ')'; + return $class; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Printer/CSSDefinition.php b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/CSSDefinition.php new file mode 100644 index 0000000..29505fe --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/CSSDefinition.php @@ -0,0 +1,44 @@ +def = $config->getCSSDefinition(); + $ret = ''; + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + $ret .= $this->start('table'); + + $ret .= $this->element('caption', 'Properties ($info)'); + + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Property', array('class' => 'heavy')); + $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + + ksort($this->def->info); + foreach ($this->def->info as $property => $obj) { + $name = $this->getClass($obj, 'AttrDef_'); + $ret .= $this->row($property, $name); + } + + $ret .= $this->end('table'); + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.css b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.css new file mode 100644 index 0000000..3ff1a88 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.css @@ -0,0 +1,10 @@ + +.hp-config {} + +.hp-config tbody th {text-align:right; padding-right:0.5em;} +.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;} +.hp-config .namespace th {text-align:center;} +.hp-config .verbose {display:none;} +.hp-config .controls {text-align:center;} + +/* vim: et sw=4 sts=4 */ diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.js b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.js new file mode 100644 index 0000000..cba00c9 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.js @@ -0,0 +1,5 @@ +function toggleWriteability(id_of_patient, checked) { + document.getElementById(id_of_patient).disabled = checked; +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.php b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.php new file mode 100644 index 0000000..36100ce --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/ConfigForm.php @@ -0,0 +1,447 @@ +docURL = $doc_url; + $this->name = $name; + $this->compress = $compress; + // initialize sub-printers + $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); + $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); + } + + /** + * Sets default column and row size for textareas in sub-printers + * @param $cols Integer columns of textarea, null to use default + * @param $rows Integer rows of textarea, null to use default + */ + public function setTextareaDimensions($cols = null, $rows = null) + { + if ($cols) { + $this->fields['default']->cols = $cols; + } + if ($rows) { + $this->fields['default']->rows = $rows; + } + } + + /** + * Retrieves styling, in case it is not accessible by webserver + */ + public static function getCSS() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); + } + + /** + * Retrieves JavaScript, in case it is not accessible by webserver + */ + public static function getJavaScript() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); + } + + /** + * Returns HTML output for a configuration form + * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array + * where [0] has an HTML namespace and [1] is being rendered. + * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. + * @param bool $render_controls + * @return string + */ + public function render($config, $allowed = true, $render_controls = true) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + + $this->config = $config; + $this->genConfig = $gen_config; + $this->prepareGenerator($gen_config); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); + $all = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $all[$ns][$directive] = $config->get($ns . '.' . $directive); + } + + $ret = ''; + $ret .= $this->start('table', array('class' => 'hp-config')); + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); + $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + foreach ($all as $ns => $directives) { + $ret .= $this->renderNamespace($ns, $directives); + } + if ($render_controls) { + $ret .= $this->start('tbody'); + $ret .= $this->start('tr'); + $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); + $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); + $ret .= '[Reset]'; + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a single namespace + * @param $ns String namespace name + * @param array $directives array of directives to values + * @return string + */ + protected function renderNamespace($ns, $directives) + { + $ret = ''; + $ret .= $this->start('tbody', array('class' => 'namespace')); + $ret .= $this->start('tr'); + $ret .= $this->element('th', $ns, array('colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + $ret .= $this->start('tbody'); + foreach ($directives as $directive => $value) { + $ret .= $this->start('tr'); + $ret .= $this->start('th'); + if ($this->docURL) { + $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); + $ret .= $this->start('a', array('href' => $url)); + } + $attr = array('for' => "{$this->name}:$ns.$directive"); + + // crop directive name if it's too long + if (!$this->compress || (strlen($directive) < $this->compress)) { + $directive_disp = $directive; + } else { + $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; + $attr['title'] = $directive; + } + + $ret .= $this->element( + 'label', + $directive_disp, + // component printers must create an element with this id + $attr + ); + if ($this->docURL) { + $ret .= $this->end('a'); + } + $ret .= $this->end('th'); + + $ret .= $this->start('td'); + $def = $this->config->def->info["$ns.$directive"]; + if (is_int($def)) { + $allow_null = $def < 0; + $type = abs($def); + } else { + $type = $def->type; + $allow_null = isset($def->allow_null); + } + if (!isset($this->fields[$type])) { + $type = 0; + } // default + $type_obj = $this->fields[$type]; + if ($allow_null) { + $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); + } + $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + } + $ret .= $this->end('tbody'); + return $ret; + } + +} + +/** + * Printer decorator for directives that accept null + */ +class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer +{ + /** + * Printer being decorated + * @type HTMLPurifier_Printer + */ + protected $obj; + + /** + * @param HTMLPurifier_Printer $obj Printer to decorate + */ + public function __construct($obj) + { + parent::__construct(); + $this->obj = $obj; + } + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + + $ret = ''; + $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Null/Disabled'); + $ret .= $this->end('label'); + $attr = array( + 'type' => 'checkbox', + 'value' => '1', + 'class' => 'null-toggle', + 'name' => "$name" . "[Null_$ns.$directive]", + 'id' => "$name:Null_$ns.$directive", + 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! + ); + if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { + // modify inline javascript slightly + $attr['onclick'] = + "toggleWriteability('$name:Yes_$ns.$directive',checked);" . + "toggleWriteability('$name:No_$ns.$directive',checked)"; + } + if ($value === null) { + $attr['checked'] = 'checked'; + } + $ret .= $this->elementEmpty('input', $attr); + $ret .= $this->text(' or '); + $ret .= $this->elementEmpty('br'); + $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); + return $ret; + } +} + +/** + * Swiss-army knife configuration form field printer + */ +class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer +{ + /** + * @type int + */ + public $cols = 18; + + /** + * @type int + */ + public $rows = 5; + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + // this should probably be split up a little + $ret = ''; + $def = $config->def->info["$ns.$directive"]; + if (is_int($def)) { + $type = abs($def); + } else { + $type = $def->type; + } + if (is_array($value)) { + switch ($type) { + case HTMLPurifier_VarParser::LOOKUP: + $array = $value; + $value = array(); + foreach ($array as $val => $b) { + $value[] = $val; + } + //TODO does this need a break? + case HTMLPurifier_VarParser::ALIST: + $value = implode(PHP_EOL, $value); + break; + case HTMLPurifier_VarParser::HASH: + $nvalue = ''; + foreach ($value as $i => $v) { + $nvalue .= "$i:$v" . PHP_EOL; + } + $value = $nvalue; + break; + default: + $value = ''; + } + } + if ($type === HTMLPurifier_VarParser::MIXED) { + return 'Not supported'; + $value = serialize($value); + } + $attr = array( + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:$ns.$directive" + ); + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + if (isset($def->allowed)) { + $ret .= $this->start('select', $attr); + foreach ($def->allowed as $val => $b) { + $attr = array(); + if ($value == $val) { + $attr['selected'] = 'selected'; + } + $ret .= $this->element('option', $val, $attr); + } + $ret .= $this->end('select'); + } elseif ($type === HTMLPurifier_VarParser::TEXT || + $type === HTMLPurifier_VarParser::ITEXT || + $type === HTMLPurifier_VarParser::ALIST || + $type === HTMLPurifier_VarParser::HASH || + $type === HTMLPurifier_VarParser::LOOKUP) { + $attr['cols'] = $this->cols; + $attr['rows'] = $this->rows; + $ret .= $this->start('textarea', $attr); + $ret .= $this->text($value); + $ret .= $this->end('textarea'); + } else { + $attr['value'] = $value; + $attr['type'] = 'text'; + $ret .= $this->elementEmpty('input', $attr); + } + return $ret; + } +} + +/** + * Bool form field printer + */ +class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer +{ + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + $ret = ''; + $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); + + $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Yes'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:Yes_$ns.$directive", + 'value' => '1' + ); + if ($value === true) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' No'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:No_$ns.$directive", + 'value' => '0' + ); + if ($value === false) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/standalone/HTMLPurifier/Printer/HTMLDefinition.php b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/HTMLDefinition.php new file mode 100644 index 0000000..5f2f2f8 --- /dev/null +++ b/lib/htmlpurifier/standalone/HTMLPurifier/Printer/HTMLDefinition.php @@ -0,0 +1,324 @@ +config =& $config; + + $this->def = $config->getHTMLDefinition(); + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + + $ret .= $this->renderDoctype(); + $ret .= $this->renderEnvironment(); + $ret .= $this->renderContentSets(); + $ret .= $this->renderInfo(); + + $ret .= $this->end('div'); + + return $ret; + } + + /** + * Renders the Doctype table + * @return string + */ + protected function renderDoctype() + { + $doctype = $this->def->doctype; + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Doctype'); + $ret .= $this->row('Name', $doctype->name); + $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); + $ret .= $this->row('Default Modules', implode($doctype->modules, ', ')); + $ret .= $this->row('Default Tidy Modules', implode($doctype->tidyModules, ', ')); + $ret .= $this->end('table'); + return $ret; + } + + + /** + * Renders environment table, which is miscellaneous info + * @return string + */ + protected function renderEnvironment() + { + $def = $this->def; + + $ret = ''; + + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Environment'); + + $ret .= $this->row('Parent of fragment', $def->info_parent); + $ret .= $this->renderChildren($def->info_parent_def->child); + $ret .= $this->row('Block wrap name', $def->info_block_wrapper); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Global attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Tag transforms'); + $list = array(); + foreach ($def->info_tag_transform as $old => $new) { + $new = $this->getClass($new, 'TagTransform_'); + $list[] = "<$old> with $new"; + } + $ret .= $this->element('td', $this->listify($list)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); + $ret .= $this->end('tr'); + + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Content Sets table + * @return string + */ + protected function renderContentSets() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Content Sets'); + foreach ($this->def->info_content_sets as $name => $lookup) { + $ret .= $this->heavyHeader($name); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($lookup)); + $ret .= $this->end('tr'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Elements ($info) table + * @return string + */ + protected function renderInfo() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Elements ($info)'); + ksort($this->def->info); + $ret .= $this->heavyHeader('Allowed tags', 2); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); + $ret .= $this->end('tr'); + foreach ($this->def->info as $name => $def) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Inline content'); + $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); + $ret .= $this->end('tr'); + if (!empty($def->excludes)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Excludes'); + $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_pre)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_post)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); + $ret .= $this->end('tr'); + } + if (!empty($def->auto_close)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Auto closed by'); + $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); + $ret .= $this->end('tr'); + } + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Allowed attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); + $ret .= $this->end('tr'); + + if (!empty($def->required_attr)) { + $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); + } + + $ret .= $this->renderChildren($def->child); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a row describing the allowed children of an element + * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element + * @return string + */ + protected function renderChildren($def) + { + $context = new HTMLPurifier_Context(); + $ret = ''; + $ret .= $this->start('tr'); + $elements = array(); + $attr = array(); + if (isset($def->elements)) { + if ($def->type == 'strictblockquote') { + $def->validateChildren(array(), $this->config, $context); + } + $elements = $def->elements; + } + if ($def->type == 'chameleon') { + $attr['rowspan'] = 2; + } elseif ($def->type == 'empty') { + $elements = array(); + } elseif ($def->type == 'table') { + $elements = array_flip( + array( + 'col', + 'caption', + 'colgroup', + 'thead', + 'tfoot', + 'tbody', + 'tr' + ) + ); + } + $ret .= $this->element('th', 'Allowed children', $attr); + + if ($def->type == 'chameleon') { + + $ret .= $this->element( + 'td', + 'Block: ' . + $this->escape($this->listifyTagLookup($def->block->elements)), + null, + 0 + ); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element( + 'td', + 'Inline: ' . + $this->escape($this->listifyTagLookup($def->inline->elements)), + null, + 0 + ); + + } elseif ($def->type == 'custom') { + + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $def->dtd_regex + ); + + } else { + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $this->escape($this->listifyTagLookup($elements)), + null, + 0 + ); + } + $ret .= $this->end('tr'); + return $ret; + } + + /** + * Listifies a tag lookup table. + * @param array $array Tag lookup array in form of array('tagname' => true) + * @return string + */ + protected function listifyTagLookup($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $discard) { + if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { + continue; + } + $list[] = $name; + } + return $this->listify($list); + } + + /** + * Listifies a list of objects by retrieving class names and internal state + * @param array $array List of objects + * @return string + * @todo Also add information about internal state + */ + protected function listifyObjectList($array) + { + ksort($array); + $list = array(); + foreach ($array as $obj) { + $list[] = $this->getClass($obj, 'AttrTransform_'); + } + return $this->listify($list); + } + + /** + * Listifies a hash of attributes to AttrDef classes + * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) + * @return string + */ + protected function listifyAttr($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $obj) { + if ($obj === false) { + continue; + } + $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; + } + return $this->listify($list); + } + + /** + * Creates a heavy header row + * @param string $text + * @param int $num + * @return string + */ + protected function heavyHeader($text, $num = 1) + { + $ret = ''; + $ret .= $this->start('tr'); + $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); + $ret .= $this->end('tr'); + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/ini.php b/lib/ini.php new file mode 100644 index 0000000..0498556 --- /dev/null +++ b/lib/ini.php @@ -0,0 +1,124 @@ + $v ) { + $constants[ $k ] = $v; // update the constants array + } +} + +function load_ini( $filename ) +{ + global $constants, $INI_PATTERN, $loaded_files; + $file = file_get_contents( $filename ); + preg_match_all( $INI_PATTERN, $file, $matches, PREG_SET_ORDER ); + + foreach( $matches as $match ) { + // replace backslash-newlines with newlines + $match[ 2 ] = preg_replace( '|\\\\[\r\n]+|', "\n", $match[ 2 ] ); + $match[ 2 ] = ltrim( $match[ 2 ] ); + + + $constants[ $match[ 1 ] ] = $match[ 2 ]; // update the constants array + } + + $loaded_files[$filename] = 1; +} + +function special_callback( $matches ) +{ + global $constants; + $val = str_replace( "!file ", "", $matches[ 1 ], $is_file ); + if( $is_file ) { + return @file_get_contents( $val ); + } + $val = str_replace( "!rand ", "", $matches[ 1 ], $is_rand ); + if( $is_rand ) { + $choices = explode( ",", $val ); + $val = array_rand( array_flip( $choices ) ); + $val = trim( $val ); + + return $val; + } + + return evaluate( $constants[ $val ] ); +} + +function evaluate( $val ) +{ + if( $val === 'yes' ) { + $val = true; + settype($val, "bool"); + } else if( $val === 'no' ) { + $val = false; + settype($val, "bool"); + } else { + $val = preg_replace_callback( '|\{\{(.*?)\}\}|', "special_callback", $val ); + + // for numeric values, try to keep the PHP internal type as 'int' + if (ctype_digit($val)) { + settype($val, "int"); + } + } + + return $val; +} + +function finalize_constants() +{ + global $constants; + + $c2 = array(); + foreach( $constants as $key => $val ) { + $val = evaluate( $val ); + $c2[ $key ] = $val; + if( !defined( $key ) ) { + define( $key, $val ); + } + } + $constants = $c2; +} + +$constants = array(); +$loaded_files = array(); + +// quick wrapper function +function load_ini_file( $filename ) +{ + global $loaded_files, $configdir, $constants; + if( strpos( $filename, '/' ) !== 0 ) $filename = "$configdir/$filename"; + + if( in_array( $filename, $loaded_files ) ) return; + + $constants = array(); + load_ini($filename); + finalize_constants(); +} diff --git a/lib/json.php b/lib/json.php new file mode 100644 index 0000000..4f0512f --- /dev/null +++ b/lib/json.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/lib/like_score-test.php b/lib/like_score-test.php new file mode 100644 index 0000000..21e90d1 --- /dev/null +++ b/lib/like_score-test.php @@ -0,0 +1,671 @@ += LIKE_MAX_LIKES) { + die("0\nYou can't like this post anymore."); + } + + $email = "$user_score.$post_likes"; + + $query = "UPDATE `$board` SET email = '$email' WHERE no = $post_id LIMIT 1"; + + $res = mysql_board_call($query); + + if (!$res) { + die("0\nDatabase Error (luls1)."); + } + + // --- + + $skip_boards = array('b', 'qa', 's4s', 'bant', 'vip'); + + if (in_array($board, $skip_boards)) { + return $post_likes; + } + + // Update user score + $query = << DATE_SUB(NOW(), INTERVAL $cd SECOND) +LIMIT 1 +SQL; + + if ($or_clause) { + $res = mysql_global_call($query, $user_id, $ip); + } + else { + $res = mysql_global_call($query, $user_id); + } + + if (!$res) { + die("0\nDabase Error (lia0)"); + } + + if (mysql_num_rows($res)) { + return true; + } + + // Rangebans + if (!$pass_user) { + $long_ip = ip2long($ip); + + if ($long_ip) { + if (isIPRangeBanned($long_ip)) { + return true; + } + } + } + + // IP cycling + /* + $user_mask = explode('.', $ip); + + $user_mask = ((int)$user_mask[0]) . '.' . ((int)$user_mask[1]); + + $query = << DATE_SUB(NOW(), INTERVAL 10 MINUTE) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + die("0\nDabase Error (lia1)"); + } + + $row = mysql_fetch_row($res); + + if ($row && (int)$row[0] > 10) { + return true; + } + */ + // Proxies + /* + $query = << DATE_SUB(NOW(), INTERVAL 5 MINUTE) +SQL; + + $res = mysql_global_call($query, $target_user_id); + + if (!$res) { + die("0\nDabase Error (lia1)"); + } + + $row = mysql_fetch_row($res); + + if ($row && (int)$row[0] > 10) { + return true; + } + */ + return false; +} + +function like_get_target_user_id($board, $post_id) { + // board and post_id params should already be escaped + $query = "SELECT host, 4pass_id FROM `$board` WHERE no = $post_id AND resto > 0 AND archived = 0 LIMIT 1"; + + $res = mysql_board_call($query); + + if (!$res) { + die("0\nDabase Error (lgtu)"); + } + + $row = mysql_fetch_row($res); + + if (!$row) { + return false; + } + + if ($row[1]) { + return array($row[1], $row[0]); + } + else if ($row[0]) { + return array($row[0], $row[0]); + } + + return false; +} + +function like_is_duplicate($user_id, $board, $post_id) { + // board and post_id params should already be escaped + $query = "SELECT id FROM like_user_log WHERE user_id = '%s' AND board = '$board' AND post_id = $post_id LIMIT 1"; + + $res = mysql_global_call($query, $user_id); + + if (!$res) { + die("0\nDabase Error (lid)"); + } + + return mysql_num_rows($res) === 1; +} + +function like_is_ip_suspicious($ip) { + $bot_countries = array( + 'AD','AE','AF','AG','AI','AL','AM','AN','AO','AR','AS','AW','AZ', + 'BB','BD','BF','BG','BH','BI','BJ','BM','BN','BO','BR','BS','BT','BV','BW','BY','BZ', + 'CC','CF','CG','CI','CK','CL','CM','CN','CO','CR','CU','CV','CX','CY','CZ', + 'DJ','DM','DO','DZ','EC','EE','EG','EH','ER','ET','FJ','FM','FO', + 'GA','GD','GE','GF','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GY', + 'HK','HM','HN','HT','HU','HR','ID','IL','IN','IO','IQ','IR','IS','JM','JO','JP', + 'KE','KG','KH','KI','KM','KN','KR','KW','KY','KZ', + 'LA','LB','LC','LI','LK','LR','LS','LU','LY', + 'MA','MD','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MY','MZ','NA', + 'NE','NF','NG','NI','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PM','PN','PR','PS','PT','PW', + 'QA','RE','RS','RO','RU','RW','SA','SB','SC','SD','SH','SI','SJ','SK','SL','SM','SN','SO','SR','ST','SV','SY','SZ', + 'TC','TD','TF','TG','TJ','TM','TN','TO','TP','TR','TT','TV','TW','TZ','UG','UM','UY','UZ', + 'VA','VE','VC','VG','VI','VN','VU','WF','WS','YE','YT','ZA','ZM','ZR','ZW' + ); + + if (!isset($_COOKIE['__cfduid'])) { + return true; + } + + $country = geoip_country_code_by_addr($ip); + + if (!$country) { + return true; + } + + if (in_array($country, $bot_countries)) { + return true; + } + + return false; +} + +function like_is_ip_known($ip) { + $query = "SELECT id FROM like_user_log WHERE user_id = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $ip); + + if (!$res) { + die("0\nDabase Error (lik)"); + } + + return mysql_num_rows($res) === 1; +} + +function like_get_user_score($no_cache = false) { + global $captcha_bypass, $passid; + + // FIXME + return 0; + + static $current_score = -1; + + if ($current_score !== -1 && $no_cache !== true) { + return $current_score; + } + + if ($captcha_bypass && $passid) { + $user_id = $passid; + } + else { + $user_id = $_SERVER['REMOTE_ADDR']; + } + + $query = "SELECT user_score FROM like_user_scores WHERE user_id = '%s'"; + + $res = mysql_global_call($query, $user_id); + + if (!$res) { + return 0; + } + + $row = mysql_fetch_row($res); + + if ($row) { + $current_score = (int)$row[0]; + } + else { + $current_score = 0; + } + + return $current_score; +} + +function like_decrease_user_score($ip, $passid, $multiplier) { + if ($passid) { + $user_id = $passid; + } + else { + $user_id = $ip; + } + + $query = << 0, + 'smiley' => 0, // single emoji + 'sad' => 0, // single emoji + //'coinflip' => 0, + //'dice+1d6' => 0, + 'ok' => 0, // single emoji + 'animal' => 0, // random animal emoji + 'food' => 0, // random food emoji + 'check' => 0, + 'cross' => 0, + //'nofile' => 0, + //'card' => 0, // random playing card emoji + //'wflag' => 0, + //'bflag' => 0, + 'like' => 0, // red heart emoji + //'rabbit' => 0, // single emoji + 'unlove' => 0, // broken heart emoji + 'rage' => 0, + 'perfect' => 0, + //'fortune' => 0, + //'dice+1d100' => 0, + //'bricks' => 0, + 'onsen' => 0, + //'party' => 0, // partyhat image + //'verified' => 0, + //'partyhat' => 0, // partyhat image, adjusted + //'pickle' => 0, // pickle rick image + //'trash' => 0, // trashcan image + 'heart' => 0, // random heart emoji (different colors) + //'santa' => 0, // santa hat image + 'joy' => 0, // single emoji + //'marquee' => 0, + 'pig' => 0, // single emoji + 'dog' => 0, // single emoji + 'cat' => 0, // single emoji + 'rainbow' => 0, + 'frog' => 0, // single emoji + //'dino' => 750, // dinosaur gif from /fit/ + //'spooky' => 1000, // random skeleton + ); + + $perks = array(); + + if ($only_unlocked) { + foreach ($req_points as $perk => $score) { + if ($current_score >= $score) { + $perks[] = $perk; + } + } + } + else { + foreach ($req_points as $perk => $score) { + $perks[$perk] = $current_score >= $score; + } + } + + return $perks; +} + +function like_parse_options_field($options) { + $show_score = false; + $active_perk = null; + + if (strlen($options) > 100) { + return array($show_score, $active_perk); + } + + $user_perks = like_get_perks_state(); + + $opts = explode(' ', $options); + + foreach ($opts as $opt) { + if ($user_perks[$opt] === true) { + if ($opt === 'showscore') { + $show_score = true; + } + else { + $active_perk = $opt; + break; + } + } + } + + return array($show_score, $active_perk); +} + +function like_build_perk_html($perk) { + $cnt_attrs = ''; + + switch ($perk) { + case 'animal': + $ary = array('🐭','🐹','🐰','🐶','🐺','🦊','🐵','🐸','🙈','🙉','🙊','🐯','🦁','🦓','🦒','🐴','🐮','🐷','🐻','🐼','🐲','🦄','🐱','😸','😹','😺','😻','😼','😽','😾','😿','🙀','🐅','🐆','🐘','🦏','🐂','🐃','🐄','🐎','🦌','🐐','🐏','🐑','🐖','🐗','🐪','🐫','🦍','🐉','🦖','🦕','🐈','🐀','🐁','🐇','🐒','🐕','🐩','🐨','🐿','🦔','🦇','🐍','🦅','🦉','🦆','🐓','🐔','🦃','🕊','🐣','🐤','🐥','🐦','🐧','🐋','🐳','🐬','🦈','🐟','🐠','🐡','🐙','🦑','🦐','🦀','🐚','🐌','🐢','🦎','🐊','🏇','🎠','♘','♞','🐽','🐾','👣','🐀','🐃','🐅','🐇','🐉','🐍','🐎','🐐','🐒','🐓','🐕','🐖'); + $html = $ary[array_rand($ary)]; + break; + case 'food': + $ary = array('🧀','🥚','🍳','🥞','🍠','🍞','🥐','🥖','🥨','🍔','🍕','🍝','🍟','🍤','🌭','🌮','🌯','🍛','🥙','🥘','🥗','🥪','🥫','🥓','🍖','🍗','🥩','🥢','🥡','🥟','🍚','🍜','🍲','🥠','🍘','🍙','🍣','🍥','🍱','🍡','🍢','🍇','🍈','🍉','🍊','🍋','🍌','🍍','🍎','🍏','🍐','🍑','🍒','🍓','🥝','🥥','🥦','🍄','🍅','🍆','🌶','🥑','🥕','🥒','🥔','🥜','🍰','🎂','🥧','🍨','🍦','🍩','🍪','🍿','🍮','🍯','🍧','🍫','🍬','🍭','🍺','🍻','🍷','🍸','🍹','🍶','🥂','🥃','🍾','☕','🍵','🥛','🍼','🥤','🍴','🍽','🥣','🥄'); + $html = $ary[array_rand($ary)]; + break; + case 'marquee': + $ary = array('🦖','⚽','🏀','⚾'); + $ico = $ary[array_rand($ary)]; + $html = << +$ico + +HTML; + break; + case 'rainbow': + $html = '🌈'; + break; + case 'wflag': + $html = '🏳️'; + break; + case 'bflag': + $html = '🏴'; + break; + case 'onsen': + $html = '♨️'; + break; + case 'rage': + $html = '💢'; + break; + case 'perfect': + $html = '💯'; + break; + case 'check': + $html = '✔️'; + break; + case 'cross': + $html = '❌'; + break; + case 'heart': + $ary = array('❤️','💙','💜','💛','🖤','💚'); + $html = $ary[array_rand($ary)]; + break; + case 'card': + $ary = array('♠️','♥️','♦️','♣️'); + $html = $ary[array_rand($ary)]; + break; + case 'like': + $html = '❤️'; + break; + case 'unlove': + $html = '💔'; + break; + case 'smiley': + $html = '😃'; + break; + case 'sad': + $html = '🙁'; + break; + case 'ok': + $html = '👌'; + break; + case 'coinflip': + $html = 'Coin Flip: ' . (mt_rand(0, 1) === 1 ? 'Heads' : 'Tails') . ''; + break; + case 'party': + $html = ''; + break; + case 'partyhat': + $cnt_attrs = ' style="position:absolute"'; + $html = ''; + break; + case 'pickle': + $html = ''; + break; + case 'nofile': + $html = ''; + break; + case 'trash': + $html = ''; + break; + case 'bricks': + $html = ''; + break; + case 'pig': + $html = '🐷'; + break; + case 'santa': + $html = ''; + break; + case 'verified': + $html = '
        '; + break; + case 'joy': + $html = '😂'; + break; + case 'rabbit': + $html = '🐰'; + break; + case 'frog': + $html = '🐸'; + break; + case 'dog': + $html = '🐶'; + break; + case 'cat': + $html = '🐱'; + break; + case 'dino': + $html = ''; + break; + case 'spooky': + $id = mt_rand(1, 23); + $html = ''; + break; + default: + return null; + break; + } + + return '
        '; +} diff --git a/lib/like_score.php b/lib/like_score.php new file mode 100644 index 0000000..21e90d1 --- /dev/null +++ b/lib/like_score.php @@ -0,0 +1,671 @@ += LIKE_MAX_LIKES) { + die("0\nYou can't like this post anymore."); + } + + $email = "$user_score.$post_likes"; + + $query = "UPDATE `$board` SET email = '$email' WHERE no = $post_id LIMIT 1"; + + $res = mysql_board_call($query); + + if (!$res) { + die("0\nDatabase Error (luls1)."); + } + + // --- + + $skip_boards = array('b', 'qa', 's4s', 'bant', 'vip'); + + if (in_array($board, $skip_boards)) { + return $post_likes; + } + + // Update user score + $query = << DATE_SUB(NOW(), INTERVAL $cd SECOND) +LIMIT 1 +SQL; + + if ($or_clause) { + $res = mysql_global_call($query, $user_id, $ip); + } + else { + $res = mysql_global_call($query, $user_id); + } + + if (!$res) { + die("0\nDabase Error (lia0)"); + } + + if (mysql_num_rows($res)) { + return true; + } + + // Rangebans + if (!$pass_user) { + $long_ip = ip2long($ip); + + if ($long_ip) { + if (isIPRangeBanned($long_ip)) { + return true; + } + } + } + + // IP cycling + /* + $user_mask = explode('.', $ip); + + $user_mask = ((int)$user_mask[0]) . '.' . ((int)$user_mask[1]); + + $query = << DATE_SUB(NOW(), INTERVAL 10 MINUTE) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + die("0\nDabase Error (lia1)"); + } + + $row = mysql_fetch_row($res); + + if ($row && (int)$row[0] > 10) { + return true; + } + */ + // Proxies + /* + $query = << DATE_SUB(NOW(), INTERVAL 5 MINUTE) +SQL; + + $res = mysql_global_call($query, $target_user_id); + + if (!$res) { + die("0\nDabase Error (lia1)"); + } + + $row = mysql_fetch_row($res); + + if ($row && (int)$row[0] > 10) { + return true; + } + */ + return false; +} + +function like_get_target_user_id($board, $post_id) { + // board and post_id params should already be escaped + $query = "SELECT host, 4pass_id FROM `$board` WHERE no = $post_id AND resto > 0 AND archived = 0 LIMIT 1"; + + $res = mysql_board_call($query); + + if (!$res) { + die("0\nDabase Error (lgtu)"); + } + + $row = mysql_fetch_row($res); + + if (!$row) { + return false; + } + + if ($row[1]) { + return array($row[1], $row[0]); + } + else if ($row[0]) { + return array($row[0], $row[0]); + } + + return false; +} + +function like_is_duplicate($user_id, $board, $post_id) { + // board and post_id params should already be escaped + $query = "SELECT id FROM like_user_log WHERE user_id = '%s' AND board = '$board' AND post_id = $post_id LIMIT 1"; + + $res = mysql_global_call($query, $user_id); + + if (!$res) { + die("0\nDabase Error (lid)"); + } + + return mysql_num_rows($res) === 1; +} + +function like_is_ip_suspicious($ip) { + $bot_countries = array( + 'AD','AE','AF','AG','AI','AL','AM','AN','AO','AR','AS','AW','AZ', + 'BB','BD','BF','BG','BH','BI','BJ','BM','BN','BO','BR','BS','BT','BV','BW','BY','BZ', + 'CC','CF','CG','CI','CK','CL','CM','CN','CO','CR','CU','CV','CX','CY','CZ', + 'DJ','DM','DO','DZ','EC','EE','EG','EH','ER','ET','FJ','FM','FO', + 'GA','GD','GE','GF','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GY', + 'HK','HM','HN','HT','HU','HR','ID','IL','IN','IO','IQ','IR','IS','JM','JO','JP', + 'KE','KG','KH','KI','KM','KN','KR','KW','KY','KZ', + 'LA','LB','LC','LI','LK','LR','LS','LU','LY', + 'MA','MD','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MY','MZ','NA', + 'NE','NF','NG','NI','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PM','PN','PR','PS','PT','PW', + 'QA','RE','RS','RO','RU','RW','SA','SB','SC','SD','SH','SI','SJ','SK','SL','SM','SN','SO','SR','ST','SV','SY','SZ', + 'TC','TD','TF','TG','TJ','TM','TN','TO','TP','TR','TT','TV','TW','TZ','UG','UM','UY','UZ', + 'VA','VE','VC','VG','VI','VN','VU','WF','WS','YE','YT','ZA','ZM','ZR','ZW' + ); + + if (!isset($_COOKIE['__cfduid'])) { + return true; + } + + $country = geoip_country_code_by_addr($ip); + + if (!$country) { + return true; + } + + if (in_array($country, $bot_countries)) { + return true; + } + + return false; +} + +function like_is_ip_known($ip) { + $query = "SELECT id FROM like_user_log WHERE user_id = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $ip); + + if (!$res) { + die("0\nDabase Error (lik)"); + } + + return mysql_num_rows($res) === 1; +} + +function like_get_user_score($no_cache = false) { + global $captcha_bypass, $passid; + + // FIXME + return 0; + + static $current_score = -1; + + if ($current_score !== -1 && $no_cache !== true) { + return $current_score; + } + + if ($captcha_bypass && $passid) { + $user_id = $passid; + } + else { + $user_id = $_SERVER['REMOTE_ADDR']; + } + + $query = "SELECT user_score FROM like_user_scores WHERE user_id = '%s'"; + + $res = mysql_global_call($query, $user_id); + + if (!$res) { + return 0; + } + + $row = mysql_fetch_row($res); + + if ($row) { + $current_score = (int)$row[0]; + } + else { + $current_score = 0; + } + + return $current_score; +} + +function like_decrease_user_score($ip, $passid, $multiplier) { + if ($passid) { + $user_id = $passid; + } + else { + $user_id = $ip; + } + + $query = << 0, + 'smiley' => 0, // single emoji + 'sad' => 0, // single emoji + //'coinflip' => 0, + //'dice+1d6' => 0, + 'ok' => 0, // single emoji + 'animal' => 0, // random animal emoji + 'food' => 0, // random food emoji + 'check' => 0, + 'cross' => 0, + //'nofile' => 0, + //'card' => 0, // random playing card emoji + //'wflag' => 0, + //'bflag' => 0, + 'like' => 0, // red heart emoji + //'rabbit' => 0, // single emoji + 'unlove' => 0, // broken heart emoji + 'rage' => 0, + 'perfect' => 0, + //'fortune' => 0, + //'dice+1d100' => 0, + //'bricks' => 0, + 'onsen' => 0, + //'party' => 0, // partyhat image + //'verified' => 0, + //'partyhat' => 0, // partyhat image, adjusted + //'pickle' => 0, // pickle rick image + //'trash' => 0, // trashcan image + 'heart' => 0, // random heart emoji (different colors) + //'santa' => 0, // santa hat image + 'joy' => 0, // single emoji + //'marquee' => 0, + 'pig' => 0, // single emoji + 'dog' => 0, // single emoji + 'cat' => 0, // single emoji + 'rainbow' => 0, + 'frog' => 0, // single emoji + //'dino' => 750, // dinosaur gif from /fit/ + //'spooky' => 1000, // random skeleton + ); + + $perks = array(); + + if ($only_unlocked) { + foreach ($req_points as $perk => $score) { + if ($current_score >= $score) { + $perks[] = $perk; + } + } + } + else { + foreach ($req_points as $perk => $score) { + $perks[$perk] = $current_score >= $score; + } + } + + return $perks; +} + +function like_parse_options_field($options) { + $show_score = false; + $active_perk = null; + + if (strlen($options) > 100) { + return array($show_score, $active_perk); + } + + $user_perks = like_get_perks_state(); + + $opts = explode(' ', $options); + + foreach ($opts as $opt) { + if ($user_perks[$opt] === true) { + if ($opt === 'showscore') { + $show_score = true; + } + else { + $active_perk = $opt; + break; + } + } + } + + return array($show_score, $active_perk); +} + +function like_build_perk_html($perk) { + $cnt_attrs = ''; + + switch ($perk) { + case 'animal': + $ary = array('🐭','🐹','🐰','🐶','🐺','🦊','🐵','🐸','🙈','🙉','🙊','🐯','🦁','🦓','🦒','🐴','🐮','🐷','🐻','🐼','🐲','🦄','🐱','😸','😹','😺','😻','😼','😽','😾','😿','🙀','🐅','🐆','🐘','🦏','🐂','🐃','🐄','🐎','🦌','🐐','🐏','🐑','🐖','🐗','🐪','🐫','🦍','🐉','🦖','🦕','🐈','🐀','🐁','🐇','🐒','🐕','🐩','🐨','🐿','🦔','🦇','🐍','🦅','🦉','🦆','🐓','🐔','🦃','🕊','🐣','🐤','🐥','🐦','🐧','🐋','🐳','🐬','🦈','🐟','🐠','🐡','🐙','🦑','🦐','🦀','🐚','🐌','🐢','🦎','🐊','🏇','🎠','♘','♞','🐽','🐾','👣','🐀','🐃','🐅','🐇','🐉','🐍','🐎','🐐','🐒','🐓','🐕','🐖'); + $html = $ary[array_rand($ary)]; + break; + case 'food': + $ary = array('🧀','🥚','🍳','🥞','🍠','🍞','🥐','🥖','🥨','🍔','🍕','🍝','🍟','🍤','🌭','🌮','🌯','🍛','🥙','🥘','🥗','🥪','🥫','🥓','🍖','🍗','🥩','🥢','🥡','🥟','🍚','🍜','🍲','🥠','🍘','🍙','🍣','🍥','🍱','🍡','🍢','🍇','🍈','🍉','🍊','🍋','🍌','🍍','🍎','🍏','🍐','🍑','🍒','🍓','🥝','🥥','🥦','🍄','🍅','🍆','🌶','🥑','🥕','🥒','🥔','🥜','🍰','🎂','🥧','🍨','🍦','🍩','🍪','🍿','🍮','🍯','🍧','🍫','🍬','🍭','🍺','🍻','🍷','🍸','🍹','🍶','🥂','🥃','🍾','☕','🍵','🥛','🍼','🥤','🍴','🍽','🥣','🥄'); + $html = $ary[array_rand($ary)]; + break; + case 'marquee': + $ary = array('🦖','⚽','🏀','⚾'); + $ico = $ary[array_rand($ary)]; + $html = << +$ico + +HTML; + break; + case 'rainbow': + $html = '🌈'; + break; + case 'wflag': + $html = '🏳️'; + break; + case 'bflag': + $html = '🏴'; + break; + case 'onsen': + $html = '♨️'; + break; + case 'rage': + $html = '💢'; + break; + case 'perfect': + $html = '💯'; + break; + case 'check': + $html = '✔️'; + break; + case 'cross': + $html = '❌'; + break; + case 'heart': + $ary = array('❤️','💙','💜','💛','🖤','💚'); + $html = $ary[array_rand($ary)]; + break; + case 'card': + $ary = array('♠️','♥️','♦️','♣️'); + $html = $ary[array_rand($ary)]; + break; + case 'like': + $html = '❤️'; + break; + case 'unlove': + $html = '💔'; + break; + case 'smiley': + $html = '😃'; + break; + case 'sad': + $html = '🙁'; + break; + case 'ok': + $html = '👌'; + break; + case 'coinflip': + $html = 'Coin Flip: ' . (mt_rand(0, 1) === 1 ? 'Heads' : 'Tails') . ''; + break; + case 'party': + $html = ''; + break; + case 'partyhat': + $cnt_attrs = ' style="position:absolute"'; + $html = ''; + break; + case 'pickle': + $html = ''; + break; + case 'nofile': + $html = ''; + break; + case 'trash': + $html = ''; + break; + case 'bricks': + $html = ''; + break; + case 'pig': + $html = '🐷'; + break; + case 'santa': + $html = ''; + break; + case 'verified': + $html = '
        '; + break; + case 'joy': + $html = '😂'; + break; + case 'rabbit': + $html = '🐰'; + break; + case 'frog': + $html = '🐸'; + break; + case 'dog': + $html = '🐶'; + break; + case 'cat': + $html = '🐱'; + break; + case 'dino': + $html = ''; + break; + case 'spooky': + $id = mt_rand(1, 23); + $html = ''; + break; + default: + return null; + break; + } + + return '
    '; +} diff --git a/lib/oekaki-test.php b/lib/oekaki-test.php new file mode 100644 index 0000000..1aa7aab --- /dev/null +++ b/lib/oekaki-test.php @@ -0,0 +1,160 @@ + 0) { + error(S_FAILEDUPLOAD); + } + + if ($file['size'] === 0) { + error(S_NOREC); + } + + if ($file['size'] > $max_size) { + error(S_TOOLARGE); + } + + $tmp_file = $file['tmp_name']; + + if (is_uploaded_file($tmp_file) !== true) { + error(S_FAILEDUPLOAD); + } + + // Check the actual data now + + $f = fopen($tmp_file, 'rb'); + + $magic = fread($f, 4); + + $decompressed_size = fread($f, 4); + + fread($f, 4); // version numbers + + $compressed_data = fread($f, $file['size'] - 12); + + fclose($f); + + if ($magic !== "\x54\x47\x4B\x01") { // TGK 0x01 + error(S_NOREC); + } + + $decompressed_size = (int)unpack('N', $decompressed_size)[1]; + + if (!$decompressed_size || $decompressed_size <= 0) { + error(S_NOREC); + } + + if ($decompressed_size > $max_data_size) { + return false; + } + + if (!$compressed_data) { + error(S_NOREC); + } + + $data = gzinflate($compressed_data, $decompressed_size); + + if ($data === false) { + error(S_NOREC); + } + + $meta_size = (int)unpack('n', $data)[1]; + + if (!$meta_size) { + return false; + } + + // tool count (byte), tool entry size (byte) + $tool_meta = unpack('C2', substr($data, $meta_size, 2)); + + if (!$tool_meta || !(int)$tool_meta[1] || !(int)$tool_meta[2]) { + return false; + } + + $events_pos = $meta_size + ((int)$tool_meta[1]) * ((int)$tool_meta[2]) + 2; + + $event_count = (int)unpack('N', substr($data, $events_pos, 4))[1]; + + if (!$event_count || $event_count > 8640000) { + return false; + } + + $prelude_type = unpack('C', substr($data, $events_pos + 4, 1)); + + if ($prelude_type === false || (int)$prelude_type[1] !== 0) { + return false; + } + + $conclusion_type = unpack('C', substr($data, -5, 1)); + + if ($conclusion_type === false || (int)$conclusion_type[1] !== 255) { + return false; + } + + return true; +} + +function oekaki_get_valid_src_pid($src_pid, $board, $thread_id) { + $src_pid = (int)$src_pid; + + if ($src_pid < 1) { + return null; + } + + $thread_id = (int)$thread_id; + + if ($thread_id < 1) { + return null; + } + + $sql = "SELECT no FROM `%s` WHERE no = $src_pid AND (resto = $thread_id OR resto = 0) AND tim != 0 LIMIT 1"; + + $res = mysql_board_call($sql, $board); + + if (!$res || mysql_num_rows($res) !== 1) { + return null; + } + + return $src_pid; +} + +function oekaki_format_info($time, $replay_tim, $src_pid) { + $time = (int)$time; + + if ($time < 1 || $time > 5184000) { // 60 days + return ''; + } + + if ($time < 60) { + $time_str = $time . 's'; + } + else if ($time < 3600) { + $time_str = round($time / 60) . 'm'; + } + else { + $time_str = (int)($time / 3600) . 'h ' . round(($time % 3600) / 60) . 'm'; + } + + if ($replay_tim && !$src_pid) { + $replay_tim = (int)$replay_tim; + $replay_link = ", Replay: View"; + } + else { + $replay_link = ''; + } + + if ($src_pid) { + $src_pid = (int)$src_pid; + $src_link = ", Source: >>$src_pid"; + } + else { + $src_link = ''; + } + + return "

    Oekaki Post (Time: $time_str" . $replay_link . $src_link . ")"; +} diff --git a/lib/oekaki.php b/lib/oekaki.php new file mode 100644 index 0000000..cf798d9 --- /dev/null +++ b/lib/oekaki.php @@ -0,0 +1,160 @@ + 0) { + error(S_FAILEDUPLOAD); + } + + if ($file['size'] === 0) { + error(S_NOREC); + } + + if ($file['size'] > $max_size) { + error(S_TOOLARGE); + } + + $tmp_file = $file['tmp_name']; + + if (is_uploaded_file($tmp_file) !== true) { + error(S_FAILEDUPLOAD); + } + + // Check the actual data now + + $f = fopen($tmp_file, 'rb'); + + $magic = fread($f, 4); + + $decompressed_size = fread($f, 4); + + fread($f, 4); // version numbers + + $compressed_data = fread($f, $file['size'] - 12); + + fclose($f); + + if ($magic !== "\x54\x47\x4B\x01") { // TGK 0x01 + error(S_NOREC); + } + + $decompressed_size = (int)unpack('N', $decompressed_size)[1]; + + if (!$decompressed_size || $decompressed_size <= 0) { + error(S_NOREC); + } + + if ($decompressed_size > $max_data_size) { + return false; + } + + if (!$compressed_data) { + error(S_NOREC); + } + + $data = gzinflate($compressed_data, $decompressed_size); + + if ($data === false) { + error(S_NOREC); + } + + $meta_size = (int)unpack('n', $data)[1]; + + if (!$meta_size) { + return false; + } + + // tool count (byte), tool entry size (byte) + $tool_meta = unpack('C2', substr($data, $meta_size, 2)); + + if (!$tool_meta || !(int)$tool_meta[1] || !(int)$tool_meta[2]) { + return false; + } + + $events_pos = $meta_size + ((int)$tool_meta[1]) * ((int)$tool_meta[2]) + 2; + + $event_count = (int)unpack('N', substr($data, $events_pos, 4))[1]; + + if (!$event_count || $event_count > 12000000) { + return false; + } + + $prelude_type = unpack('C', substr($data, $events_pos + 4, 1)); + + if ($prelude_type === false || (int)$prelude_type[1] !== 0) { + return false; + } + + $conclusion_type = unpack('C', substr($data, -5, 1)); + + if ($conclusion_type === false || (int)$conclusion_type[1] !== 255) { + return false; + } + + return true; +} + +function oekaki_get_valid_src_pid($src_pid, $board, $thread_id) { + $src_pid = (int)$src_pid; + + if ($src_pid < 1) { + return null; + } + + $thread_id = (int)$thread_id; + + if ($thread_id < 1) { + return null; + } + + $sql = "SELECT no FROM `%s` WHERE no = $src_pid AND (resto = $thread_id OR resto = 0) AND tim != 0 LIMIT 1"; + + $res = mysql_board_call($sql, $board); + + if (!$res || mysql_num_rows($res) !== 1) { + return null; + } + + return $src_pid; +} + +function oekaki_format_info($time, $replay_tim, $src_pid) { + $time = (int)$time; + + if ($time < 1 || $time > 5184000) { // 60 days + return ''; + } + + if ($time < 60) { + $time_str = $time . 's'; + } + else if ($time < 3600) { + $time_str = round($time / 60) . 'm'; + } + else { + $time_str = (int)($time / 3600) . 'h ' . round(($time % 3600) / 60) . 'm'; + } + + if ($replay_tim && !$src_pid) { + $replay_tim = (int)$replay_tim; + $replay_link = ", Replay: View"; + } + else { + $replay_link = ''; + } + + if ($src_pid) { + $src_pid = (int)$src_pid; + $src_link = ", Source: >>$src_pid"; + } + else { + $src_link = ''; + } + + return "

    Oekaki Post (Time: $time_str" . $replay_link . $src_link . ")"; +} diff --git a/lib/perk_options-test.php b/lib/perk_options-test.php new file mode 100644 index 0000000..42a1c82 --- /dev/null +++ b/lib/perk_options-test.php @@ -0,0 +1,230 @@ + 50)) { + return false; + } + + $perks = explode(' ', $options_str); + + if (empty($perks)) { + return false; + } + + if (count($perks) > 3) { + $perks = array_slice($perks, 0, 3); + } + + $all_perks = array( + 'smiley','happy','neutral','sad','ok','check','cross', + 'whiteflag','blackflag','like','rabbit','nolove','anger','perfect', + 'onsen','heart','joy','pigface','dogface','catface','monkeyface', + 'frogface','tigerface','lionface','bearface','chicken','eagle','cowface', + 'babychick','rainbow','star','fire','zap','water','maple','animal','food', + 'apple','banana','tomato','hamburger','potato','carrot','popcorn','pizza', + 'dango','beer','football','superball','vidya','thoughts','confetti', + 'lightbulb','book','creditcard','music','rpg' + ); + + $html_perks = array(); + + foreach ($perks as $perk) { + if ($perk === 'randomperk') { + $perk = $all_perks[array_rand($all_perks)]; + } + + switch ($perk) { + case 'smiley': + $html_perks[] = "🙂"; + break; + case 'happy': + $html_perks[] = "😃"; + break; + case 'neutral': + $html_perks[] = "😐"; + break; + case 'sad': + $html_perks[] = "🙁"; + break; + case 'ok': + $html_perks[] = "👌"; + break; + case 'check': + $html_perks[] = "✔️"; + break; + case 'cross': + $html_perks[] = ""; + break; + case 'whiteflag': + $html_perks[] = "🏳️"; + break; + case 'blackflag': + $html_perks[] = "🏴"; + break; + case 'like': + $html_perks[] = "❤️"; + break; + case 'rabbit': + $html_perks[] = "🐰"; + break; + case 'nolove': + $html_perks[] = "💔"; + break; + case 'anger': + $html_perks[] = "💢"; + break; + case 'perfect': + $html_perks[] = "💯"; + break; + case 'onsen': + $html_perks[] = "♨️"; + break; + case 'heart': + $ary = array('❤️','💙','💜','💛','🖤','💚'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + case 'joy': + $html_perks[] = "😂"; + break; + case 'pigface': + $html_perks[] = "🐷"; + break; + case 'dogface': + $html_perks[] = "🐶"; + break; + case 'catface': + $html_perks[] = "🐱"; + break; + case 'monkeyface': + $html_perks[] = "🐵"; + break; + case 'frogface': + $html_perks[] = "🐸"; + break; + case 'tigerface': + $html_perks[] = "🐯"; + break; + case 'lionface': + $html_perks[] = "🦁"; + break; + case 'bearface': + $html_perks[] = "🐻"; + break; + case 'chicken': + $html_perks[] = "🐔"; + break; + case 'eagle': + $html_perks[] = "🦅"; + break; + case 'cowface': + $html_perks[] = "🐮"; + break; + case 'babychick': + $html_perks[] = "🐤"; + break; + case 'rainbow': + $html_perks[] = "🌈"; + break; + case 'star': + $html_perks[] = ""; + break; + case 'fire': + $html_perks[] = "🔥"; + break; + case 'zap': + $html_perks[] = ""; + break; + case 'water': + $html_perks[] = "💧"; + break; + case 'maple': + $html_perks[] = "🍁"; + break; + + case 'animal': + $ary = array('🐭','🐹','🐰','🐶','🐺','🦊','🐵','🐸','🙈','🙉','🙊','🐯','🦁','🦓','🦒','🐴','🐮','🐷','🐻','🐼','🐲','🦄','🐱','😸','😹','😺','😻','😼','😽','😾','😿','🙀','🐅','🐆','🐘','🦏','🐂','🐃','🐄','🐎','🦌','🐐','🐏','🐑','🐖','🐗','🐪','🐫','🦍','🐉','🦖','🦕','🐈','🐀','🐁','🐇','🐒','🐕','🐩','🐨','🐿','🦔','🦇','🐍','🦅','🦉','🦆','🐓','🐔','🦃','🕊','🐣','🐤','🐥','🐦','🐧','🐋','🐳','🐬','🦈','🐟','🐠','🐡','🐙','🦑','🦐','🦀','🐚','🐌','🐢','🦎','🐊','🏇','🎠','♘','♞','🐽','🐾','👣','🐀','🐃','🐅','🐇','🐉','🐍','🐎','🐐','🐒','🐓','🐕','🐖'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + + case 'food': + $ary = array('🧀','🥚','🍳','🥞','🍠','🍞','🥐','🥖','🥨','🍔','🍕','🍝','🍟','🍤','🌭','🌮','🌯','🍛','🥙','🥘','🥗','🥪','🥫','🥓','🍖','🍗','🥩','🥢','🥡','🥟','🍚','🍜','🍲','🥠','🍘','🍙','🍣','🍥','🍱','🍡','🍢','🍇','🍈','🍉','🍊','🍋','🍌','🍍','🍎','🍏','🍐','🍑','🍒','🍓','🥝','🥥','🥦','🍄','🍅','🍆','🌶','🥑','🥕','🥒','🥔','🥜','🍰','🎂','🥧','🍨','🍦','🍩','🍪','🍿','🍮','🍯','🍧','🍫','🍬','🍭','🍺','🍻','🍷','🍸','🍹','🍶','🥂','🥃','🍾','☕','🍵','🥛','🍼','🥤','🍴','🍽','🥣','🥄'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + + case 'apple': + $ary = array('🍏','🍎'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + case 'banana': + $html_perks[] = "🍌"; + break; + case 'tomato': + $html_perks[] = "🍅"; + break; + case 'hamburger': + $html_perks[] = "🍔"; + break; + case 'potato': + $html_perks[] = "🥔"; + break; + case 'carrot': + $html_perks[] = "🥕"; + break; + case 'popcorn': + $html_perks[] = "🍿"; + break; + case 'pizza': + $html_perks[] = "🍕"; + break; + case 'dango': + $html_perks[] = "🍡"; + break; + case 'beer': + $html_perks[] = "🍺"; + break; + case 'football': + $html_perks[] = ""; + break; + case 'superball': + $html_perks[] = "🏈"; + break; + case 'vidya': + $html_perks[] = "🎮"; + break; + case 'thoughts': + $html_perks[] = "💭"; + break; + case 'confetti': + $html_perks[] = "🎉"; + break; + case 'lightbulb': + $html_perks[] = "💡"; + break; + case 'book': + $ary = array('📕','📙','📘','📗'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + case 'creditcard': + $html_perks[] = "💳"; + break; + case 'music': + $html_perks[] = "🎵"; + break; + case 'rpg': + $ary = array('✊','✋','✌'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + + default: + break; + } + + } + + if (!empty($html_perks)) { + $comment = $comment . '
    ' . implode(' ', $html_perks) . '
    '; + return true; + } + + return false; +} diff --git a/lib/perk_options.php b/lib/perk_options.php new file mode 100644 index 0000000..42a1c82 --- /dev/null +++ b/lib/perk_options.php @@ -0,0 +1,230 @@ + 50)) { + return false; + } + + $perks = explode(' ', $options_str); + + if (empty($perks)) { + return false; + } + + if (count($perks) > 3) { + $perks = array_slice($perks, 0, 3); + } + + $all_perks = array( + 'smiley','happy','neutral','sad','ok','check','cross', + 'whiteflag','blackflag','like','rabbit','nolove','anger','perfect', + 'onsen','heart','joy','pigface','dogface','catface','monkeyface', + 'frogface','tigerface','lionface','bearface','chicken','eagle','cowface', + 'babychick','rainbow','star','fire','zap','water','maple','animal','food', + 'apple','banana','tomato','hamburger','potato','carrot','popcorn','pizza', + 'dango','beer','football','superball','vidya','thoughts','confetti', + 'lightbulb','book','creditcard','music','rpg' + ); + + $html_perks = array(); + + foreach ($perks as $perk) { + if ($perk === 'randomperk') { + $perk = $all_perks[array_rand($all_perks)]; + } + + switch ($perk) { + case 'smiley': + $html_perks[] = "🙂"; + break; + case 'happy': + $html_perks[] = "😃"; + break; + case 'neutral': + $html_perks[] = "😐"; + break; + case 'sad': + $html_perks[] = "🙁"; + break; + case 'ok': + $html_perks[] = "👌"; + break; + case 'check': + $html_perks[] = "✔️"; + break; + case 'cross': + $html_perks[] = ""; + break; + case 'whiteflag': + $html_perks[] = "🏳️"; + break; + case 'blackflag': + $html_perks[] = "🏴"; + break; + case 'like': + $html_perks[] = "❤️"; + break; + case 'rabbit': + $html_perks[] = "🐰"; + break; + case 'nolove': + $html_perks[] = "💔"; + break; + case 'anger': + $html_perks[] = "💢"; + break; + case 'perfect': + $html_perks[] = "💯"; + break; + case 'onsen': + $html_perks[] = "♨️"; + break; + case 'heart': + $ary = array('❤️','💙','💜','💛','🖤','💚'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + case 'joy': + $html_perks[] = "😂"; + break; + case 'pigface': + $html_perks[] = "🐷"; + break; + case 'dogface': + $html_perks[] = "🐶"; + break; + case 'catface': + $html_perks[] = "🐱"; + break; + case 'monkeyface': + $html_perks[] = "🐵"; + break; + case 'frogface': + $html_perks[] = "🐸"; + break; + case 'tigerface': + $html_perks[] = "🐯"; + break; + case 'lionface': + $html_perks[] = "🦁"; + break; + case 'bearface': + $html_perks[] = "🐻"; + break; + case 'chicken': + $html_perks[] = "🐔"; + break; + case 'eagle': + $html_perks[] = "🦅"; + break; + case 'cowface': + $html_perks[] = "🐮"; + break; + case 'babychick': + $html_perks[] = "🐤"; + break; + case 'rainbow': + $html_perks[] = "🌈"; + break; + case 'star': + $html_perks[] = ""; + break; + case 'fire': + $html_perks[] = "🔥"; + break; + case 'zap': + $html_perks[] = ""; + break; + case 'water': + $html_perks[] = "💧"; + break; + case 'maple': + $html_perks[] = "🍁"; + break; + + case 'animal': + $ary = array('🐭','🐹','🐰','🐶','🐺','🦊','🐵','🐸','🙈','🙉','🙊','🐯','🦁','🦓','🦒','🐴','🐮','🐷','🐻','🐼','🐲','🦄','🐱','😸','😹','😺','😻','😼','😽','😾','😿','🙀','🐅','🐆','🐘','🦏','🐂','🐃','🐄','🐎','🦌','🐐','🐏','🐑','🐖','🐗','🐪','🐫','🦍','🐉','🦖','🦕','🐈','🐀','🐁','🐇','🐒','🐕','🐩','🐨','🐿','🦔','🦇','🐍','🦅','🦉','🦆','🐓','🐔','🦃','🕊','🐣','🐤','🐥','🐦','🐧','🐋','🐳','🐬','🦈','🐟','🐠','🐡','🐙','🦑','🦐','🦀','🐚','🐌','🐢','🦎','🐊','🏇','🎠','♘','♞','🐽','🐾','👣','🐀','🐃','🐅','🐇','🐉','🐍','🐎','🐐','🐒','🐓','🐕','🐖'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + + case 'food': + $ary = array('🧀','🥚','🍳','🥞','🍠','🍞','🥐','🥖','🥨','🍔','🍕','🍝','🍟','🍤','🌭','🌮','🌯','🍛','🥙','🥘','🥗','🥪','🥫','🥓','🍖','🍗','🥩','🥢','🥡','🥟','🍚','🍜','🍲','🥠','🍘','🍙','🍣','🍥','🍱','🍡','🍢','🍇','🍈','🍉','🍊','🍋','🍌','🍍','🍎','🍏','🍐','🍑','🍒','🍓','🥝','🥥','🥦','🍄','🍅','🍆','🌶','🥑','🥕','🥒','🥔','🥜','🍰','🎂','🥧','🍨','🍦','🍩','🍪','🍿','🍮','🍯','🍧','🍫','🍬','🍭','🍺','🍻','🍷','🍸','🍹','🍶','🥂','🥃','🍾','☕','🍵','🥛','🍼','🥤','🍴','🍽','🥣','🥄'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + + case 'apple': + $ary = array('🍏','🍎'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + case 'banana': + $html_perks[] = "🍌"; + break; + case 'tomato': + $html_perks[] = "🍅"; + break; + case 'hamburger': + $html_perks[] = "🍔"; + break; + case 'potato': + $html_perks[] = "🥔"; + break; + case 'carrot': + $html_perks[] = "🥕"; + break; + case 'popcorn': + $html_perks[] = "🍿"; + break; + case 'pizza': + $html_perks[] = "🍕"; + break; + case 'dango': + $html_perks[] = "🍡"; + break; + case 'beer': + $html_perks[] = "🍺"; + break; + case 'football': + $html_perks[] = ""; + break; + case 'superball': + $html_perks[] = "🏈"; + break; + case 'vidya': + $html_perks[] = "🎮"; + break; + case 'thoughts': + $html_perks[] = "💭"; + break; + case 'confetti': + $html_perks[] = "🎉"; + break; + case 'lightbulb': + $html_perks[] = "💡"; + break; + case 'book': + $ary = array('📕','📙','📘','📗'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + case 'creditcard': + $html_perks[] = "💳"; + break; + case 'music': + $html_perks[] = "🎵"; + break; + case 'rpg': + $ary = array('✊','✋','✌'); + $html_perks[] = "" . $ary[array_rand($ary)] . ''; + break; + + default: + break; + } + + } + + if (!empty($html_perks)) { + $comment = $comment . '
    ' . implode(' ', $html_perks) . '
    '; + return true; + } + + return false; +} diff --git a/lib/phash.php b/lib/phash.php new file mode 100644 index 0000000..3e21e6a --- /dev/null +++ b/lib/phash.php @@ -0,0 +1,201 @@ + $median) { + $hash |= 0x01; + } + } + + return sprintf("%016x", $hash); + } + + public static function hash_file($image_path, &$err = null) { + if (!$image_path) { + return false; + } + + $size = getimagesize($image_path); + + if (!$size) { + $err = "Couldn't get file information"; + return false; + } + + $type = $size[2]; + + if ($type === IMAGETYPE_PNG) { + $img = imagecreatefrompng($image_path); + } + else if ($type === IMAGETYPE_JPEG) { + $img = imagecreatefromjpeg($image_path); + } + else if ($type === IMAGETYPE_GIF) { + $img = imagecreatefromgif($image_path); + } + else { + $err = "Invalid image"; + return false; + } + + $width = $size[0]; + $height = $size[1]; + + $hash = self::hash($img, $width, $height); + + if ($hash === false) { + $err = "Couldn't process the image"; + return false; + } + + return $hash; + } +} diff --git a/lib/postfilter-test.php b/lib/postfilter-test.php new file mode 100644 index 0000000..25351b8 --- /dev/null +++ b/lib/postfilter-test.php @@ -0,0 +1,3965 @@ + array_search($f2, $pkeys); +} + +// style 1 - no spaces, just letters+numbers in fields +function is_random_text($fields) +{ + foreach($fields as $field) { + $num = preg_match('/[0-9]/', $field); + $lc = preg_match('/[a-z]/', $field); + $uc = preg_match('/[A-Z]/', $field); + $spc = preg_match('/ /', $field); + if ($spc || ($num + $lc + $uc)<2) return false; + } + + return true; +} + +// style 2 - uc+lc+spaces in fields +function is_random_word_text($fields) +{ + foreach($fields as $field) { + $other = preg_match('/[^a-zA-Z \n]/', $field); + $lc = preg_match('/[a-z]/', $field); + $uc = preg_match('/[A-Z]/', $field); + $spc = preg_match('/ /', $field); + if ($other || !$spc || !$lc || !$uc) return false; + } + + return true; +} + +function has_cyrillic($txt) +{ + return preg_match('/[\xD0-\xD3][\x80-\xBF]/', $txt) > 0; +} + +function expand_short_urls($com) +{ + $urls = array(); + $urltext = ""; + + $com = preg_replace("/\(dot\)|DOT/", ".", $com); + preg_replace("@(([A-Za-z0-9_-]+\.)?((tinyurl|go2cut|doiop|x2t|snipr|gorkk|veeox|gurlx|goshrink|shrink[a-z]+|shortmaker|notlong|2gohere|urlmr|adjix|icanhaz|linkbee|oeeq|url9|urlzen|cladpal|redirx|yuarel|llfk|as2h|at|url-go|shrten|eyodude|urlxp|myurlz|allno1|nlz2|ye-s|way|lymme|unrelo|qfwr|urluda|golinkgo|cnekt|12n3|peqno|pasukan|vktw|snipie|4gk|82au|[a-z]+url|tinyden)\\.com|(lix|urlm|t2w|trigg|flib)\\.in|j\\.mp|hub\\.tm|(smarturl|fogz|urlink)\\.eu|sn\\.vc|atu\\.ca|(a|is)\\.gd|(xrl|nuurl|kore|p3n|txtn|hepy|w95|freepl|0x3|2tu)\\.us|dduri\\.info|cc\\.st|mzan\\.si|(\xe2\x9d\xbd|cctv)\\.ws|(metamark|sogood|idek|2tr|ln-s|urlaxe|littleurl)\\.net|(fyad|linkmenow|linkplug)\\.org|ad\\.vu|(bit|xa|ow|3|smal|to)\\.ly|safe\\.mn|goo\\.gl|is\\.gd|zi\\.ma|(tr|sn|jar|gow)\\.im|twurl\\.(cc|nl)|(fon|cli|rod|mug)\\.gs|(urlenco|dboost)\\.de|(tiny|bizz|blu|juu)\\.cc|2big\\.at|(crum|sk9)\\.pl|(bloat|pnt|lnq)\\.me|kl\\.am|(showip|2so)\\.be|minilink\\.me|lurl\\.no|(jai|cvm)\\.biz|xs\\.md|short\\.to|(yep|trunc)\\.it|a\\.nf|shortlinks\\.co\\.uk|redir\\.ec|tim\\.pe)(/[?A-Za-z0-9_-]*)?)@ei", '$urls[] = "http://$1";', $com); + + foreach($urls as $url) { + $com .= strtolower(rpc_find_real_url(str_replace("preview.tinyurl","tinyurl",$url))); + } + + return $com; +} + +function normalize_ascii($text, $preserve_case = 0) +{ + $text = preg_replace( '#[(\[={]dot[)\]=}]#i', '.', $text ); + + $t = Transliterator::create("Any-Latin; nfd; [:nonspacing mark:] remove; nfkc; Latin-ASCII"); + if (!$t) return $text; + $text = $t->transliterate($text); + if (!$preserve_case) $text = strtolower($text); + + return $text; +} + +function strip_zerowidth($text) { + $text = preg_replace('/ + [\x17\x8\x1f]|\x{0702}|\x{1D176}|\x{008D}|\x{00A0}|\x{205F}|\x{FEFF}|\x{11A6}|\x{00AD}|\x{3164}|\x{2800}|\x{180B}|\x{180C}|\x{180D}| + \x{115F}|\x{1160}|\x{FFA0}|\x{034f}|\x{180e}|\x{17B4}|\x{17B5}| + [\x{0001}-\x{0008}\x{000E}\x{000F}\x{0010}-\x{001F}\x{007F}-\x{009F}]| + [\x{2000}-\x{200F}]| + [\x{2028}-\x{202F}]| + [\x{2060}-\x{206F}]| + [\x{fe00}-\x{fe0f}]| + [\x{FFF0}-\x{FFFB}]| + [\x{E0100}-\x{E01EF}]| + [\x{E0001}-\x{E007F}] + /ux', '', $text); + return $text; +} + +/** + * Removes codepoints above 3134F: + * E0000..E007F; Tags + * E0100..E01EF; Variation Selectors Supplement + * F0000..FFFFF; Supplementary Private Use Area-A + * 100000..10FFFF; Supplementary Private Use Area-B + */ +function strip_private_unicode($str) { + if ($str === '') { + return $str; + } + + return preg_replace('/[^\x{0000}-\x{3134F}]/u', '', $str); +} + +function strip_emoticons($text, $has_sjis_art = false) { + $regex = '[\x{2300}-\x{2311}\x{2313}-\x{23FF}]|[\x{3200}-\x{32FF}\x{2190}-\x{21FF}\x{2580}-\x{259F}\x{2600}-\x{26FF}\x{2B00}-\x{2BFE}\x{1F700}-\x{1F77F}\x{1F780}-\x{1F7D8}\x{1F780}-\x{1F7D8}\x{1F800}-\x{1F8FF}\x{1F900}-\x{1F9FF}]|[\x{1F200}-\x{1F2FF}]|[\x{2460}-\x{24FF}]|[\x{1F100}-\x{1F1FF}]|[\x{1F600}-\x{1F64F}]|[\x{1F300}-\x{1F5FF}]|[\x{1F680}-\x{1F6FF}]|[\x{2600}-\x{26FF}]|[\x{2700}-\x{27BF}]|[\x{1F000}-\x{1F02F}]|[\x{1F0A0}-\x{1F0FF}]|[\x{2139}\x{23F2}]|[\x{1F910}-\x{1F9E6}]|[\x{0365}]|\x{FDFD}|[\x{0488}\x{0489}\x{1abe}\x{20dd}\x{20de}\x{20df}\x{20e0}\x{20e2}\x{20e3}\x{20e4}\x{a670}\x{a671}\x{a672}\x{061c}\x{070F}\x{0332}\x{0305}\x{2B55}]|[\x{202A}-\x{202E}\x{2060}-\x{206F}]|[\x{200E}\x{200F}\x{180e}\x{2b50}\x{23b3}\x{23F1}]|[\x{1F780}-\x{1F7FF}\x{1FA70}-\x{1FAFF}]|[\x{1D173}-\x{1D17A}\x{13000}-\x{1342F}\x{fe00}-\x{fe0f}]'; + + if (!$has_sjis_art) { + $regex .= '|[\x{2502}-\x{257F}]'; + } + + return preg_replace("/$regex/u", '', $text); +} + +function strip_fake_capcodes($str) { + // double FULLWIDTH NUMBER SIGN or # + // PLACE OF INTEREST SIGN + return preg_replace('/[\x{FF03}#]{2,}|\x{2318}/u', '', $str); +} + +function normalize_text($text, $filter = '') { + $text = normalize_ascii($text); + $text = strip_zerowidth($text); + + //if ($filter) $text = $filter($text); + + $text = preg_replace('@[^a-zA-Z0-9.,/&:;?=~_-]@', '', $text); + + return $text; +} + +function normalize_content( $name ) +{ + // this needs some absolutely retarded shit to get this to not suck, however + // it is an almost fool proof way of translating to ascii letters + // without breaking kanji, cyrillic etc + + $name = preg_replace( '#[\x{2600}-\x{26FF}]#u', '', $name ); + + // set internal incoding to utf-8 + //die($name); + $oldEncoding = mb_internal_encoding(); + mb_internal_encoding('UTF-8'); + $name = convert_to_utf8($name); + // Done, back to old encoding + + $newname = ''; + + $len = mb_strlen( $name ); + for( $i = 0; $i < $len; $i++ ) { + trans_similar_to_ascii( $newname, mb_substr( $name, $i, 1 ) ); + } + + mb_internal_encoding($oldEncoding); + return $newname; +} + +function convert_to_utf8( $content ) +{ + if( !mb_check_encoding( $content, 'UTF-8' ) || !($content === mb_convert_encoding(mb_convert_encoding($content, 'UTF-32', 'UTF-8' ), 'UTF-8', 'UTF-32')) ) { + $content = mb_convert_encoding($content, 'UTF-8'); + } + + return $content; +} + +function mb_ord( $char ) +{ + mb_detect_order( array( 'UTF-8', 'ISO-8859-15', 'ISO-8859-1', 'ASCII' ) ); + $result = unpack( 'N', mb_convert_encoding( $char, 'UCS-4BE', 'UTF-8' ) ); + + if( is_array( $result ) === true ) return $result[1]; + + return ord($char); +} + +function normalize_check($com,$sub,$f) +{ + $n = normalize_text($com.$sub, "expand_short_urls"); + $n2 = preg_replace("/[\x80-\xFF]/", "", html_entity_decode($com, ENT_QUOTES, "UTF-8")); + + record_post_info($f, "from: $com$sub\nto: $n\ndeutf8: $n2"); +} + +function match_banned_text($links, $text, $is_re) +{ + $badlink = ""; + $should_ban = false; + + foreach ($links as $l) { + if ($l == '#') continue; + + $badlink = $l; + + if ($is_re) { + $should_ban = preg_match($l, $text, $m) > 0; + $badlink = TEST_BOARD ? "'$l' ({$m[0]})" : "'{$m[0]}'"; + } else { + $should_ban = strpos($text, $l) !== FALSE; + } + + if ($should_ban) + break; + } + + return $should_ban ? $badlink : ""; +} + +function check_banned_links($text, $links, $priv, $pub, $is_re, $name, $dest, $ban, $long, $perm = false) +{ + //check_banned_links($normalized_com, $sex, "sex spam links", S_BANNEDLINK, false, $name, $dest, true, false); + $badlink = match_banned_text($links, $text, $is_re); + $should_ban = ($badlink != ""); + $len = $long ? 14 : 1; + $len = $perm ? -1 : $len; + + if ($should_ban == true) { + $privres = sprintf("banned %s %s: %s", $is_re?"regex":"string",htmlspecialchars($badlink),$priv); + if ($ban) { + $pub = str_replace('Error: ', '', $pub); + auto_ban_poster($name, $len, 1, $privres, $pub, true); + } + if (TEST_BOARD) $pub .= "
    ".$privres; + error($pub, $dest); + } +} + +// this used to check the autobans table but now it just permabans +function auto_ban($name, $reason) { + auto_ban_poster($name, 7, 1, $reason); +} + +function get_jpeg_dimensions($contents) +{ + // this is faster than getimagesize + + $i = 0; + $len = strlen($contents); + + if( ord($contents{0}) == 0xFF & ord($contents{1}) == 0xD8 && ord($contents{2}) == 0xFF & ord($contents{3}) == 0xE0 ) { + $i = 4; + + if( $contents{$i+2} == 'J' && $contents{$i+3} == 'F' && $contents{$i+4} == 'I' && $contents{$i+5} == 'F' && ord($contents{$i+6}) == 0x00 ) { + // valid image. + $block_length = ord($contents{$i}) * 256 + ord($contents{$i+1}); + + while( $i < $len ) { + $i += $block_length; + + if( $i > $len ) { + return false; + } + + if( ord($contents{$i}) != 0xFF ) { + return false; + } + + + if( ord($contents{$i+1}) == 0xC0 ) { + $width = ord($contents{$i+7})*256 + ord($contents{$i+8}); + $height = ord($contents{$i+5})*256 + ord($contents{$i+6}); + + return array($width, $height); + } else { + $i+=2; + $block_length = ord($contents{$i}) * 256 + ord($contents{$i+1}); + } + } + } + } + + return false; +} + +function file_too_big_for_type( $ext, $w, $h, $fsize ) +{ + if ($ext === ".gif" || $ext === ".pdf" || $ext === '.webm' || $ext === '.mp4') { + return NO; + } + + $uncompressed_size = $w * $h * 4; + + return ($fsize > (3*$uncompressed_size)) ? YES : NO; +} + +function regex_ignoring_nulls($words) +{ + $rwords = preg_replace("/./", "$0[^\\\\x01-\\\\xFF]*", $words); + $rwords = str_replace(".", "\\.", $rwords); + return "/".implode("|", $rwords)."/i"; +} + +/** + * Strips exif from JPEG images + * $file needs to be safe to use as shell argument + */ +function strip_jpeg_exif($file) { + return system("/usr/local/bin/jpegtran -copy none -outfile '$file' '$file'") !== false; +} + +/** + * Strips non-whitelisted PNG chunks. + * Returns an error if an animated PNG is detected. + * Overwrites input file if modifications have been made. + * $file needs to be safe to use as shell argument + * Returns the number of chunks skipped or an error code (negative value). + */ +function strip_png_chunks($file, $max_chunk_len = 16 * 1024 * 1024) { + $keep_chunks = [ + 'ihdr', + 'plte', + 'idat', + 'iend', + 'trns', + 'gama', + 'sbit', + 'phys', + 'srgb', + 'bkgd', + 'time', + 'chrm', + 'iccp' + ]; + + $img = fopen($file, 'rb'); + + if (!$img) { + return -9; + } + + $data = fread($img, 8); + + if ($data !== "\x89PNG\r\n\x1a\n") { + fclose($img); + return -1; + } + + $output = ''; + + $skip_count = 0; + + while (!feof($img)) { + $chunk_len_buf = fread($img, 4); + + if (!$chunk_len_buf) { + break; + } + + if (strlen($chunk_len_buf) !== 4) { + return -1; + } + + $chunk_len = unpack('N', $chunk_len_buf)[1]; + + if ($chunk_len > $max_chunk_len) { + return -1; + } + + $chunk_type_buf = fread($img, 4); + + if (strlen($chunk_type_buf) !== 4) { + return -1; + } + + $chunk_type = strtolower($chunk_type_buf); + + // aPNG is not supported + if ($chunk_type === 'actl' || $chink_type === 'fctl' || $chink_type === 'fdat') { + return -2; + } + + if (in_array($chunk_type, $keep_chunks)) { + if ($chunk_len > 0) { + $data = fread($img, $chunk_len); + + if (strlen($data) !== $chunk_len) { + return -1; + } + } + else { + $data = ''; + } + + $crc = fread($img, 4); + + if (strlen($crc) !== 4) { + return -1; + } + + $output .= $chunk_len_buf . $chunk_type_buf . $data . $crc; + + if ($chunk_type === 'iend') { + fread($img, 1); + if (!feof($img)) { + $skip_count++; + } + break; + } + } + else { + fseek($img, $chunk_len + 4, SEEK_CUR); + $skip_count++; + } + } + + fclose($img); + + if ($output === '') { + return -1; + } + + if ($skip_count === 0) { + return 0; + } + + $out_file = $file . '_pngtmp'; + + $out = fopen($out_file, 'wb'); + + if (!$out) { + return -9; + } + + if (fwrite($out, "\x89PNG\r\n\x1a\n") === false) { + return -9; + } + + if (fwrite($out, $output) === false) { + return -9; + } + + fclose($out); + + if (rename($out_file, $file) === false) { + return -9; + } + + return $skip_count; +} + +// Calculates the actual image data inside a JPEG file and errors out +// if it's smaller than the reported size. +function validate_jpeg_size($file, $reported_size) { + $eof = false; + + $img = fopen($file, 'rb'); + + $data = fread($img, 2); + + if ($data !== "\xff\xd8") { + fclose($img); + return false; + } + + while (!feof($img)) { + $data = fread($img, 1); + + if ($data !== "\xff") { + continue; + } + + while (!feof($img)) { + $data = fread($img, 1); + + if ($data !== "\xff") { + break; + } + } + + if (feof($img)) { + break; + } + + $byte = unpack('C', $data)[1]; + + if ($byte === 217) { + $eof = ftell($img); + break; + } + + if ($byte === 0 || $byte === 1 || ($byte >= 208 && $byte <= 216)) { + continue; + } + + $data = fread($img, 2); + + $length = unpack('n', $data)[1]; + + if ($length < 1) { + break; + } + + fseek($img, $length - 2, SEEK_CUR); + } + + fclose($img); + + // 50 KB + if ($reported_size - $eof >= 51200) { + error(S_IMGCONTAINSFILE, $file); + } + + return $eof; +} + +/** + * Checks for extensions and comments + * and calculates actual GIF data. + * Strips extra data if it exists. + * $file needs to be safe to use as shell argument. + */ +function strip_gif_extra_data($file, $reported_size) { + $binary = '/usr/local/bin/gifsicle'; + + $res = shell_exec("$binary --sinfo \"$file\" 2>&1"); + + if ($res !== null) { + $size = 0; + + $need_strip = false; + + if (preg_match('/ extensions [0-9]+| comment /', $res)) { + $need_strip = true; + } + else if (preg_match_all('/compressed size ([0-9]+)/', $res, $m)) { + foreach ($m[1] as $frame_size) { + $size += (int)$frame_size; + } + + // Strip if 50+ KB of extra data is found + if ($reported_size - $size >= 51200) { + $need_strip = true; + } + } + + if ($need_strip) { + if (system("$binary --no-comments --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1") === false) { + // gifsicle error + return -1; + } + else { + // file was modified + return 1; + } + } + } + else { + // gifsicle error + return -1; + } + + // nothing changed + return 0; +} + +// No longer used +function spam_filter_post_image($name, $dest, $md5, $upfile_name, $ext, $w, $h, $fsize) +{ + if( $upfile_name == '' ) error('Blank file names are not supported.'); + + if (file_too_big_for_type($ext, $w, $h, $fsize) === YES) { + $lim = 3*4*$w*$h; + error(S_IMGCONTAINSFILE, $dest); + } + + $img_bytes = file_get_contents($dest); + $img_beginning = strlen($img_bytes) > 0x50000 ? substr($img_bytes, 0, 0x40000).substr($img_bytes, -(0x10000)) : $img_bytes; + + global $silent_reject; + $silent_reject = 0; + + // protect against IE's retarded MIME-sniffing XSS vulnerability + // by doing our own sniffing and rejecting exploitable files + { + $negative_match = regex_ignoring_nulls(array("minitokyonet", "urchin.js")); + //except minitokyo from this, it causes false positives + if (preg_match($negative_match, $img_beginning)===0) + { + // ' DATE_SUB(NOW(), INTERVAL 1 HOUR) LIMIT 1 +SQL; + + $res = mysql_global_call($query); + + if ($res && mysql_num_rows($res) === 1) { + return true; + } + + $query = "INSERT INTO postfilter_hits (filter_id, board, long_ip) VALUES($filter_id, '%s', $long_ip)"; + + return mysql_global_call($query, BOARD_DIR); +} + +function log_postfilter_hit($filter, $board, $thread_id, $name, $sub, $com, $upfile_name) { + $ip = $_SERVER['REMOTE_ADDR']; + + $country = $_SERVER['HTTP_X_GEO_COUNTRY']; + + $threat_score = spam_filter_get_threat_score($country, !$thread_id, true); + + $meta = spam_filter_format_http_headers("$name\n$sub\n$com", $country, $upfile_name, $threat_score); + + $action = "filter_{$filter['id']}"; + + $query = <<\/]+|>/', ' ', $sub . ' ' . $com . ' '. $name); + $normalized_com_sage = ucwords(strtolower($normalized_com_sage)); + $normalized_com_sage = normalize_ascii($normalized_com_sage, 1); + } + + $userpwd = UserPwd::getSession(); + + $matched_filter = false; + + while ($filter = mysql_fetch_assoc($res)) { + // Counter mode: triggers when the number of matches is at least $min_count + $min_count = (int)$filter['min_count']; + + if ($min_count < 1) { + $min_count = 1; + } + + // Lenient filter + if ($filter['lenient']) { + if ($userpwd) { + if ($filter['updated_on']) { + $since_ts = (int)$filter['updated_on']; + } + else { + $since_ts = (int)$filter['created_on']; + } + + if ($userpwd->isUserKnownOrVerified(60, $since_ts)) { // 1 hour + continue; + } + } + } + + // OPs-only filter + if ($filter['ops_only'] && $resto) { + continue; + } + + if ($filter['autosage']) { + // Autosage filter but the post is a reply + if ($resto) { + continue; + } + // Regex filter + if ($filter['regex']) { + if ($min_count > 1) { + if (preg_match_all($filter['pattern'], $expanded_com) >= $min_count) { + $matched_filter = $filter; + break; + } + } + else { + if (preg_match($filter['pattern'], $expanded_com) === 1) { + $matched_filter = $filter; + break; + } + } + } + // String filter for autosaging + else { + if ($min_count > 1) { + if (substr_count($normalized_com_sage, $filter['pattern']) >= $min_count) { + $matched_filter = $filter; + break; + } + } + else { + if (strpos($normalized_com_sage, $filter['pattern']) !== false) { + $matched_filter = $filter; + break; + } + } + } + } + // Regex filter + if ($filter['regex']) { + if ($min_count > 1) { + if (preg_match_all($filter['pattern'], $expanded_com) >= $min_count) { + $matched_filter = $filter; + break; + } + } + else { + if (preg_match($filter['pattern'], $expanded_com) === 1) { + $matched_filter = $filter; + break; + } + } + } + // String filter + else { + if ($min_count > 1) { + if (substr_count($normalized_com, $filter['pattern']) >= $min_count) { + $matched_filter = $filter; + break; + } + } + else { + if (strpos($normalized_com, $filter['pattern']) !== false) { + $matched_filter = $filter; + break; + } + } + } + } + + if ($matched_filter !== false) { + // Update hit stats + register_postfilter_hit($matched_filter['id']); + + // Autosage + if ($matched_filter['autosage']) { + return true; + } + // Log + else if ($matched_filter['log']) { + log_postfilter_hit($matched_filter, $board, $resto, $name, $sub, $com, $upfile_name); + } + // Reject + else { + if ($matched_filter['ban_days']) { + $err = S_BANNEDTEXT; + $ban_days = (int)$matched_filter['ban_days']; + $private_reason = 'banned string in comment (filter ID: ' . $matched_filter['id'] . ')'; + $public_reason = $err; + auto_ban_poster($name, $ban_days, 1, $private_reason, $public_reason, true, $pwd, $pass_id); + } + else { + $err = S_REJECTTEXT; + } + + if ($matched_filter['quiet']) { + show_post_successful_fake($resto); + die(); + } + + if (TEST_BOARD) { + $err .= ' (filter ID: ' . $matched_filter['id'] . ')'; + } + + error($err); + } + } + + // Other + if ($sub !== '') { + $normalized_sub = normalize_text($sub); + + if (stripos($sub, 'moot') !== false) { + error("You can't post with that subject."); + } + + if (stripos($normalized_com, '##') !== false || stripos($sub, 'admin') !== false) { + error("You can't post with that subject."); + } + } + + return false; +} + +function isIPRangeBannedReport($long_ip, $asn, $board, $userpwd = null) { + return isIPRangeBanned($long_ip, $asn, + [ + 'board' => $board, + 'is_report' => true, + 'userpwd' => $userpwd, + ] + ); +} + +// Checks if the IP is rangebanned +// options: +// board(string), is_sfw(bool, requires board) +// userpwd(UserPwd): instance of UserPwd or null, +// is_report(bool), is_op(bool), has_img(bool), +// browser_id(string), +// op_content(string): content of the thread OP for per-thread bans (unused) +// returns the rangeban database entry if the IP is banned, false otherwise +function isIPRangeBanned($long_ip, $asn, $options = []) { + $long_ip = (int)$long_ip; + + $asn = (int)$asn; + + $now = (int)$_SERVER['REQUEST_TIME']; + + $cols = 'created_on, updated_on, expires_on, active, boards, ops_only, img_only, lenient, report_only, ua_ids'; + + $query = <<= $long_ip AND active = 1 +AND (expires_on = 0 OR expires_on > $now)) +SQL; + + if ($asn > 0) { + $query .= << $now)) +SQL; + } + + $query .= ' ORDER BY lenient ASC'; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + // Parameters + if (isset($options['board'])) { + $board = $options['board']; + $is_sfw = isset($options['is_sfw']) && $options['is_sfw']; + } + else { + $board = null; + $is_sfw = false; + } + + if (isset($options['browser_id'])) { + $browser_id = $options['browser_id']; + } + else { + $browser_id = null; + } + + if (isset($options['req_sig'])) { + $req_sig = $options['req_sig']; + } + else { + $req_sig = null; + } + + if (isset($options['op_content']) && $options['op_content'] !== '') { + $op_content = $options['op_content']; + } + else { + $op_content = null; + } + + $is_op = isset($options['is_op']) && $options['is_op']; + $is_report = isset($options['is_report']) && $options['is_report']; + $has_img = isset($options['has_img']) && $options['has_img']; + + if (isset($options['userpwd']) && $options['userpwd'] && $options['userpwd'] instanceof UserPwd) { + $userpwd = $options['userpwd']; + } + else { + $userpwd = null; + } + + // OP-only and Image-only lenient rangebans also require a certain number of posts + $post_count_ok = $userpwd && $userpwd->postCount() >= 3 && ($userpwd->maskLifetime() > 900 || $userpwd->postCount() >= 15); + + while ($range = mysql_fetch_assoc($res)) { + if ($range['boards']) { + if ($board === null) { + continue; + } + + $board_matcher = ",{$range['boards']},"; + + if (strpos($board_matcher, ",$board,") === false) { + // _ws_ scope affects all work safe boards + if ($is_sfw) { + if (strpos($board_matcher, ",_ws_,") === false) { + continue; + } + } + else { + continue; + } + } + } + + $post_count_check = true; + + if ($range['report_only'] && !$is_report) { + continue; + } + + if ($range['ops_only']) { + if (!$is_op) { + continue; + } + else { + $post_count_check = $post_count_ok; + } + } + + if ($range['img_only']) { + if (!$has_img) { + continue; + } + else { + $post_count_check = $post_count_ok; + } + } + + if ($range['ua_ids']) { + $_skip = true; + + if ($browser_id && strpos($range['ua_ids'], $browser_id) !== false) { + $_skip = false; + } + + if ($_skip && $req_sig && strpos($range['ua_ids'], $req_sig) !== false) { + $_skip = false; + } + + if ($_skip) { + continue; + } + } + + if ($userpwd && $range['lenient']) { + $lenient = (int)$range['lenient']; + + if ($range['updated_on']) { + $since_ts = (int)$range['updated_on']; + } + else { + $since_ts = (int)$range['created_on']; + } + + // Mode 1: Known 24h or Verified + if ($lenient === 1 && ($userpwd->verifiedLevel() || ($userpwd->isUserKnown(1440, $since_ts) && $post_count_check))) { + continue; + } + // Mode 2: Known 24h only + else if ($lenient === 2 && $userpwd->isUserKnown(1440, $since_ts) && $post_count_check) { + continue; + } + // Mode 3: Verified only + else if ($lenient === 3 && $userpwd->verifiedLevel()) { + continue; + } + } + + return $range; + } + + return false; +} + +/** + * Checks if the IP has enough posting history + * $mode: 0 = check for replies, 1 = check for image replies, 2 = check of threads + * Caches results. + */ +function spam_filter_is_ip_known($long_ip, $board = null, $mode = 0, $minutes_min = 0, $posts_min = 1) { + static $cache = array(); + + $long_ip = (int)$long_ip; + + if (!$long_ip) { + return false; + } + + $cache_key = "$long_ip.$board.$mode.$minutes_min.$posts_min"; + + if (isset($cache[$cache_key])) { + return $cache[$cache_key]; + } + + // Not after (3 days) + $minutes_max = 4320; + + // Not before + $minutes_min = (int)$minutes_min; + + // At least X replies + $posts_min = (int)$posts_min; + + // Board + if ($board) { + $board_clause = "AND board = '" . mysql_real_escape_string($board) . "'"; + } + else { + $board_clause = ''; + } + + // Mode: 1 = image replies, 2 = threads, 0 = any reply + if ($mode === 1) { + $action_clause = "AND action = 'new_reply' AND had_image = 1"; + } + else if ($mode === 2) { + $action_clause = "AND action = 'new_thread'"; + } + else { + $action_clause = "AND action = 'new_reply'"; + } + + // Not before + if (!$minutes_min) { + $time_clause = "time >= DATE_SUB(NOW(), INTERVAL $minutes_max MINUTE)"; + } + else { + $time_clause = "(time BETWEEN DATE_SUB(NOW(), INTERVAL $minutes_max MINUTE) AND DATE_SUB(NOW(), INTERVAL $minutes_min MINUTE))"; + } + + // Check posting history + $query = <<= DATE_SUB(NOW(), INTERVAL 48 HOUR) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count > 0) { + $cache[$cache_key] = false; + return false; + } + */ + + $cache[$cache_key] = true; + + return true; +} + +/* + * Checks if the user has a valid posting history to bypass a rangeban (FIXME: deprecated) + */ +function spam_filter_is_user_known($long_ip, $board = null, $pwd = null, $minutes = 15, $count = 1) { + static $cache = array(); + + $pwd = null; // FIXME + + $interval = (int)$minutes; + + $cache_key_ip = "{$long_ip}:{$interval}"; + + if (isset($cache[$cache_key_ip])) { + return $cache[$cache_key_ip]; + } + + if ($pwd && $board) { + $cache_key_pwd = "{$board}:{$pwd}:{$interval}"; + + if (isset($cache[$cache_key_pwd])) { + return $cache[$cache_key_pwd]; + } + } + + $count = (int)$count; + + if ($count < 1) { + $count = 1; + } + + // Check the IP + $query = << BOARD_DIR, + 'is_sfw' => DEFAULT_BURICHAN, + 'userpwd' => $userpwd, + 'is_op' => $thread_id == 0, + 'has_img' => $has_img, + 'browser_id' => $browser_id, + 'req_sig' => $req_sig + ]; + + if ($range = isIPRangeBanned($long_ip, $asn, $options)) { + if ($range['lenient'] && $userpwd) { + $userpwd->setCookie('.' . L::d(BOARD_DIR)); + } + + // Images only + if ($range['img_only']) { + $_err = S_IPRANGE_BLOCKED_IMG; + } + // Threads only + else if ($range['ops_only']) { + $_err = S_IPRANGE_BLOCKED_OP; + } + else { + $_err = S_IPRANGE_BLOCKED; + } + + // Temporarily or Permanently + if ($range['expires_on'] || $range['lenient']) { + $_err .= ' ' . S_IPRANGE_BLOCKED_TEMP; + } + else { + $_err .= ' ' . S_IPRANGE_BLOCKED_PERM; + } + + // Bypassed by verified or known users + if ($range['lenient'] == 1) { + $_err .= S_IPRANGE_BLOCKED_L1; + } + // Bypassed by known users only + else if ($range['lenient'] == 2) { + $_err .= S_IPRANGE_BLOCKED_L2; + } + // Bypassed by verified users only + else if ($range['lenient'] == 3) { + $_err .= S_IPRANGE_BLOCKED_L3; + } + + // 4chan pass mention + $_err .= S_IPRANGE_BLOCKED_PASS; + + error($_err); + } + + // Auto-rangebans, Mobile only + // Bypassed by verified users or known users for at least 2h + // or users who have made at least one post on the board 15 minutes ago + if ($thread_id !== null && $browser_id[0] === '1') { + $since_ts = 0; + + if ($userpwd) { + $user_known = $userpwd->isUserKnownOrVerified(120, 1); + + $now = $_SERVER['REQUEST_TIME']; + + if ($userpwd->postCount() > 0 && $userpwd->maskTs() <= $now - 900) { + $since_ts = $userpwd->maskTs(); + } + } + else { + $user_known = false; + } + + if (!$user_known) { + if (is_ip_auto_rangebanned($ip, BOARD_DIR, $thread_id, $browser_id, $since_ts)) { + write_to_event_log('auto_range_hit', $ip, [ + 'board' => BOARD_DIR, + 'thread_id' => $thread_id, + 'ua_sig' => $browser_id + ]); + + if ($userpwd) { + $userpwd->setCookie('.' . L::d(BOARD_DIR)); + } + + // Temporary, bypassablee by known or verified users + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1 . S_IPRANGE_BLOCKED_PASS); + } + } + } + + return false; +} + +function is_ip_auto_rangebanned($ip, $board, $thread_id, $browser_id, $since_ts = 0) { + $range_sql = explode('.', $ip); + + $range_sql = "{$range_sql[0]}.{$range_sql[1]}.%"; + + $thread_id = (int)$thread_id; + + if ($since_ts > 0) { + $since_sql = ' AND created_on <= FROM_UNIXTIME(' . ((int)$since_ts) . ')'; + } + else { + $since_sql = ''; + } + + $sql =<< DATE_SUB(NOW(), INTERVAL 120 MINUTE)$since_sql +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $board, $browser_id, $range_sql); + + if (!$res) { + return false; + } + + if (mysql_num_rows($res)) { + return true; + } + + return false; +} + +/** + * Dumps and formats HTTP headers and other request information for logging + */ +function spam_filter_format_http_headers($com = null, $country = null, $filename = null, $threat_score = null, $req_sig = null) { + $bot_headers = ''; + + foreach ($_SERVER as $_h_name => $_h_val) { + if (substr($_h_name, 0, 5) == 'HTTP_') { + if ($_h_name === 'HTTP_COOKIE') { + $_cookies = array_keys($_COOKIE); + $_cookies = array_intersect($_cookies, ['ws_style', 'nws_style', '4chan_pass', '_tcs', '_ga', 'cf_clearance' ]); + $_cookie_count = count($_COOKIE); + $bot_headers .= "HTTP_COOKIE: " . htmlspecialchars(implode(', ', $_cookies)) . " ($_cookie_count in total)\n"; + } + else if (strpos($_h_name, 'AUTH') !== false) { + continue; + } + else { + $bot_headers .= "$_h_name: " . htmlspecialchars($_h_val) . "\n"; + } + } + } + + $bot_headers .= "_POST: " . htmlspecialchars(implode(', ', array_keys($_POST))) . "\n"; + + if ($country !== null) { + $bot_headers .= "_Country: $country\n"; + } + + if ($threat_score !== null) { + $bot_headers .= "_Score: " . $threat_score . "\n"; + } + + if ($req_sig !== null) { + $bot_headers .= "_Sig: " . $req_sig . "\n"; + } + + if (isset($_COOKIE['_tcs'])) { + $bot_headers .= "_TCS: " . htmlspecialchars($_COOKIE['_tcs']) . "\n"; + } + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = UserPwd::getSession(); + + if ($userpwd) { + $bot_headers .= "_Pwd: " . htmlspecialchars($userpwd->getPwd()) . "\n"; + } + } + + if ($filename !== null) { + $bot_headers .= "_File: " . htmlspecialchars($filename) . "\n"; + } + + if ($com !== null) { + $bot_headers .= "_Comment: $com"; + } + + return $bot_headers; +} + +function spam_filter_get_req_sig() { + static $cache = null; + + if ($cache !== null) { + return $cache; + } + + $pick_headers = [ + 'HTTP_SEC_CH_UA_PLATFORM', + 'HTTP_SEC_CH_UA_MOBILE', + 'HTTP_SEC_CH_UA_MODEL', + 'HTTP_USER_AGENT', + 'HTTP_ACCEPT_LANGUAGE', + 'HTTP_SEC_FETCH_SITE', + 'HTTP_SEC_FETCH_MODE', + 'HTTP_SEC_FETCH_DEST', + ]; + + $need_headers = [ + 'HTTP_USER_AGENT', + 'HTTP_ACCEPT', + 'HTTP_REFERER', + 'HTTP_ACCEPT_LANGUAGE', + ]; + + $datapoints = []; + + $keys = []; + + foreach ($_SERVER as $k => $v) { + if (in_array($k, $pick_headers)) { + $keys[] = $k; + } + } + + $keyline = implode('.', $keys); + + $pointlines = [ + 'fetch_smd' => 'HTTP_SEC_FETCH_SITE.HTTP_SEC_FETCH_MODE.HTTP_SEC_FETCH_DEST', + 'fetch_dms' => 'HTTP_SEC_FETCH_DEST.HTTP_SEC_FETCH_MODE.HTTP_SEC_FETCH_SITE', + 'fetch_any' => 'HTTP_SEC_FETCH_' + ]; + + foreach ($pointlines as $key => $value) { + if (strpos($keyline, $value) !== false) { + $datapoints[] = $key; + } + } + + if (isset($_SERVER['HTTP_SEC_GPC']) || isset($_SERVER['HTTP_DNT'])) { + $datapoints[] = 'dnt_gpc'; + } + + if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { + $datapoints[] = 'xrw'; + } + + if (preg_match('/HTTP_SEC_CH_UA_[^.]+\.HTTP_SEC_CH_UA_[^.]+\.HTTP_SEC_CH_UA_/', $keyline)) { + $datapoints[] = 'ch_ua_block'; + } + + foreach ($need_headers as $k) { + if (!isset($_SERVER[$k])) { + $datapoints[] = 'missing'; + break; + } + } + + $sig = implode('+', $datapoints); + + if (!$sig) { + $sig = 'deadbeef'; + } + + $cache = substr(md5($sig), 0, 8); + + return $cache; +} + +// Covers 251044 (79.14 %) unique IPs and 10454 (77.09 %) unique bans +// IPs %: 0.1 | Bans %: 0 | GR1 all: 0 | Any EU: false +function spam_filter_is_asn_whitelisted() { + static $val = null; + + static $whitelist = [ + 21928, 6167, 7922, 7018, 812, 701, 1221, 20115, 22773, 2856, 3320, 6805, 577, + 3209, 852, 20057, 20001, 5089, 4804, 10796, 8151, 5617, 7545, 11427, 209, 16086, + 719, 33363, 15557, 5650, 133612, 6327, 6128, 5607, 206067, 1267, 26599, 6830, + 8881, 2119, 14593, 28573, 3215, 26615, 3352, 1759, 7303, 11426, 35228, 55836, + 1136, 22085, 11351, 8708, 1257, 3269, 5410, 7418, 23693, 30722, 9299, 13285, 17676, + 3301, 7713, 5391, 33915, 44034, 8374, 4764, 51207, 29447, 27651, 7552, 12389, + 19108, 8359, 11315, 6057, 16591, 12479, 5769, 17072, 31615, 12271, 28403, 11664, + 6147, 15704, 10139, 39603, 12874, 25135, 5483, 5378, 4771, 4775, 12912, 6871, + 12322, 6614, 132199, 5432, 212238, 12929, 27699, 22927, 8473, 2860, 12430, 7029, + 6848, 5645, 8412, 62240, 8447, 15502, 174, 30036, 27747, 14638, 4230, 45727, 9009, + 17639, 4788, 10030, 11492, 45143, 18881, 2516, 21334, 15895, 4766, 16232, 6799, + 8400, 9443, 6079, 13999, 5610, 45899, 4761, 2586, 12353, 20845, 20365, 13280, + 22047, 3243, 4818, 27995, 20055, 24203, 9500, 25255, 45609, 29518, 7992, 39891, + 3303, 4773, 855, 8452, 136787, 18403, 3329, 52341, + ]; + + if ($val !== null) { + return $val; + } + + if (isset($_SERVER['HTTP_X_GEO_ASN'])) { + $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; + } + else { + $asn = 0; + } + + if (!$asn) { + return true; + } + + $val = in_array($asn, $whitelist); + + return $val; +} + +function spam_filter_is_bad_actor() { + static $cache = null; + + if ($cache !== null) { + return $cache; + } + /* + if (isset($_SERVER['HTTP_X_HTTP_VERSION'])) { + if (strpos($_SERVER['HTTP_X_HTTP_VERSION'], 'HTTP/1') === 0) { + $cache = true; + return true; + } + } + */ + $no_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) === false; + $no_accept = isset($_SERVER['HTTP_ACCEPT']) === false; + + if ($no_lang && $no_accept) { + $cache = true; + return true; + } + + if ($no_lang && strpos($_SERVER['HTTP_USER_AGENT'], '; wv)') !== false) { + $cache = true; + return true; + } + + if ($no_accept && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') === false) { + $cache = true; + return true; + } + + if ($no_lang && isset($_SERVER['HTTP_REFERER'])) { + $ref = $_SERVER['HTTP_REFERER']; + + if (strpos($ref, 'sys.4chan.org') !== false || strpos($ref, '/thread/') !== false) { + $cache = true; + return true; + } + } + + $cache = false; + return false; +} + +function spam_filter_get_threat_score($country = null, $is_op = false, $multipart = true/*, &$log = []*/) { + $increase = []; + $more = []; + + $domain = DEFAULT_BURICHAN ? '4channel' : '4chan'; + + if (isset($_SERVER['HTTP_USER_AGENT'])) { + $ua = $_SERVER['HTTP_USER_AGENT']; + } + else { + $ua = ''; + } + + if (isset($_SERVER['HTTP_CONTENT_TYPE'])) { + $content_type = $_SERVER['HTTP_CONTENT_TYPE']; + } + else { + $content_type = ''; + } + + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $accept_lang = $_SERVER['HTTP_ACCEPT_LANGUAGE']; + } + else { + $accept_lang = ''; + } + + if (isset($_SERVER['HTTP_ACCEPT'])) { + $accept_header = $_SERVER['HTTP_ACCEPT']; + } + else { + $accept_header = ''; + } + + if (isset($_SERVER['HTTP_REFERER'])) { + $referer_header = $_SERVER['HTTP_REFERER']; + } + else { + $referer_header = ''; + } + + $header_keys = array_keys($_SERVER); + + $ua_is_webkit = false; + + $check_for_sec_headers = false; + + $is_mobile_ua = preg_match('/Android|Mobile/', $ua) || $_SERVER['HTTP_SEC_CH_UA_MOBILE'] === '?1'; + + $is_brave = false; + + if (isset($_SERVER['HTTP_SEC_CH_UA']) && strpos($_SERVER['HTTP_SEC_CH_UA'], 'Brave') !== false) { + if (isset($_SERVER['HTTP_SEC_GPC'])) { + $is_brave = true; + } + } + + // Mobile app (webviews, etc) + $is_webview = strpos($ua, '; wv') !== false; + $is_mobile_app = !$accept_header && !$accept_lang && ($is_webview || strpos($referer_header, '/thread/') !== false); + + if (!$is_mobile_app && !$accept_header && $accept_lang && strpos($referer_header, 'sys.4chan.org') !== false) { + $is_mobile_app = true; + } + + if (!$is_mobile_app && strpos($ua, 'Mozilla/') === false && preg_match('/Android|Dalvik|iOS|iPhone/', $ua)) { + $is_mobile_app = true; + } + + if (!$is_mobile_app && isset($_SERVER['HTTP_X_REQUESTED_WITH']) && preg_match('/floens|adamantcheese|clover/', $_SERVER['HTTP_X_REQUESTED_WITH'])) { + $is_mobile_app = true; + } + + if (!$is_mobile_app && preg_match('/boundary=[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$/', $content_type)) { + if (strpos($ua, 'Android') !== false) { + $is_mobile_app = true; + } + else if (strpos($ua, 'Firefox/') !== false && !$accept_lang) { + $is_mobile_app = true; + } + } + + // No UA + if (!$ua) { + $increase[] = 0.25; + //$log[] = 'NO_UA'; + } + // Firefox + else if ((strpos($ua, 'Firefox/') !== false || strpos($ua, 'FxiOS/') !== false) && strpos($ua, 'WebKit') === false) { + // Suspicious Content-Type + if ($multipart && !$is_mobile_app && !preg_match('/=-+([0-9]+|geckoformboundary[a-f0-9]+)$/i', $content_type)) { + $increase[] = 0.25; + //$log[] = 'BAD_CT_FF'; + } + + // Suspicious language + if (!$accept_lang) { + if (!$is_mobile_app) { + $increase[] = 0.02; + $more[] = 0.1; + //$log[] = 'NO_LANG'; + } + } + else if (preg_match('/[a-z]-[a-z]/', $accept_lang)) { + $increase[] = 0.1; + //$log[] = 'LC_LANG'; + } + + if (isset($_SERVER['HTTP_PRAGMA']) && !isset($_SERVER['HTTP_CACHE_CONTROL'])) { + $increase[] = 0.35; + $more[] = 0.1; + //$log[] = 'FF_PRAGMA'; + } + + // Wrong Accept header + if (strpos($accept_header, 'application/signed-exchange') !== false) { + $increase[] = 0.15; + //$log[] = 'FF_SIGEX'; + } + + // Old and spoofed versions + if ($accept_header && preg_match('/(?:Firefox)\/([0-9]+)[^0-9]/', $ua, $m) && strpos($ua, 'PaleMoon') === false) { + $v = (int)$m[1]; + + if ($v < 52) { + $increase[] = 0.2; + //$log[] = 'OLD_FF'; + } + else if ($v < 60) { + $increase[] = 0.1; + //$log[] = 'OLD_FF'; + } + else if ($v < 78) { + $increase[] = 0.01; + //$log[] = 'OLD_FF'; + } + else if ($v > 500) { + $increase[] = 0.5; + //$log[] = 'FUTURE_FF'; + } + + if ($v > 110) { + $check_for_sec_headers = true; + } + } + } + // Webkit + else if (strpos($ua, 'WebKit') !== false) { + $ua_is_webkit = true; + $ua_is_chrome = strpos($ua, 'Chrome') !== false; + + // Suspicious Content-Type + if ($multipart && !$is_mobile_app) { + if (!strpos($content_type, 'WebKit')) { + $increase[] = 0.25; + //$log[] = 'BAD_CT_WK'; + } + else if (strpos($content_type, '-') === false) { + $increase[] = 0.50; + //$log[] = 'BAD_CT_DASH'; + } + } + + // Suspicious language + if (!$accept_lang) { + if (!$is_mobile_app) { + $increase[] = 0.02; + $more[] = 0.1; + //$log[] = 'NO_LANG'; + } + } + else if ($ua_is_chrome && strpos($ua, 'Android') === false && preg_match('/[a-z]-[a-z]/', $accept_lang)) { + $increase[] = 0.1; + //$log[] = 'LC_LANG'; + } + + // Old and spoofed versions + if (preg_match('/(?:Chrome)\/([0-9]+)[^0-9]/', $ua, $m)) { + $v = (int)$m[1]; + + if ($v < 60) { + $increase[] = 0.2; + //$log[] = 'OLD_WK'; + } + else if ($v < 70) { + $increase[] = 0.1; + //$log[] = 'OLD_WK'; + } + else if ($v < 80) { + $increase[] = 0.05; + //$log[] = 'OLD_WK'; + } + else if ($v > 500) { + $increase[] = 0.5; + //$log[] = 'FUTURE_WK'; + } + + if ($v > 110) { + $check_for_sec_headers = true; + } + } + + if (preg_match('/(?:Safari)\/([0-9]+)/', $ua, $m)) { + $v = (int)$m[1]; + + if ($v < 530) { + $increase[] = 0.5; + //$log[] = 'OLD_SAFARI'; + } + } + + // iPhone UA too short + if (strpos($ua, 'iPhone') !== false && strpos($ua, 'Mobile') === false) { + $increase[] = 0.06; + //$log[] = 'SHORT_IPHONE'; + } + } + // Other + else { + if (!$is_mobile_app && $multipart && preg_match('/boundary=[a-zA-Z0-9]+$/', $content_type)) { + $increase[] = 0.5; + //$log[] = 'STRANGE_CT'; + } + + if (preg_match('/Netscape\/|Opera\b|Camino\/|Trident\/|Presto\/|compatible; MSIE /', $ua)) { + $increase[] = 0.75; + //$log[] = 'OLD_UA'; + } + else if (!$is_mobile_app && strpos($ua, 'Mozilla/') === false) { + $increase[] = 0.15; + $more[] = 0.1; + //$log[] = 'STRANGE_UA'; + } + + if (!$is_mobile_app && !$accept_lang) { + $more[] = 0.25; + //$log[] = 'NO_LANG'; + } + + // UA too short + if (!$is_mobile_app && (strlen($ua) < 25 || strpos($ua, ' ') === false)) { + $increase[] = 0.25; + //$log[] = 'UA_SPOOF'; + } + } + + // Suspicious Content-Type + if ($multipart) { + if (strpos($content_type, 'WebKit') && !$ua_is_webkit) { + $increase[] = 0.25; + //$log[] = 'BAD_UA_CT_WK'; + } + } + + // Sec-Fetch headers should be together + // Some iPhones have those separated + if (!$is_brave && !$is_webview && strpos($ua, 'Chrome') !== false) { + $_sf_start = false; + $_sf_end = false; + + foreach ($_SERVER as $_hdr => $_value) { + if (strpos($_hdr, 'HTTP_SEC_FETCH_') === 0) { + if ($_sf_start && $_sf_end) { + $increase[] = 0.25; + //$log[] = 'SPARSE_SEC_FETCH'; + break; + } + + $_sf_start = true; + } + else if ($_sf_start) { + $_sf_end = true; + } + } + } + + // HTTP_SEC_FETCH_USER should always be ?1 + if (isset($_SERVER['HTTP_SEC_FETCH_USER']) && $_SERVER['HTTP_SEC_FETCH_USER'] !== '?1') { + $increase[] = 0.15; + //$log[] = 'BAD_SEC_FU'; + } + + // Unusual Accept header + if ($accept_header) { + if (strpos($accept_header, 'text/plain') !== false) { + $increase[] = 0.05; + //$log[] = 'ACCEPT_TP'; + } + } + + // Referer is set but is empty + if (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], '4chan.org') === false) { + $increase[] = 0.1; + //$log[] = 'BAD_REFERER'; + } + + // Platform mismatch: client hints vs user agent + if (isset($_SERVER['HTTP_SEC_CH_UA_PLATFORM']) && $ua) { + $_ch_platform = $_SERVER['HTTP_SEC_CH_UA_PLATFORM']; + + if (strpos($ua, 'Windows') !== false) { + if (strpos($_ch_platform, 'Windows') === false) { + $increase[] = 0.5; + //$log[] = 'CH_BAD_PLATFORM'; + } + } + else if (strpos($ua, 'Mac OS') !== false) { + if (strpos($_ch_platform, 'macOS') === false) { + $increase[] = 0.5; + //$log[] = 'CH_BAD_PLATFORM'; + } + } + else if (strpos($ua, 'Linux') !== false) { + if (preg_match('/Linux|Android|BSD/', $_ch_platform) === false) { + $increase[] = 0.5; + //$log[] = 'CH_BAD_PLATFORM'; + } + } + } + + if (isset($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS']) && $accept_header === '*/*') { + $increase[] = 0.2; + $more[] = 0.1; + //$log[] = 'ACCEPT_UIR'; + } + + if (!isset($_SERVER['HTTP_PRAGMA']) && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') === false) { + $increase[] = 0.09; + //$log[] = 'BAD_IPHONE_WV'; + } + + // Suspicious OS + if (strpos($ua, 'Windows NT ') !== false) { + if (preg_match('/Windows NT ([0-9]+)/', $ua, $m) && strpos($ua, 'Mypal/') === false) { + $v = (int)$m[1]; + + if ($v < 6) { + if (strpos($ua, 'Goanna') === false) { + $increase[] = 0.25; + } + else { + $increase[] = 0.03; + } + //$log[] = 'OLD_WIN'; + } + else if ($v < 10) { + $increase[] = 0.03; + //$log[] = 'OLD_WIN'; + } + else if ($v > 10) { + $increase[] = 0.5; + //$log[] = 'FUTURE_WIN'; + } + } + } + else if (strpos($ua, 'Mac OS X ') !== false) { + if (preg_match('/Mac OS X ([0-9]+)_([0-9]+)/', $ua, $m)) { + $v_maj = (int)$m[1]; + $v_min = (int)$m[2]; + + if ($v_maj < 10) { + $increase[] = 0.5; + //$log[] = 'OLD_OSX'; + } + else if ($v_maj == 10) { + if ($v_min < 7) { + $increase[] = 0.25; + //$log[] = 'OLD_OSX'; + } + else if ($v_min < 12) { + $increase[] = 0.05; + //$log[] = 'OLD_OSX'; + } + } + else if ($v_maj > 10 && strpos($ua, 'Safari') !== false) { + $increase[] = 0.30; + //$log[] = 'FUTURE_OSX'; + } + } + } + else if (strpos($ua, 'Android') !== false) { + if (preg_match('/Android ([0-9]+)/', $ua, $m)) { + $v = (int)$m[1]; + + if ($v < 4) { + $increase[] = 0.25; + //$log[] = 'OLD_DROID'; + } + else if ($v < 8) { + $increase[] = 0.05; + //$log[] = 'OLD_DROID'; + } + else if ($v > 20) { + $increase[] = 0.5; + //$log[] = 'FUTURE_DROID'; + } + } + + if (strpos($ua, 'Win64;') !== false) { + $increase[] = 0.25; + //$log[] = 'OS_SOUP'; + } + } + + // Spoofed OS + if (preg_match('/Mozilla|Firefox|Chrome/', $ua) && !preg_match('/Windows NT|Android|Linux|Mac|iOS|X11;|BSD|Nintendo|PlayStation|Steam/', $ua)) { + $increase[] = 0.20; + //$log[] = 'NO_OS'; + } + + // Non-browser user agents + if (preg_match('/headless|node-fetch|python-|java\/|jakarta|-perl|http-?client|-resty-|awesomium\//i', $ua)) { + $increase[] = 1.0; + //$log[] = 'NOT_BROWSER'; + } + + // Wrong content type + if ($multipart) { + // Posting + if ($_SERVER['HTTP_CONTENT_TYPE'] === 'application/x-www-form-urlencoded') { + $increase[] = 0.75; + //$log[] = 'BAD_CT_MP'; + } + } + else if (!$is_mobile_app) { + // Reporting + if ($_SERVER['HTTP_CONTENT_TYPE'] !== 'application/x-www-form-urlencoded') { + $increase[] = 0.75; + //$log[] = 'BAD_CT_NMP'; + } + } + + // Unusual headers + if (isset($_SERVER['HTTP_VARY'])) { + $increase[] = 0.2; + //$log[] = 'VARY_HDR'; + } + + if (isset($_SERVER['HTTP_PATH']) || isset($_SERVER['HTTP_SAME_ORIGIN']) || isset($_SERVER['HTTP_REFERRER_POLICY'])) { + $increase[] = 0.8; + //$log[] = 'USELESS_HDR'; + } + + if (!$is_mobile_app && !$is_webview) { + if (isset($_SERVER['HTTP_SEC_FETCH_MODE']) && $_SERVER['HTTP_SEC_FETCH_MODE'] === 'navigate') { + // Only threads should be posted using the default form + if (!$is_op && !$is_mobile_ua) { + $increase[] = 0.02; + $more[] = 0.10; + //$log[] = 'BAD_OP_SFM'; + } + + // Model hints are never sent when using the default form + if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && !isset($_SERVER['HTTP_SEC_CH_UA_BITNESS'])) { + $increase[] = 0.1; + $more[] = 0.20; + //$log[] = 'MODEL_NAV'; + } + } + } + + // iPhone fetch site none + if (strpos($ua, 'like Mac OS') && isset($_SERVER['HTTP_SEC_FETCH_SITE']) && $_SERVER['HTTP_SEC_FETCH_SITE'] === 'none') { + $increase[] = 0.08; + $more[] = 0.10; + //$log[] = 'IOS_FSN'; + } + + // No cookies + if (!$is_mobile_app) { + if (!isset($_SERVER['HTTP_COOKIE']) || !$_SERVER['HTTP_COOKIE']) { + $increase[] = 0.05; + $more[] = 0.25; + //$log[] = 'NO_COOKIE'; + } + else if (count($_COOKIE) === 1 && isset($_COOKIE['cf_clearance'])) { + $increase[] = 0.05; + $more[] = 0.25; + //$log[] = 'NO_COOKIE'; + } + } + + // Timezones and Time + if (isset($_COOKIE['_tcs'])) { + list($_time, $_tz, $_time_s, $_tcs_v) = explode('.', $_COOKIE['_tcs']); + + if (!$_tcs_v) { + $increase[] = 0.09; + //$log[] = 'BAD_TCS'; + } + else { + if (!$is_webview && strpos($ua, 'Chrome/') !== false && $_tcs_v != 33) { + $increase[] = 0.09; + //$log[] = 'BAD_TCS_CR'; + } + } + + if ($_time_s && isset($_POST['t-challenge']) && $_POST['t-challenge'] !== 'noop' && $_POST['t-response']) { + $_d = $_SERVER['REQUEST_TIME'] - $_time_s; + + if ($_d > 0 && $_d < 2) { + $increase[] = 1.0; + //$log[] = 'FAST_TCS'; + } + } + + if (isset($_SERVER['HTTP_X_TIMEZONE'])) { + $_tz0 = explode('/', $_tz, 2)[0]; + if ($_tz0) { + if ($_tz0 === 'UTC') { + $increase[] = 0.02; + $more[] = 0.02; + //$log[] = 'UTC_TZ'; + } + else if ($_tz0 === 'Etc') { + $increase[] = 0.01; + $more[] = 0.01; + //$log[] = 'ETC_TZ'; + } + else if (strpos($_SERVER['HTTP_X_TIMEZONE'], $_tz0) !== 0) { + if (strpos($_tz0, 'Atlantic') === false || strpos($_SERVER['HTTP_X_TIMEZONE'], 'Europe') === false) { + $increase[] = 0.03; + $more[] = 0.03; + //$log[] = 'BAD_TZ'; + } + } + } + } + } + + // No Accept + if (!$accept_header && !$is_mobile_app) { + $increase[] = 0.15; + //$log[] = 'NO_ACCEPT'; + } + + // No SEC + if ($check_for_sec_headers && !$is_mobile_app) { + if (!isset($_SERVER['HTTP_SEC_FETCH_DEST']) || !isset($_SERVER['HTTP_SEC_FETCH_MODE']) || !isset($_SERVER['HTTP_SEC_FETCH_SITE'])) { + $increase[] = 0.15; + //$log[] = 'NO_SEC'; + } + } + + // HTTP 1.1 + if (isset($_SERVER['HTTP_X_HTTP_VERSION']) && $_SERVER['HTTP_X_HTTP_VERSION'] === 'HTTP/1.1') { + $more[] = 0.1; + //$log[] = 'HTTP1'; + } + + // Spoofed device model + if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && isset($_SERVER['HTTP_SEC_CH_UA_PLATFORM'])) { + $_k1 = (int)array_search('HTTP_SEC_CH_UA_MODEL', $header_keys); + $_k2 = (int)array_search('HTTP_SEC_CH_UA_PLATFORM', $header_keys); + + if (abs($_k1 - $_k2) > 5) { + $increase[] = 0.05; + $more[] = 0.01; + //$log[] = 'FAR_MODEL'; + } + } + + if ($country) { + // Language is set but doesn't match the country + if ($accept_lang && strpos($accept_lang, 'en') !== 0) { + $lang_regex = get_lang_regex_from_country($country); + + if ($lang_regex && !preg_match($lang_regex, $accept_lang)) { + $more[] = 0.025; + $increase[] = 0.025; + //$log[] = 'LANG_MISMATCH'; + } + + // No quality in lang + if (!preg_match('/iPhone|iPad/', $ua)) { + if ($accept_lang && strpos($accept_lang, ';') === false) { + $more[] = 0.025; + $increase[] = 0.025; + //$log[] = 'LANG_NOQ'; + } + } + } + + // Highly suspicious countries + $countries_0 = array( + 'AD','AE','AF','AG','AI','AL','AM','AN','AO','AQ','AS','AW','AX','AZ', + 'BB','BD','BF','BH','BI','BJ','BL','BM','BN','BO','BQ','BS','BT','BV','BW','BZ', + 'CC','CD','CF','CG','CI','CK','CM','CN','CR','CU','CV','CW','CX', + 'DJ','DM','DO','DZ', + 'EC','EG','EH','ER','ET', + 'FJ','FK','FM','FO', + 'GA','GD','GF','GG','GH','GI','GM','GN','GP','GQ','GS','GT','GU','GW','GY', + 'HK','HM','HN','HT', + 'IM','IO','IQ','IR','JE','JM','JO','KE','KG','KH','KI','KM','KN','KP','KW','KY','KZ', + 'LA','LB','LC','LK','LR','LS','LY', + 'MA','MD','MF','MG','MH','ML','MM','MN','MO','MP','MQ','MR','MS','MU','MV','MW','MZ', + 'NA','NC','NE','NF','NG','NI','NP','NR','NU', + 'OM','PA','PF','PG','PK','PM','PN','PS','PW','PY','QA','RE','RW', + 'SA','SB','SC','SD','SH','SJ','SL','SM','SN','SO','SR','SS','ST','SV','SX','SY','SZ', + 'TC','TD','TF','TG','TJ','TK','TM','TN','TO','TP','TR','TT','TV','TZ', + 'UG','UM','UZ','VA','VC','VG','VI','VU','WF','WS','YE','YT','YU','ZM','ZW','XX' + ); + + // Less suspicious countries + $countries_1 = array( + 'BR','VE','AR','CL','UY','CO','PE','MX', + 'UA','BA','RU','MC','MK','CY','MT','ME','KR','JP','TH','VN','ID' + ); + + if (in_array($country, $countries_0)) { + $more[] = 0.30; + //$log[] = 'SUSP_COUNTRY_0'; + } + else if (in_array($country, $countries_1)) { + $more[] = 0.10; + //$log[] = 'SUSP_COUNTRY_1'; + } + } + + $score = 0.0; + + if (!empty($increase)) { + $score += array_sum($increase); + } + + if ($score > 0.0 && !empty($more)) { + foreach ($more as $r) { + $score *= (1.0 + $r); + } + } + + return round($score, 2); +} + +/** + * Necrobumping prevention checks + */ +function spam_filter_can_bump_thread($thread_root) { + $userpwd = UserPwd::getSession(); + + if (!$userpwd || !$thread_root) { + return true; + } + + if ($userpwd->maskLifetime() >= 21600) { // 6 hours + return true; + } + + $thres = (int)(PAGE_MAX * DEF_PAGES * 0.85); + + if ($thres <= 0) { + return true; + } + + $sql = "SELECT COUNT(*) FROM `%s` WHERE resto = 0 AND archived = 0 AND root > '%s'"; + + $res = mysql_board_call($sql, BOARD_DIR, $thread_root); + + if (!$res) { + return true; + } + + $pos = (int)mysql_fetch_row($res)[0]; + + if ($pos < $thres) { + return true; + } + + // Check the IP if cookies are blocked + if (spam_filter_is_ip_known(ip2long($_SERVER['REMOTE_ADDR']), BOARD_DIR, 0, 720)) { + return true; + } + + return false; +} + +/** + * Returns true if the uploaded file had multiple previous bans for it + * and should be blocked + */ +function check_for_banned_upload($md5) { + if (!$md5 || BOARD_DIR === 'b') { + return false; + } + + // 6 : Global 5 - NWS on Worksafe Board + // 226 : Global 3 - Loli/shota pornography + $template_clause = '226'; + + if (DEFAULT_BURICHAN) { + $template_clause .= ',6'; + } + + $sql = <<= 3) { + return true; + } + + return false; +} + +function spam_filter_is_likely_automated($memcached = null, $threshold = 29) { + if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { + return false; + } + + $ua = $_SERVER['HTTP_USER_AGENT']; + + // Skip Android Webviews + if (strpos($ua, '; wv)') !== false) { + return false; + } + + // Skip iPhone Webviews + if (preg_match('/iPhone|iPad/', $ua) && !preg_match('/Mobile|Safari/', $ua)) { + return false; + } + + $score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + + if ($score > 0 && $score <= $threshold) { + return true; + } + + if ($memcached) { + $key = 'bmbot' . $_SERVER['REMOTE_ADDR']; + + if ($memcached->get($key)) { + return true; + } + } + + return false; +} + +function spam_filter_is_pwd_blocked($pwd, $type, $hours = 24) { + if (!$pwd || !$type || $hours <= 0 || $hours > 720) { + return false; + } + + $hours = (int)$hours; + + $sql =<< DATE_SUB(NOW(), INTERVAL $hours HOUR) +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $type, $pwd); + + if (!$res) { + return false; + } + + return mysql_num_rows($res) === 1; +} + +function spam_filter_has_country_changed($pwd) { + if (!$pwd) { + return false; + } + + $sql =<< DATE_SUB(NOW(), INTERVAL 24 HOUR) +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $pwd); + + if (!$res) { + return false; + } + + return mysql_num_rows($res) === 1; +} + +// Logs posts made by new users. +// Returns 1 if the posting rate is above limits. +function spam_filter_is_post_flood($ip, $userpwd, $board, $thread_id, $phash) { + $user_is_known = $userpwd && $userpwd->isUserKnownOrVerified(60); + + if ($user_is_known) { + return 0; + } + + $thread_id = (int)$thread_id; + + // Per thread reply flood + if ($thread_id) { + $interval_minutes = (int)ANTIFLOOD_INTERVAL_REPLY; + $threshold = (int)ANTIFLOOD_THRES_REPLY; + } + // OP flood + else { + $interval_minutes = (int)ANTIFLOOD_INTERVAL_OP; + $threshold = (int)ANTIFLOOD_THRES_OP; + } + + if (!$interval_minutes || !$threshold) { + return 0; + } + + $long_ip = ip2long($ip); + + if (!$long_ip) { + return 0; + } + + $tbl = 'flood_log'; + + // Prune old entries + $sql = "DELETE FROM `$tbl` WHERE created_on < DATE_SUB(NOW(), INTERVAL 24 HOUR)"; + mysql_global_call($sql); + + // Count flood entries + $ret_val = 0; + + $sql = <<= DATE_SUB(NOW(), INTERVAL $interval_minutes MINUTE) +AND board = '%s' +AND thread_id = $thread_id +SQL; + + $res = mysql_global_call($sql, $ip, $board); + + if ($res) { + $count = (int)mysql_fetch_row($res)[0]; + + if ($count >= $threshold) { + $ret_val = 1; + } + } + + // Insert new entry + $ua_sig = spam_filter_get_browser_id(); + $req_sig = spam_filter_get_req_sig(); + + $sql = << '/\b(?:ca)\b/', + 'AE' => '/\b(?:ar|fa|hi|ur)\b/', + 'AF' => '/\b(?:fa|ps|uz|tk)\b/', + 'AL' => '/\b(?:sq|el)\b/', + 'AM' => '/\b(?:hy)\b/', + 'AO' => '/\b(?:pt)\b/', + 'AR' => '/\b(?:es|it|de|fr|gn)\b/', + 'AS' => '/\b(?:sm|to)\b/', + 'AT' => '/\b(?:de|hr|hu|sl)\b/', + 'AW' => '/\b(?:nl|pap|es)\b/', + 'AX' => '/\b(?:sv)\b/', + 'AZ' => '/\b(?:az|ru|hy)\b/', + 'BA' => '/\b(?:bs|hr|sr)\b/', + 'BD' => '/\b(?:bn)\b/', + 'BE' => '/\b(?:nl|fr|de)\b/', + 'BF' => '/\b(?:fr|mos)\b/', + 'BG' => '/\b(?:bg|tr|rom)\b/', + 'BH' => '/\b(?:ar|fa|ur)\b/', + 'BI' => '/\b(?:fr|rn)\b/', + 'BJ' => '/\b(?:fr)\b/', + 'BL' => '/\b(?:fr)\b/', + 'BM' => '/\b(?:pt)\b/', + 'BN' => '/\b(?:ms)\b/', + 'BO' => '/\b(?:es|qu|ay)\b/', + 'BQ' => '/\b(?:nl|pap)\b/', + 'BR' => '/\b(?:pt|es|fr)\b/', + 'BT' => '/\b(?:dz)\b/', + 'BW' => '/\b(?:tn)\b/', + 'BY' => '/\b(?:be|ru)\b/', + 'BZ' => '/\b(?:es)\b/', + 'CA' => '/\b(?:fr|iu)\b/', + 'CC' => '/\b(?:ms)\b/', + 'CD' => '/\b(?:fr|ln|ktu|kg|sw|lua)\b/', + 'CF' => '/\b(?:fr|sg|ln|kg)\b/', + 'CG' => '/\b(?:fr|kg|ln)\b/', + 'CH' => '/\b(?:de|fr|it|rm)\b/', + 'CI' => '/\b(?:fr)\b/', + 'CK' => '/\b(?:mi)\b/', + 'CL' => '/\b(?:es)\b/', + 'CM' => '/\b(?:fr)\b/', + 'CN' => '/\b(?:zh|yue|wuu|dta|ug|za)\b/', + 'CO' => '/\b(?:es)\b/', + 'CR' => '/\b(?:es)\b/', + 'CU' => '/\b(?:es|pap)\b/', + 'CV' => '/\b(?:pt)\b/', + 'CW' => '/\b(?:nl|pap)\b/', + 'CX' => '/\b(?:zh|ms)\b/', + 'CY' => '/\b(?:el|tr)\b/', + 'CZ' => '/\b(?:cs|sk)\b/', + 'DE' => '/\b(?:de)\b/', + 'DJ' => '/\b(?:fr|ar|so|aa)\b/', + 'DK' => '/\b(?:da|fo|de)\b/', + 'DO' => '/\b(?:es)\b/', + 'DZ' => '/\b(?:ar|fr)\b/', + 'EC' => '/\b(?:es)\b/', + 'EE' => '/\b(?:et|ru)\b/', + 'EG' => '/\b(?:ar|fr)\b/', + 'EH' => '/\b(?:ar|mey)\b/', + 'ER' => '/\b(?:aa|ar|tig|kun|ti)\b/', + 'ES' => '/\b(?:es|ca|gl|eu|oc)\b/', + 'ET' => '/\b(?:am|om|ti|so|sid)\b/', + 'FI' => '/\b(?:fi|sv|smn)\b/', + 'FJ' => '/\b(?:fj)\b/', + 'FM' => '/\b(?:chk|pon|yap|kos|uli|woe|nkr|kpg)\b/', + 'FO' => '/\b(?:fo|da)\b/', + 'FR' => '/\b(?:fr|frp|br|co|ca|eu|oc)\b/', + 'GA' => '/\b(?:fr)\b/', + 'GB' => '/\b(?:en)\b/', + 'GE' => '/\b(?:ka|ru|hy|az)\b/', + 'GF' => '/\b(?:fr)\b/', + 'GG' => '/\b(?:nrf)\b/', + 'GH' => '/\b(?:ak|ee|tw)\b/', + 'GI' => '/\b(?:es|it|pt)\b/', + 'GL' => '/\b(?:kl|da)\b/', + 'GM' => '/\b(?:mnk|wof|wo|ff)\b/', + 'GN' => '/\b(?:fr)\b/', + 'GP' => '/\b(?:fr)\b/', + 'GQ' => '/\b(?:es|fr)\b/', + 'GR' => '/\b(?:el|fr)\b/', + 'GT' => '/\b(?:es)\b/', + 'GU' => '/\b(?:ch)\b/', + 'GW' => '/\b(?:pt|pov)\b/', + 'HK' => '/\b(?:zh|yue|zh)\b/', + 'HN' => '/\b(?:es|cab|miq)\b/', + 'HR' => '/\b(?:hr|sr)\b/', + 'HT' => '/\b(?:ht|fr)\b/', + 'HU' => '/\b(?:hu)\b/', + 'ID' => '/\b(?:id|nl|jv)\b/', + 'IE' => '/\b(?:ga)\b/', + 'IL' => '/\b(?:he|ar)\b/', + 'IM' => '/\b(?:gv)\b/', + 'IN' => '/\b(?:hi|bn|te|mr|ta|ur|gu|kn|ml|or|pa|as|bh|sat|ks|ne|sd|kok|doi|mni|sit|sa|fr|lus|inc)\b/', + 'IQ' => '/\b(?:ar|ku|hy)\b/', + 'IR' => '/\b(?:fa|ku)\b/', + 'IS' => '/\b(?:is|de|da|sv|no)\b/', + 'IT' => '/\b(?:it|de|fr|sc|ca|co|sl)\b/', + 'JE' => '/\b(?:fr|nrf)\b/', + 'JO' => '/\b(?:ar)\b/', + 'JP' => '/\b(?:ja)\b/', + 'KE' => '/\b(?:sw)\b/', + 'KG' => '/\b(?:ky|uz|ru)\b/', + 'KH' => '/\b(?:km|fr)\b/', + 'KI' => '/\b(?:gil)\b/', + 'KM' => '/\b(?:ar|fr)\b/', + 'KP' => '/\b(?:ko)\b/', + 'KR' => '/\b(?:ko)\b/', + 'XK' => '/\b(?:sq|sr)\b/', + 'KW' => '/\b(?:ar)\b/', + 'KZ' => '/\b(?:kk|ru)\b/', + 'LA' => '/\b(?:lo|fr)\b/', + 'LB' => '/\b(?:ar|fr|hy)\b/', + 'LI' => '/\b(?:de)\b/', + 'LK' => '/\b(?:si|ta)\b/', + 'LS' => '/\b(?:st|zu|xh)\b/', + 'LT' => '/\b(?:lt|ru|pl)\b/', + 'LU' => '/\b(?:lb|de|fr)\b/', + 'LV' => '/\b(?:lv|ru|lt)\b/', + 'LY' => '/\b(?:ar|it)\b/', + 'MA' => '/\b(?:ar|ber|fr)\b/', + 'MC' => '/\b(?:fr|it)\b/', + 'MD' => '/\b(?:ro|ru|gag|tr)\b/', + 'ME' => '/\b(?:sr|hu|bs|sq|hr|rom)\b/', + 'MF' => '/\b(?:fr)\b/', + 'MG' => '/\b(?:fr|mg)\b/', + 'MH' => '/\b(?:mh)\b/', + 'MK' => '/\b(?:mk|sq|tr|rmm|sr)\b/', + 'ML' => '/\b(?:fr|bm)\b/', + 'MM' => '/\b(?:my)\b/', + 'MN' => '/\b(?:mn|ru)\b/', + 'MO' => '/\b(?:zh|zh|pt)\b/', + 'MP' => '/\b(?:fil|tl|zh|ch)\b/', + 'MQ' => '/\b(?:fr)\b/', + 'MR' => '/\b(?:ar|fuc|snk|fr|mey|wo)\b/', + 'MT' => '/\b(?:mt)\b/', + 'MU' => '/\b(?:bho|fr)\b/', + 'MV' => '/\b(?:dv)\b/', + 'MW' => '/\b(?:ny|yao|tum|swk)\b/', + 'MX' => '/\b(?:es)\b/', + 'MY' => '/\b(?:ms|zh|ta|te|ml|pa|th)\b/', + 'MZ' => '/\b(?:pt|vmw)\b/', + 'NA' => '/\b(?:af|de|hz|naq)\b/', + 'NC' => '/\b(?:fr)\b/', + 'NE' => '/\b(?:fr|ha|kr|dje)\b/', + 'NG' => '/\b(?:ha|yo|ig|ff)\b/', + 'NI' => '/\b(?:es)\b/', + 'NL' => '/\b(?:nl|fy)\b/', + 'NO' => '/\b(?:no|nb|nn|se|fi)\b/', + 'NP' => '/\b(?:ne)\b/', + 'NR' => '/\b(?:na)\b/', + 'NU' => '/\b(?:niu)\b/', + 'NZ' => '/\b(?:mi)\b/', + 'OM' => '/\b(?:ar|bal|ur)\b/', + 'PA' => '/\b(?:es)\b/', + 'PE' => '/\b(?:es|qu|ay)\b/', + 'PF' => '/\b(?:fr|ty)\b/', + 'PG' => '/\b(?:ho|meu|tpi)\b/', + 'PH' => '/\b(?:tl|fil|ceb|tgl|ilo|hil|war|pam|bik|bcl|pag|mrw|tsg|mdh|cbk|krj|sgd|msb|akl|ibg|yka|mta|abx)\b/', + 'PK' => '/\b(?:ur|pa|sd|ps|brh)\b/', + 'PL' => '/\b(?:pl)\b/', + 'PM' => '/\b(?:fr)\b/', + 'PR' => '/\b(?:es)\b/', + 'PS' => '/\b(?:ar)\b/', + 'PT' => '/\b(?:pt|mwl)\b/', + 'PW' => '/\b(?:pau|sov|tox|ja|fil|zh)\b/', + 'PY' => '/\b(?:es|gn)\b/', + 'QA' => '/\b(?:ar|es)\b/', + 'RE' => '/\b(?:fr)\b/', + 'RO' => '/\b(?:ro|hu|rom)\b/', + 'RS' => '/\b(?:sr|hu|bs|rom)\b/', + 'RU' => '/\b(?:ru|tt|xal|cau|ady|kv|ce|tyv|cv|udm|tut|mns|bua|myv|mdf|chm|ba|inh|tut|kbd|krc|av|sah|nog)\b/', + 'RW' => '/\b(?:rw|fr|sw)\b/', + 'SA' => '/\b(?:ar)\b/', + 'SB' => '/\b(?:tpi)\b/', + 'SC' => '/\b(?:fr)\b/', + 'SD' => '/\b(?:ar|fia)\b/', + 'SE' => '/\b(?:sv|se|sma|fi)\b/', + 'SG' => '/\b(?:cmn|ms|ta|zh)\b/', + 'SI' => '/\b(?:sl|sh)\b/', + 'SJ' => '/\b(?:no|ru)\b/', + 'SK' => '/\b(?:sk|hu)\b/', + 'SL' => '/\b(?:mtem)\b/', + 'SM' => '/\b(?:it)\b/', + 'SN' => '/\b(?:fr|wo|fuc|mnk)\b/', + 'SO' => '/\b(?:so|ar|it)\b/', + 'SR' => '/\b(?:nl|srn|hns|jv)\b/', + 'ST' => '/\b(?:pt)\b/', + 'SV' => '/\b(?:es)\b/', + 'SX' => '/\b(?:nl)\b/', + 'SY' => '/\b(?:ar|ku|hy|arc|fr)\b/', + 'SZ' => '/\b(?:ss)\b/', + 'TD' => '/\b(?:fr|ar|sre)\b/', + 'TF' => '/\b(?:fr)\b/', + 'TG' => '/\b(?:fr|ee|hna|kbp|dag|ha)\b/', + 'TH' => '/\b(?:th)\b/', + 'TJ' => '/\b(?:tg|ru)\b/', + 'TK' => '/\b(?:tkl)\b/', + 'TL' => '/\b(?:tet|pt|id)\b/', + 'TM' => '/\b(?:tk|ru|uz)\b/', + 'TN' => '/\b(?:ar|fr)\b/', + 'TO' => '/\b(?:to)\b/', + 'TR' => '/\b(?:tr|ku|diq|az|av)\b/', + 'TT' => '/\b(?:hns|fr|es|zh)\b/', + 'TV' => '/\b(?:tvl|sm|gil)\b/', + 'TW' => '/\b(?:zh|zh|nan|hak)\b/', + 'TZ' => '/\b(?:sw|ar)\b/', + 'UA' => '/\b(?:uk|ru|rom|pl|hu)\b/', + 'UG' => '/\b(?:lg|sw|ar)\b/', + 'US' => '/\b(?:en|es)\b/', + 'UY' => '/\b(?:es)\b/', + 'UZ' => '/\b(?:uz|ru|tg)\b/', + 'VA' => '/\b(?:la|it|fr)\b/', + 'VC' => '/\b(?:fr)\b/', + 'VE' => '/\b(?:es)\b/', + 'VN' => '/\b(?:vi|fr|zh|km)\b/', + 'VU' => '/\b(?:bi|fr)\b/', + 'WF' => '/\b(?:wls|fud|fr)\b/', + 'WS' => '/\b(?:sm)\b/', + 'YE' => '/\b(?:ar)\b/', + 'YT' => '/\b(?:fr)\b/', + 'ZA' => '/\b(?:zu|xh|af|nso|tn|st|ts|ss|ve|nr)\b/', + 'ZM' => '/\b(?:bem|loz|lun|lue|ny|toi)\b/', + 'ZW' => '/\b(?:sn|nr|nd)\b/', + 'CS' => '/\b(?:cu|hu|sq|sr)\b/', + 'AN' => '/\b(?:nl|es)\b/' + ]; + + if (isset($codes[$country])) { + return $codes[$country]; + } + + return null; +} diff --git a/lib/postfilter.php b/lib/postfilter.php new file mode 100644 index 0000000..2bd31a4 --- /dev/null +++ b/lib/postfilter.php @@ -0,0 +1,3915 @@ + array_search($f2, $pkeys); +} + +// style 1 - no spaces, just letters+numbers in fields +function is_random_text($fields) +{ + foreach($fields as $field) { + $num = preg_match('/[0-9]/', $field); + $lc = preg_match('/[a-z]/', $field); + $uc = preg_match('/[A-Z]/', $field); + $spc = preg_match('/ /', $field); + if ($spc || ($num + $lc + $uc)<2) return false; + } + + return true; +} + +// style 2 - uc+lc+spaces in fields +function is_random_word_text($fields) +{ + foreach($fields as $field) { + $other = preg_match('/[^a-zA-Z \n]/', $field); + $lc = preg_match('/[a-z]/', $field); + $uc = preg_match('/[A-Z]/', $field); + $spc = preg_match('/ /', $field); + if ($other || !$spc || !$lc || !$uc) return false; + } + + return true; +} + +function has_cyrillic($txt) +{ + return preg_match('/[\xD0-\xD3][\x80-\xBF]/', $txt) > 0; +} + +function expand_short_urls($com) +{ + $urls = array(); + $urltext = ""; + + $com = preg_replace("/\(dot\)|DOT/", ".", $com); + preg_replace("@(([A-Za-z0-9_-]+\.)?((tinyurl|go2cut|doiop|x2t|snipr|gorkk|veeox|gurlx|goshrink|shrink[a-z]+|shortmaker|notlong|2gohere|urlmr|adjix|icanhaz|linkbee|oeeq|url9|urlzen|cladpal|redirx|yuarel|llfk|as2h|at|url-go|shrten|eyodude|urlxp|myurlz|allno1|nlz2|ye-s|way|lymme|unrelo|qfwr|urluda|golinkgo|cnekt|12n3|peqno|pasukan|vktw|snipie|4gk|82au|[a-z]+url|tinyden)\\.com|(lix|urlm|t2w|trigg|flib)\\.in|j\\.mp|hub\\.tm|(smarturl|fogz|urlink)\\.eu|sn\\.vc|atu\\.ca|(a|is)\\.gd|(xrl|nuurl|kore|p3n|txtn|hepy|w95|freepl|0x3|2tu)\\.us|dduri\\.info|cc\\.st|mzan\\.si|(\xe2\x9d\xbd|cctv)\\.ws|(metamark|sogood|idek|2tr|ln-s|urlaxe|littleurl)\\.net|(fyad|linkmenow|linkplug)\\.org|ad\\.vu|(bit|xa|ow|3|smal|to)\\.ly|safe\\.mn|goo\\.gl|is\\.gd|zi\\.ma|(tr|sn|jar|gow)\\.im|twurl\\.(cc|nl)|(fon|cli|rod|mug)\\.gs|(urlenco|dboost)\\.de|(tiny|bizz|blu|juu)\\.cc|2big\\.at|(crum|sk9)\\.pl|(bloat|pnt|lnq)\\.me|kl\\.am|(showip|2so)\\.be|minilink\\.me|lurl\\.no|(jai|cvm)\\.biz|xs\\.md|short\\.to|(yep|trunc)\\.it|a\\.nf|shortlinks\\.co\\.uk|redir\\.ec|tim\\.pe)(/[?A-Za-z0-9_-]*)?)@ei", '$urls[] = "http://$1";', $com); + + foreach($urls as $url) { + $com .= strtolower(rpc_find_real_url(str_replace("preview.tinyurl","tinyurl",$url))); + } + + return $com; +} + +function normalize_ascii($text, $preserve_case = 0) +{ + $text = preg_replace( '#[(\[={]dot[)\]=}]#i', '.', $text ); + + $t = Transliterator::create("Any-Latin; nfd; [:nonspacing mark:] remove; nfkc; Latin-ASCII"); + if (!$t) return $text; + $text = $t->transliterate($text); + if (!$preserve_case) $text = strtolower($text); + + return $text; +} + +function strip_zerowidth($text) { + $text = preg_replace('/ + [\x17\x8\x1f]|\x{0702}|\x{1D176}|\x{008D}|\x{00A0}|\x{205F}|\x{FEFF}|\x{11A6}|\x{00AD}|\x{3164}|\x{2800}|\x{180B}|\x{180C}|\x{180D}| + \x{115F}|\x{1160}|\x{FFA0}|\x{034f}|\x{180e}|\x{17B4}|\x{17B5}| + [\x{0001}-\x{0008}\x{000E}\x{000F}\x{0010}-\x{001F}\x{007F}-\x{009F}]| + [\x{2000}-\x{200F}]| + [\x{2028}-\x{202F}]| + [\x{2060}-\x{206F}]| + [\x{fe00}-\x{fe0f}]| + [\x{FFF0}-\x{FFFB}]| + [\x{E0100}-\x{E01EF}]| + [\x{E0001}-\x{E007F}] + /ux', '', $text); + return $text; +} + +/** + * Removes codepoints above 3134F: + * E0000..E007F; Tags + * E0100..E01EF; Variation Selectors Supplement + * F0000..FFFFF; Supplementary Private Use Area-A + * 100000..10FFFF; Supplementary Private Use Area-B + */ +function strip_private_unicode($str) { + if ($str === '') { + return $str; + } + + return preg_replace('/[^\x{0000}-\x{3134F}]/u', '', $str); +} + +function strip_emoticons($text, $has_sjis_art = false) { + $regex = '[\x{2300}-\x{2311}\x{2313}-\x{23FF}]|[\x{3200}-\x{32FF}\x{2190}-\x{21FF}\x{2580}-\x{259F}\x{2600}-\x{26FF}\x{2B00}-\x{2BFE}\x{1F700}-\x{1F77F}\x{1F780}-\x{1F7D8}\x{1F780}-\x{1F7D8}\x{1F800}-\x{1F8FF}\x{1F900}-\x{1F9FF}]|[\x{1F200}-\x{1F2FF}]|[\x{2460}-\x{24FF}]|[\x{1F100}-\x{1F1FF}]|[\x{1F600}-\x{1F64F}]|[\x{1F300}-\x{1F5FF}]|[\x{1F680}-\x{1F6FF}]|[\x{2600}-\x{26FF}]|[\x{2700}-\x{27BF}]|[\x{1F000}-\x{1F02F}]|[\x{1F0A0}-\x{1F0FF}]|[\x{2139}\x{23F2}]|[\x{1F910}-\x{1F9E6}]|[\x{0365}]|\x{FDFD}|[\x{0488}\x{0489}\x{1abe}\x{20dd}\x{20de}\x{20df}\x{20e0}\x{20e2}\x{20e3}\x{20e4}\x{a670}\x{a671}\x{a672}\x{061c}\x{070F}\x{0332}\x{0305}\x{2B55}]|[\x{202A}-\x{202E}\x{2060}-\x{206F}]|[\x{200E}\x{200F}\x{180e}\x{2b50}\x{23b3}\x{23F1}]|[\x{1F780}-\x{1F7FF}\x{1FA70}-\x{1FAFF}]|[\x{1D173}-\x{1D17A}\x{13000}-\x{1342F}\x{fe00}-\x{fe0f}]'; + + if (!$has_sjis_art) { + $regex .= '|[\x{2502}-\x{257F}]'; + } + + return preg_replace("/$regex/u", '', $text); +} + +function strip_fake_capcodes($str) { + // double FULLWIDTH NUMBER SIGN or # + // PLACE OF INTEREST SIGN + return preg_replace('/[\x{FF03}#]{2,}|\x{2318}/u', '', $str); +} + +function normalize_text($text, $filter = '') { + $text = normalize_ascii($text); + $text = strip_zerowidth($text); + + //if ($filter) $text = $filter($text); + + $text = preg_replace('@[^a-zA-Z0-9.,/&:;?=~_-]@', '', $text); + + return $text; +} + +function normalize_content( $name ) +{ + // this needs some absolutely retarded shit to get this to not suck, however + // it is an almost fool proof way of translating to ascii letters + // without breaking kanji, cyrillic etc + + $name = preg_replace( '#[\x{2600}-\x{26FF}]#u', '', $name ); + + // set internal incoding to utf-8 + //die($name); + $oldEncoding = mb_internal_encoding(); + mb_internal_encoding('UTF-8'); + $name = convert_to_utf8($name); + // Done, back to old encoding + + $newname = ''; + + $len = mb_strlen( $name ); + for( $i = 0; $i < $len; $i++ ) { + trans_similar_to_ascii( $newname, mb_substr( $name, $i, 1 ) ); + } + + mb_internal_encoding($oldEncoding); + return $newname; +} + +function convert_to_utf8( $content ) +{ + if( !mb_check_encoding( $content, 'UTF-8' ) || !($content === mb_convert_encoding(mb_convert_encoding($content, 'UTF-32', 'UTF-8' ), 'UTF-8', 'UTF-32')) ) { + $content = mb_convert_encoding($content, 'UTF-8'); + } + + return $content; +} + +function mb_ord( $char ) +{ + mb_detect_order( array( 'UTF-8', 'ISO-8859-15', 'ISO-8859-1', 'ASCII' ) ); + $result = unpack( 'N', mb_convert_encoding( $char, 'UCS-4BE', 'UTF-8' ) ); + + if( is_array( $result ) === true ) return $result[1]; + + return ord($char); +} + +function normalize_check($com,$sub,$f) +{ + $n = normalize_text($com.$sub, "expand_short_urls"); + $n2 = preg_replace("/[\x80-\xFF]/", "", html_entity_decode($com, ENT_QUOTES, "UTF-8")); + + record_post_info($f, "from: $com$sub\nto: $n\ndeutf8: $n2"); +} + +function match_banned_text($links, $text, $is_re) +{ + $badlink = ""; + $should_ban = false; + + foreach ($links as $l) { + if ($l == '#') continue; + + $badlink = $l; + + if ($is_re) { + $should_ban = preg_match($l, $text, $m) > 0; + $badlink = TEST_BOARD ? "'$l' ({$m[0]})" : "'{$m[0]}'"; + } else { + $should_ban = strpos($text, $l) !== FALSE; + } + + if ($should_ban) + break; + } + + return $should_ban ? $badlink : ""; +} + +function check_banned_links($text, $links, $priv, $pub, $is_re, $name, $dest, $ban, $long, $perm = false) +{ + //check_banned_links($normalized_com, $sex, "sex spam links", S_BANNEDLINK, false, $name, $dest, true, false); + $badlink = match_banned_text($links, $text, $is_re); + $should_ban = ($badlink != ""); + $len = $long ? 14 : 1; + $len = $perm ? -1 : $len; + + if ($should_ban == true) { + $privres = sprintf("banned %s %s: %s", $is_re?"regex":"string",htmlspecialchars($badlink),$priv); + if ($ban) { + $pub = str_replace('Error: ', '', $pub); + auto_ban_poster($name, $len, 1, $privres, $pub, true); + } + if (TEST_BOARD) $pub .= "
    ".$privres; + error($pub, $dest); + } +} + +// this used to check the autobans table but now it just permabans +function auto_ban($name, $reason) { + auto_ban_poster($name, 7, 1, $reason); +} + +function get_jpeg_dimensions($contents) +{ + // this is faster than getimagesize + + $i = 0; + $len = strlen($contents); + + if( ord($contents{0}) == 0xFF & ord($contents{1}) == 0xD8 && ord($contents{2}) == 0xFF & ord($contents{3}) == 0xE0 ) { + $i = 4; + + if( $contents{$i+2} == 'J' && $contents{$i+3} == 'F' && $contents{$i+4} == 'I' && $contents{$i+5} == 'F' && ord($contents{$i+6}) == 0x00 ) { + // valid image. + $block_length = ord($contents{$i}) * 256 + ord($contents{$i+1}); + + while( $i < $len ) { + $i += $block_length; + + if( $i > $len ) { + return false; + } + + if( ord($contents{$i}) != 0xFF ) { + return false; + } + + + if( ord($contents{$i+1}) == 0xC0 ) { + $width = ord($contents{$i+7})*256 + ord($contents{$i+8}); + $height = ord($contents{$i+5})*256 + ord($contents{$i+6}); + + return array($width, $height); + } else { + $i+=2; + $block_length = ord($contents{$i}) * 256 + ord($contents{$i+1}); + } + } + } + } + + return false; +} + +function file_too_big_for_type( $ext, $w, $h, $fsize ) +{ + if ($ext === ".gif" || $ext === ".pdf" || $ext === '.webm' || $ext === '.mp4') { + return NO; + } + + $uncompressed_size = $w * $h * 4; + + return ($fsize > (3*$uncompressed_size)) ? YES : NO; +} + +function regex_ignoring_nulls($words) +{ + $rwords = preg_replace("/./", "$0[^\\\\x01-\\\\xFF]*", $words); + $rwords = str_replace(".", "\\.", $rwords); + return "/".implode("|", $rwords)."/i"; +} + +/** + * Strips exif from JPEG images + * $file needs to be safe to use as shell argument + */ +function strip_jpeg_exif($file) { + return system("/usr/local/bin/jpegtran -copy none -outfile '$file' '$file'") !== false; +} + +/** + * Strips non-whitelisted PNG chunks. + * Returns an error if an animated PNG is detected. + * Overwrites input file if modifications have been made. + * $file needs to be safe to use as shell argument + * Returns the number of chunks skipped or an error code (negative value). + */ +function strip_png_chunks($file, $max_chunk_len = 16 * 1024 * 1024) { + $keep_chunks = [ + 'ihdr', + 'plte', + 'idat', + 'iend', + 'trns', + 'gama', + 'sbit', + 'phys', + 'srgb', + 'bkgd', + 'time', + 'chrm', + 'iccp' + ]; + + $img = fopen($file, 'rb'); + + if (!$img) { + return -9; + } + + $data = fread($img, 8); + + if ($data !== "\x89PNG\r\n\x1a\n") { + fclose($img); + return -1; + } + + $output = ''; + + $skip_count = 0; + + while (!feof($img)) { + $chunk_len_buf = fread($img, 4); + + if (!$chunk_len_buf) { + break; + } + + if (strlen($chunk_len_buf) !== 4) { + return -1; + } + + $chunk_len = unpack('N', $chunk_len_buf)[1]; + + if ($chunk_len > $max_chunk_len) { + return -1; + } + + $chunk_type_buf = fread($img, 4); + + if (strlen($chunk_type_buf) !== 4) { + return -1; + } + + $chunk_type = strtolower($chunk_type_buf); + + // aPNG is not supported + if ($chunk_type === 'actl' || $chink_type === 'fctl' || $chink_type === 'fdat') { + return -2; + } + + if (in_array($chunk_type, $keep_chunks)) { + if ($chunk_len > 0) { + $data = fread($img, $chunk_len); + + if (strlen($data) !== $chunk_len) { + return -1; + } + } + else { + $data = ''; + } + + $crc = fread($img, 4); + + if (strlen($crc) !== 4) { + return -1; + } + + $output .= $chunk_len_buf . $chunk_type_buf . $data . $crc; + + if ($chunk_type === 'iend') { + fread($img, 1); + if (!feof($img)) { + $skip_count++; + } + break; + } + } + else { + fseek($img, $chunk_len + 4, SEEK_CUR); + $skip_count++; + } + } + + fclose($img); + + if ($output === '') { + return -1; + } + + if ($skip_count === 0) { + return 0; + } + + $out_file = $file . '_pngtmp'; + + $out = fopen($out_file, 'wb'); + + if (!$out) { + return -9; + } + + if (fwrite($out, "\x89PNG\r\n\x1a\n") === false) { + return -9; + } + + if (fwrite($out, $output) === false) { + return -9; + } + + fclose($out); + + if (rename($out_file, $file) === false) { + return -9; + } + + return $skip_count; +} + +// Calculates the actual image data inside a JPEG file and errors out +// if it's smaller than the reported size. +function validate_jpeg_size($file, $reported_size) { + $eof = false; + + $img = fopen($file, 'rb'); + + $data = fread($img, 2); + + if ($data !== "\xff\xd8") { + fclose($img); + return false; + } + + while (!feof($img)) { + $data = fread($img, 1); + + if ($data !== "\xff") { + continue; + } + + while (!feof($img)) { + $data = fread($img, 1); + + if ($data !== "\xff") { + break; + } + } + + if (feof($img)) { + break; + } + + $byte = unpack('C', $data)[1]; + + if ($byte === 217) { + $eof = ftell($img); + break; + } + + if ($byte === 0 || $byte === 1 || ($byte >= 208 && $byte <= 216)) { + continue; + } + + $data = fread($img, 2); + + $length = unpack('n', $data)[1]; + + if ($length < 1) { + break; + } + + fseek($img, $length - 2, SEEK_CUR); + } + + fclose($img); + + // 50 KB + if ($reported_size - $eof >= 51200) { + error(S_IMGCONTAINSFILE, $file); + } + + return $eof; +} + +/** + * Checks for extensions and comments + * and calculates actual GIF data. + * Strips extra data if it exists. + * $file needs to be safe to use as shell argument. + */ +function strip_gif_extra_data($file, $reported_size) { + $binary = '/usr/local/bin/gifsicle'; + + $res = shell_exec("$binary --sinfo \"$file\" 2>&1"); + + if ($res !== null) { + $size = 0; + + $need_strip = false; + + if (preg_match('/ extensions [0-9]+| comment /', $res)) { + $need_strip = true; + } + else if (preg_match_all('/compressed size ([0-9]+)/', $res, $m)) { + foreach ($m[1] as $frame_size) { + $size += (int)$frame_size; + } + + // Strip if 50+ KB of extra data is found + if ($reported_size - $size >= 51200) { + $need_strip = true; + } + } + + if ($need_strip) { + if (system("$binary --no-comments --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1") === false) { + // gifsicle error + return -1; + } + else { + // file was modified + return 1; + } + } + } + else { + // gifsicle error + return -1; + } + + // nothing changed + return 0; +} + +// No longer used +function spam_filter_post_image($name, $dest, $md5, $upfile_name, $ext, $w, $h, $fsize) +{ + if( $upfile_name == '' ) error('Blank file names are not supported.'); + + if (file_too_big_for_type($ext, $w, $h, $fsize) === YES) { + $lim = 3*4*$w*$h; + error(S_IMGCONTAINSFILE, $dest); + } + + $img_bytes = file_get_contents($dest); + $img_beginning = strlen($img_bytes) > 0x50000 ? substr($img_bytes, 0, 0x40000).substr($img_bytes, -(0x10000)) : $img_bytes; + + global $silent_reject; + $silent_reject = 0; + + // protect against IE's retarded MIME-sniffing XSS vulnerability + // by doing our own sniffing and rejecting exploitable files + { + $negative_match = regex_ignoring_nulls(array("minitokyonet", "urchin.js")); + //except minitokyo from this, it causes false positives + if (preg_match($negative_match, $img_beginning)===0) + { + // ' DATE_SUB(NOW(), INTERVAL 1 HOUR) LIMIT 1 +SQL; + + $res = mysql_global_call($query); + + if ($res && mysql_num_rows($res) === 1) { + return true; + } + + $query = "INSERT INTO postfilter_hits (filter_id, board, long_ip) VALUES($filter_id, '%s', $long_ip)"; + + return mysql_global_call($query, BOARD_DIR); +} + +function log_postfilter_hit($filter, $board, $thread_id, $name, $sub, $com, $upfile_name) { + $ip = $_SERVER['REMOTE_ADDR']; + + $country = $_SERVER['HTTP_X_GEO_COUNTRY']; + + $threat_score = spam_filter_get_threat_score($country, !$thread_id, true); + + $meta = spam_filter_format_http_headers("$name\n$sub\n$com", $country, $upfile_name, $threat_score); + + $action = "filter_{$filter['id']}"; + + $query = <<\/]+|>/', ' ', $sub . ' ' . $com . ' '. $name); + $normalized_com_sage = ucwords(strtolower($normalized_com_sage)); + $normalized_com_sage = normalize_ascii($normalized_com_sage, 1); + } + + $userpwd = UserPwd::getSession(); + + if ($userpwd && $userpwd->isUserKnownOrVerified(1440) && $userpwd->postCount() > 10) { // 24 hours, 10 posts + $user_is_known = true; + } + else { + $user_is_known = false; + } + + $matched_filter = false; + + while ($filter = mysql_fetch_assoc($res)) { + // Counter mode: triggers when the number of matches is at least $min_count + $min_count = (int)$filter['min_count']; + + if ($min_count < 1) { + $min_count = 1; + } + + // Lenient filter + if ($user_is_known && $filter['lenient']) { + continue; + } + + // OPs-only filter + if ($filter['ops_only'] && $resto) { + continue; + } + + if ($filter['autosage']) { + // Autosage filter but the post is a reply + if ($resto) { + continue; + } + // Regex filter + if ($filter['regex']) { + if ($min_count > 1) { + if (preg_match_all($filter['pattern'], $expanded_com) >= $min_count) { + $matched_filter = $filter; + break; + } + } + else { + if (preg_match($filter['pattern'], $expanded_com) === 1) { + $matched_filter = $filter; + break; + } + } + } + // String filter for autosaging + else { + if ($min_count > 1) { + if (substr_count($normalized_com_sage, $filter['pattern']) >= $min_count) { + $matched_filter = $filter; + break; + } + } + else { + if (strpos($normalized_com_sage, $filter['pattern']) !== false) { + $matched_filter = $filter; + break; + } + } + } + } + // Regex filter + if ($filter['regex']) { + if ($min_count > 1) { + if (preg_match_all($filter['pattern'], $expanded_com) >= $min_count) { + $matched_filter = $filter; + break; + } + } + else { + if (preg_match($filter['pattern'], $expanded_com) === 1) { + $matched_filter = $filter; + break; + } + } + } + // String filter + else { + if ($min_count > 1) { + if (substr_count($normalized_com, $filter['pattern']) >= $min_count) { + $matched_filter = $filter; + break; + } + } + else { + if (strpos($normalized_com, $filter['pattern']) !== false) { + $matched_filter = $filter; + break; + } + } + } + } + + if ($matched_filter !== false) { + // Update hit stats + register_postfilter_hit($matched_filter['id']); + + // Autosage + if ($matched_filter['autosage']) { + return true; + } + // Log + else if ($matched_filter['log']) { + log_postfilter_hit($matched_filter, $board, $resto, $name, $sub, $com, $upfile_name); + } + // Reject + else { + if ($matched_filter['ban_days']) { + $err = S_BANNEDTEXT; + $ban_days = (int)$matched_filter['ban_days']; + $private_reason = 'banned string in comment (filter ID: ' . $matched_filter['id'] . ')'; + $public_reason = $err; + auto_ban_poster($name, $ban_days, 1, $private_reason, $public_reason, true, $pwd, $pass_id); + } + else { + $err = S_REJECTTEXT; + } + + if ($matched_filter['quiet']) { + show_post_successful_fake($resto); + die(); + } + + if (TEST_BOARD) { + $err .= ' (filter ID: ' . $matched_filter['id'] . ')'; + } + + error($err); + } + } + + // Other + if ($sub !== '') { + $normalized_sub = normalize_text($sub); + + if (stripos($sub, 'moot') !== false) { + error("You can't post with that subject."); + } + + if (stripos($normalized_com, '##') !== false || stripos($sub, 'admin') !== false) { + error("You can't post with that subject."); + } + } + + return false; +} + +function isIPRangeBannedReport($long_ip, $asn, $board, $userpwd = null) { + return isIPRangeBanned($long_ip, $asn, + [ + 'board' => $board, + 'is_report' => true, + 'userpwd' => $userpwd, + ] + ); +} + +// Checks if the IP is rangebanned +// options: +// board(string), is_sfw(bool, requires board) +// userpwd(UserPwd): instance of UserPwd or null, +// is_report(bool), is_op(bool), has_img(bool), +// browser_id(string), +// op_content(string): content of the thread OP for per-thread bans (unused) +// returns the rangeban database entry if the IP is banned, false otherwise +function isIPRangeBanned($long_ip, $asn, $options = []) { + $long_ip = (int)$long_ip; + + $asn = (int)$asn; + + $now = (int)$_SERVER['REQUEST_TIME']; + + $cols = 'created_on, updated_on, expires_on, active, boards, ops_only, img_only, lenient, report_only, ua_ids'; + + $query = <<= $long_ip AND active = 1 +AND (expires_on = 0 OR expires_on > $now)) +SQL; + + if ($asn > 0) { + $query .= << $now)) +SQL; + } + + $query .= ' ORDER BY lenient ASC'; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + // Parameters + if (isset($options['board'])) { + $board = $options['board']; + $is_sfw = isset($options['is_sfw']) && $options['is_sfw']; + } + else { + $board = null; + $is_sfw = false; + } + + if (isset($options['browser_id'])) { + $browser_id = $options['browser_id']; + } + else { + $browser_id = null; + } + + if (isset($options['req_sig'])) { + $req_sig = $options['req_sig']; + } + else { + $req_sig = null; + } + + if (isset($options['op_content']) && $options['op_content'] !== '') { + $op_content = $options['op_content']; + } + else { + $op_content = null; + } + + $is_op = isset($options['is_op']) && $options['is_op']; + $is_report = isset($options['is_report']) && $options['is_report']; + $has_img = isset($options['has_img']) && $options['has_img']; + + if (isset($options['userpwd']) && $options['userpwd'] && $options['userpwd'] instanceof UserPwd) { + $userpwd = $options['userpwd']; + } + else { + $userpwd = null; + } + + // OP-only and Image-only lenient rangebans also require a certain number of posts + $post_count_ok = $userpwd && $userpwd->postCount() >= 3 && ($userpwd->maskLifetime() > 900 || $userpwd->postCount() >= 15); + + while ($range = mysql_fetch_assoc($res)) { + if ($range['boards']) { + if ($board === null) { + continue; + } + + $board_matcher = ",{$range['boards']},"; + + if (strpos($board_matcher, ",$board,") === false) { + // _ws_ scope affects all work safe boards + if ($is_sfw) { + if (strpos($board_matcher, ",_ws_,") === false) { + continue; + } + } + else { + continue; + } + } + } + + $post_count_check = true; + + if ($range['report_only'] && !$is_report) { + continue; + } + + if ($range['ops_only']) { + if (!$is_op) { + continue; + } + else { + $post_count_check = $post_count_ok; + } + } + + if ($range['img_only']) { + if (!$has_img) { + continue; + } + else { + $post_count_check = $post_count_ok; + } + } + + if ($range['ua_ids']) { + $_skip = true; + + if ($browser_id && strpos($range['ua_ids'], $browser_id) !== false) { + $_skip = false; + } + + if ($_skip && $req_sig && strpos($range['ua_ids'], $req_sig) !== false) { + $_skip = false; + } + + if ($_skip) { + continue; + } + } + + if ($userpwd && $range['lenient']) { + $lenient = (int)$range['lenient']; + + if ($range['updated_on']) { + $since_ts = (int)$range['updated_on']; + } + else { + $since_ts = (int)$range['created_on']; + } + + // Mode 1: Known 24h or Verified + if ($lenient === 1 && ($userpwd->verifiedLevel() || ($userpwd->isUserKnown(1440, $since_ts) && $post_count_check))) { + continue; + } + // Mode 2: Known 24h only + else if ($lenient === 2 && $userpwd->isUserKnown(1440, $since_ts) && $post_count_check) { + continue; + } + // Mode 3: Verified only + else if ($lenient === 3 && $userpwd->verifiedLevel()) { + continue; + } + } + + return $range; + } + + return false; +} + +/** + * Checks if the IP has enough posting history + * $mode: 0 = check for replies, 1 = check for image replies, 2 = check of threads + * Caches results. + */ +function spam_filter_is_ip_known($long_ip, $board = null, $mode = 0, $minutes_min = 0, $posts_min = 1) { + static $cache = array(); + + $long_ip = (int)$long_ip; + + if (!$long_ip) { + return false; + } + + $cache_key = "$long_ip.$board.$mode.$minutes_min.$posts_min"; + + if (isset($cache[$cache_key])) { + return $cache[$cache_key]; + } + + // Not after (3 days) + $minutes_max = 4320; + + // Not before + $minutes_min = (int)$minutes_min; + + // At least X replies + $posts_min = (int)$posts_min; + + // Board + if ($board) { + $board_clause = "AND board = '" . mysql_real_escape_string($board) . "'"; + } + else { + $board_clause = ''; + } + + // Mode: 1 = image replies, 2 = threads, 0 = any reply + if ($mode === 1) { + $action_clause = "AND action = 'new_reply' AND had_image = 1"; + } + else if ($mode === 2) { + $action_clause = "AND action = 'new_thread'"; + } + else { + $action_clause = "AND action = 'new_reply'"; + } + + // Not before + if (!$minutes_min) { + $time_clause = "time >= DATE_SUB(NOW(), INTERVAL $minutes_max MINUTE)"; + } + else { + $time_clause = "(time BETWEEN DATE_SUB(NOW(), INTERVAL $minutes_max MINUTE) AND DATE_SUB(NOW(), INTERVAL $minutes_min MINUTE))"; + } + + // Check posting history + $query = <<= DATE_SUB(NOW(), INTERVAL 48 HOUR) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count > 0) { + $cache[$cache_key] = false; + return false; + } + */ + + $cache[$cache_key] = true; + + return true; +} + +/* + * Checks if the user has a valid posting history to bypass a rangeban (FIXME: deprecated) + */ +function spam_filter_is_user_known($long_ip, $board = null, $pwd = null, $minutes = 15, $count = 1) { + static $cache = array(); + + $pwd = null; // FIXME + + $interval = (int)$minutes; + + $cache_key_ip = "{$long_ip}:{$interval}"; + + if (isset($cache[$cache_key_ip])) { + return $cache[$cache_key_ip]; + } + + if ($pwd && $board) { + $cache_key_pwd = "{$board}:{$pwd}:{$interval}"; + + if (isset($cache[$cache_key_pwd])) { + return $cache[$cache_key_pwd]; + } + } + + $count = (int)$count; + + if ($count < 1) { + $count = 1; + } + + // Check the IP + $query = << BOARD_DIR, + 'is_sfw' => DEFAULT_BURICHAN, + 'userpwd' => $userpwd, + 'is_op' => $thread_id == 0, + 'has_img' => $has_img, + 'browser_id' => $browser_id, + 'req_sig' => $req_sig + ]; + + if ($range = isIPRangeBanned($long_ip, $asn, $options)) { + if ($range['lenient'] && $userpwd) { + $userpwd->setCookie('.' . L::d(BOARD_DIR)); + } + + // Images only + if ($range['img_only']) { + $_err = S_IPRANGE_BLOCKED_IMG; + } + // Threads only + else if ($range['ops_only']) { + $_err = S_IPRANGE_BLOCKED_OP; + } + else { + $_err = S_IPRANGE_BLOCKED; + } + + // Temporarily or Permanently + if ($range['expires_on'] || $range['lenient']) { + $_err .= ' ' . S_IPRANGE_BLOCKED_TEMP; + } + else { + $_err .= ' ' . S_IPRANGE_BLOCKED_PERM; + } + + // Bypassed by verified or known users + if ($range['lenient'] == 1) { + $_err .= S_IPRANGE_BLOCKED_L1; + } + // Bypassed by known users only + else if ($range['lenient'] == 2) { + $_err .= S_IPRANGE_BLOCKED_L2; + } + // Bypassed by verified users only + else if ($range['lenient'] == 3) { + $_err .= S_IPRANGE_BLOCKED_L3; + } + + // 4chan pass mention + $_err .= S_IPRANGE_BLOCKED_PASS; + + error($_err); + } + + // Auto-rangebans, Mobile only + // Bypassed by verified users or known users for at least 2h + // or users who have made at least one post on the board 15 minutes ago + if ($thread_id !== null && $browser_id[0] === '1') { + $since_ts = 0; + + if ($userpwd) { + $user_known = $userpwd->isUserKnownOrVerified(120, 1); + + $now = $_SERVER['REQUEST_TIME']; + + if ($userpwd->postCount() > 0 && $userpwd->maskTs() <= $now - 900) { + $since_ts = $userpwd->maskTs(); + } + } + else { + $user_known = false; + } + + if (!$user_known) { + if (is_ip_auto_rangebanned($ip, BOARD_DIR, $thread_id, $browser_id, $since_ts)) { + write_to_event_log('auto_range_hit', $ip, [ + 'board' => BOARD_DIR, + 'thread_id' => $thread_id, + 'ua_sig' => $browser_id + ]); + + if ($userpwd) { + $userpwd->setCookie('.' . L::d(BOARD_DIR)); + } + + // Temporary, bypassablee by known or verified users + error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1 . S_IPRANGE_BLOCKED_PASS); + } + } + } + + return false; +} + +function is_ip_auto_rangebanned($ip, $board, $thread_id, $browser_id, $since_ts = 0) { + $range_sql = explode('.', $ip); + + $range_sql = "{$range_sql[0]}.{$range_sql[1]}.%"; + + $thread_id = (int)$thread_id; + + if ($since_ts > 0) { + $since_sql = ' AND created_on <= FROM_UNIXTIME(' . ((int)$since_ts) . ')'; + } + else { + $since_sql = ''; + } + + $sql =<< DATE_SUB(NOW(), INTERVAL 120 MINUTE)$since_sql +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $board, $browser_id, $range_sql); + + if (!$res) { + return false; + } + + if (mysql_num_rows($res)) { + return true; + } + + return false; +} + +/** + * Dumps and formats HTTP headers and other request information for logging + */ +function spam_filter_format_http_headers($com = null, $country = null, $filename = null, $threat_score = null, $req_sig = null) { + $bot_headers = ''; + + foreach ($_SERVER as $_h_name => $_h_val) { + if (substr($_h_name, 0, 5) == 'HTTP_') { + if ($_h_name === 'HTTP_COOKIE') { + $_cookies = array_keys($_COOKIE); + $_cookies = array_intersect($_cookies, ['ws_style', 'nws_style', '4chan_pass', '_tcs', '_ga', 'cf_clearance' ]); + $_cookie_count = count($_COOKIE); + $bot_headers .= "HTTP_COOKIE: " . htmlspecialchars(implode(', ', $_cookies)) . " ($_cookie_count in total)\n"; + } + else if (strpos($_h_name, 'AUTH') !== false) { + continue; + } + else { + $bot_headers .= "$_h_name: " . htmlspecialchars($_h_val) . "\n"; + } + } + } + + $bot_headers .= "_POST: " . htmlspecialchars(implode(', ', array_keys($_POST))) . "\n"; + + if ($country !== null) { + $bot_headers .= "_Country: $country\n"; + } + + if ($threat_score !== null) { + $bot_headers .= "_Score: " . $threat_score . "\n"; + } + + if ($req_sig !== null) { + $bot_headers .= "_Sig: " . $req_sig . "\n"; + } + + if (isset($_COOKIE['_tcs'])) { + $bot_headers .= "_TCS: " . htmlspecialchars($_COOKIE['_tcs']) . "\n"; + } + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = UserPwd::getSession(); + + if ($userpwd) { + $bot_headers .= "_Pwd: " . htmlspecialchars($userpwd->getPwd()) . "\n"; + } + } + + if ($filename !== null) { + $bot_headers .= "_File: " . htmlspecialchars($filename) . "\n"; + } + + if ($com !== null) { + $bot_headers .= "_Comment: $com"; + } + + return $bot_headers; +} + +function spam_filter_get_req_sig() { + static $cache = null; + + if ($cache !== null) { + return $cache; + } + + $pick_headers = [ + 'HTTP_SEC_CH_UA_PLATFORM', + 'HTTP_SEC_CH_UA_MOBILE', + 'HTTP_SEC_CH_UA_MODEL', + 'HTTP_USER_AGENT', + 'HTTP_ACCEPT_LANGUAGE', + 'HTTP_SEC_FETCH_SITE', + 'HTTP_SEC_FETCH_MODE', + 'HTTP_SEC_FETCH_DEST', + ]; + + $need_headers = [ + 'HTTP_USER_AGENT', + 'HTTP_ACCEPT', + 'HTTP_REFERER', + 'HTTP_ACCEPT_LANGUAGE', + ]; + + $datapoints = []; + + $keys = []; + + foreach ($_SERVER as $k => $v) { + if (in_array($k, $pick_headers)) { + $keys[] = $k; + } + } + + $keyline = implode('.', $keys); + + $pointlines = [ + 'fetch_smd' => 'HTTP_SEC_FETCH_SITE.HTTP_SEC_FETCH_MODE.HTTP_SEC_FETCH_DEST', + 'fetch_dms' => 'HTTP_SEC_FETCH_DEST.HTTP_SEC_FETCH_MODE.HTTP_SEC_FETCH_SITE', + 'fetch_any' => 'HTTP_SEC_FETCH_' + ]; + + foreach ($pointlines as $key => $value) { + if (strpos($keyline, $value) !== false) { + $datapoints[] = $key; + } + } + + if (isset($_SERVER['HTTP_SEC_GPC']) || isset($_SERVER['HTTP_DNT'])) { + $datapoints[] = 'dnt_gpc'; + } + + if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { + $datapoints[] = 'xrw'; + } + + if (preg_match('/HTTP_SEC_CH_UA_[^.]+\.HTTP_SEC_CH_UA_[^.]+\.HTTP_SEC_CH_UA_/', $keyline)) { + $datapoints[] = 'ch_ua_block'; + } + + foreach ($need_headers as $k) { + if (!isset($_SERVER[$k])) { + $datapoints[] = 'missing'; + break; + } + } + + $sig = implode('+', $datapoints); + + if (!$sig) { + $sig = 'deadbeef'; + } + + $cache = substr(md5($sig), 0, 8); + + return $cache; +} + +// Covers 251044 (79.14 %) unique IPs and 10454 (77.09 %) unique bans +// IPs %: 0.1 | Bans %: 0 | GR1 all: 0 | Any EU: false +function spam_filter_is_asn_whitelisted() { + static $val = null; + + static $whitelist = [ + 21928, 6167, 7922, 7018, 812, 701, 1221, 20115, 22773, 2856, 3320, 6805, 577, + 3209, 852, 20057, 20001, 5089, 4804, 10796, 8151, 5617, 7545, 11427, 209, 16086, + 719, 33363, 15557, 5650, 133612, 6327, 6128, 5607, 206067, 1267, 26599, 6830, + 8881, 2119, 14593, 28573, 3215, 26615, 3352, 1759, 7303, 11426, 35228, 55836, + 1136, 22085, 11351, 8708, 1257, 3269, 5410, 7418, 23693, 30722, 9299, 13285, 17676, + 3301, 7713, 5391, 33915, 44034, 8374, 4764, 51207, 29447, 27651, 7552, 12389, + 19108, 8359, 11315, 6057, 16591, 12479, 5769, 17072, 31615, 12271, 28403, 11664, + 6147, 15704, 10139, 39603, 12874, 25135, 5483, 5378, 4771, 4775, 12912, 6871, + 12322, 6614, 132199, 5432, 212238, 12929, 27699, 22927, 8473, 2860, 12430, 7029, + 6848, 5645, 8412, 62240, 8447, 15502, 174, 30036, 27747, 14638, 4230, 45727, 9009, + 17639, 4788, 10030, 11492, 45143, 18881, 2516, 21334, 15895, 4766, 16232, 6799, + 8400, 9443, 6079, 13999, 5610, 45899, 4761, 2586, 12353, 20845, 20365, 13280, + 22047, 3243, 4818, 27995, 20055, 24203, 9500, 25255, 45609, 29518, 7992, 39891, + 3303, 4773, 855, 8452, 136787, 18403, 3329, 52341, + ]; + + if ($val !== null) { + return $val; + } + + if (isset($_SERVER['HTTP_X_GEO_ASN'])) { + $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; + } + else { + $asn = 0; + } + + if (!$asn) { + return true; + } + + $val = in_array($asn, $whitelist); + + return $val; +} + +function spam_filter_is_bad_actor() { + static $cache = null; + + if ($cache !== null) { + return $cache; + } + /* + if (isset($_SERVER['HTTP_X_HTTP_VERSION'])) { + if (strpos($_SERVER['HTTP_X_HTTP_VERSION'], 'HTTP/1') === 0) { + $cache = true; + return true; + } + } + */ + $no_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) === false; + $no_accept = isset($_SERVER['HTTP_ACCEPT']) === false; + + if ($no_lang && $no_accept) { + $cache = true; + return true; + } + + if ($no_lang && strpos($_SERVER['HTTP_USER_AGENT'], '; wv)') !== false) { + $cache = true; + return true; + } + + if ($no_accept && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') === false) { + $cache = true; + return true; + } + + if ($no_lang && isset($_SERVER['HTTP_REFERER'])) { + $ref = $_SERVER['HTTP_REFERER']; + + if (strpos($ref, 'sys.4chan.org') !== false || strpos($ref, '/thread/') !== false) { + $cache = true; + return true; + } + } + + $cache = false; + return false; +} + +function spam_filter_get_threat_score($country = null, $is_op = false, $multipart = true/*, &$log = []*/) { + $increase = []; + $more = []; + + $domain = DEFAULT_BURICHAN ? '4channel' : '4chan'; + + if (isset($_SERVER['HTTP_USER_AGENT'])) { + $ua = $_SERVER['HTTP_USER_AGENT']; + } + else { + $ua = ''; + } + + if (isset($_SERVER['HTTP_CONTENT_TYPE'])) { + $content_type = $_SERVER['HTTP_CONTENT_TYPE']; + } + else { + $content_type = ''; + } + + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $accept_lang = $_SERVER['HTTP_ACCEPT_LANGUAGE']; + } + else { + $accept_lang = ''; + } + + if (isset($_SERVER['HTTP_ACCEPT'])) { + $accept_header = $_SERVER['HTTP_ACCEPT']; + } + else { + $accept_header = ''; + } + + if (isset($_SERVER['HTTP_REFERER'])) { + $referer_header = $_SERVER['HTTP_REFERER']; + } + else { + $referer_header = ''; + } + + $header_keys = array_keys($_SERVER); + + $ua_is_webkit = false; + + $check_for_sec_headers = false; + + $is_mobile_ua = preg_match('/Android|Mobile/', $ua) || $_SERVER['HTTP_SEC_CH_UA_MOBILE'] === '?1'; + + $is_brave = false; + + if (isset($_SERVER['HTTP_SEC_CH_UA']) && strpos($_SERVER['HTTP_SEC_CH_UA'], 'Brave') !== false) { + if (isset($_SERVER['HTTP_SEC_GPC'])) { + $is_brave = true; + } + } + + // Mobile app (webviews, etc) + $is_webview = strpos($ua, '; wv') !== false; + $is_mobile_app = !$accept_header && !$accept_lang && ($is_webview || strpos($referer_header, '/thread/') !== false); + + if (!$is_mobile_app && !$accept_header && $accept_lang && strpos($referer_header, 'sys.4chan.org') !== false) { + $is_mobile_app = true; + } + + if (!$is_mobile_app && strpos($ua, 'Mozilla/') === false && preg_match('/Android|Dalvik|iOS|iPhone/', $ua)) { + $is_mobile_app = true; + } + + if (!$is_mobile_app && isset($_SERVER['HTTP_X_REQUESTED_WITH']) && preg_match('/floens|adamantcheese|clover/', $_SERVER['HTTP_X_REQUESTED_WITH'])) { + $is_mobile_app = true; + } + + if (!$is_mobile_app && preg_match('/boundary=[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$/', $content_type)) { + if (strpos($ua, 'Android') !== false) { + $is_mobile_app = true; + } + else if (strpos($ua, 'Firefox/') !== false && !$accept_lang) { + $is_mobile_app = true; + } + } + + // No UA + if (!$ua) { + $increase[] = 0.25; + //$log[] = 'NO_UA'; + } + // Firefox + else if ((strpos($ua, 'Firefox/') !== false || strpos($ua, 'FxiOS/') !== false) && strpos($ua, 'WebKit') === false) { + // Suspicious Content-Type + if ($multipart && !$is_mobile_app && !preg_match('/=-+([0-9]+|geckoformboundary[a-f0-9]+)$/i', $content_type)) { + $increase[] = 0.25; + //$log[] = 'BAD_CT_FF'; + } + + // Suspicious language + if (!$accept_lang) { + if (!$is_mobile_app) { + $increase[] = 0.02; + $more[] = 0.1; + //$log[] = 'NO_LANG'; + } + } + else if (preg_match('/[a-z]-[a-z]/', $accept_lang)) { + $increase[] = 0.1; + //$log[] = 'LC_LANG'; + } + + if (isset($_SERVER['HTTP_PRAGMA']) && !isset($_SERVER['HTTP_CACHE_CONTROL'])) { + $increase[] = 0.35; + $more[] = 0.1; + //$log[] = 'FF_PRAGMA'; + } + + // Wrong Accept header + if (strpos($accept_header, 'application/signed-exchange') !== false) { + $increase[] = 0.15; + //$log[] = 'FF_SIGEX'; + } + + // Old and spoofed versions + if ($accept_header && preg_match('/(?:Firefox)\/([0-9]+)[^0-9]/', $ua, $m) && strpos($ua, 'PaleMoon') === false) { + $v = (int)$m[1]; + + if ($v < 52) { + $increase[] = 0.2; + //$log[] = 'OLD_FF'; + } + else if ($v < 60) { + $increase[] = 0.1; + //$log[] = 'OLD_FF'; + } + else if ($v < 78) { + $increase[] = 0.01; + //$log[] = 'OLD_FF'; + } + else if ($v > 500) { + $increase[] = 0.5; + //$log[] = 'FUTURE_FF'; + } + + if ($v > 110) { + $check_for_sec_headers = true; + } + } + } + // Webkit + else if (strpos($ua, 'WebKit') !== false) { + $ua_is_webkit = true; + $ua_is_chrome = strpos($ua, 'Chrome') !== false; + + // Suspicious Content-Type + if ($multipart && !$is_mobile_app) { + if (!strpos($content_type, 'WebKit')) { + $increase[] = 0.25; + //$log[] = 'BAD_CT_WK'; + } + else if (strpos($content_type, '-') === false) { + $increase[] = 0.50; + //$log[] = 'BAD_CT_DASH'; + } + } + + // Suspicious language + if (!$accept_lang) { + if (!$is_mobile_app) { + $increase[] = 0.02; + $more[] = 0.1; + //$log[] = 'NO_LANG'; + } + } + else if ($ua_is_chrome && strpos($ua, 'Android') === false && preg_match('/[a-z]-[a-z]/', $accept_lang)) { + $increase[] = 0.1; + //$log[] = 'LC_LANG'; + } + + // Old and spoofed versions + if (preg_match('/(?:Chrome)\/([0-9]+)[^0-9]/', $ua, $m)) { + $v = (int)$m[1]; + + if ($v < 60) { + $increase[] = 0.2; + //$log[] = 'OLD_WK'; + } + else if ($v < 70) { + $increase[] = 0.1; + //$log[] = 'OLD_WK'; + } + else if ($v < 80) { + $increase[] = 0.05; + //$log[] = 'OLD_WK'; + } + else if ($v > 500) { + $increase[] = 0.5; + //$log[] = 'FUTURE_WK'; + } + + if ($v > 110) { + $check_for_sec_headers = true; + } + } + + if (preg_match('/(?:Safari)\/([0-9]+)/', $ua, $m)) { + $v = (int)$m[1]; + + if ($v < 530) { + $increase[] = 0.5; + //$log[] = 'OLD_SAFARI'; + } + } + + // iPhone UA too short + if (strpos($ua, 'iPhone') !== false && strpos($ua, 'Mobile') === false) { + $increase[] = 0.06; + //$log[] = 'SHORT_IPHONE'; + } + } + // Other + else { + if (!$is_mobile_app && $multipart && preg_match('/boundary=[a-zA-Z0-9]+$/', $content_type)) { + $increase[] = 0.5; + //$log[] = 'STRANGE_CT'; + } + + if (preg_match('/Netscape\/|Opera\b|Camino\/|Trident\/|Presto\/|compatible; MSIE /', $ua)) { + $increase[] = 0.75; + //$log[] = 'OLD_UA'; + } + else if (!$is_mobile_app && strpos($ua, 'Mozilla/') === false) { + $increase[] = 0.15; + $more[] = 0.1; + //$log[] = 'STRANGE_UA'; + } + + if (!$is_mobile_app && !$accept_lang) { + $more[] = 0.25; + //$log[] = 'NO_LANG'; + } + + // UA too short + if (!$is_mobile_app && (strlen($ua) < 25 || strpos($ua, ' ') === false)) { + $increase[] = 0.25; + //$log[] = 'UA_SPOOF'; + } + } + + // Suspicious Content-Type + if ($multipart) { + if (strpos($content_type, 'WebKit') && !$ua_is_webkit) { + $increase[] = 0.25; + //$log[] = 'BAD_UA_CT_WK'; + } + } + + // Sec-Fetch headers should be together + // Some iPhones have those separated + if (!$is_brave && !$is_webview && strpos($ua, 'Chrome') !== false) { + $_sf_start = false; + $_sf_end = false; + + foreach ($_SERVER as $_hdr => $_value) { + if (strpos($_hdr, 'HTTP_SEC_FETCH_') === 0) { + if ($_sf_start && $_sf_end) { + $increase[] = 0.25; + //$log[] = 'SPARSE_SEC_FETCH'; + break; + } + + $_sf_start = true; + } + else if ($_sf_start) { + $_sf_end = true; + } + } + } + + // HTTP_SEC_FETCH_USER should always be ?1 + if (isset($_SERVER['HTTP_SEC_FETCH_USER']) && $_SERVER['HTTP_SEC_FETCH_USER'] !== '?1') { + $increase[] = 0.15; + //$log[] = 'BAD_SEC_FU'; + } + + // Unusual Accept header + if ($accept_header) { + if (strpos($accept_header, 'text/plain') !== false) { + $increase[] = 0.05; + //$log[] = 'ACCEPT_TP'; + } + } + + // Referer is set but is empty + if (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], '4chan.org') === false) { + $increase[] = 0.1; + //$log[] = 'BAD_REFERER'; + } + + // Platform mismatch: client hints vs user agent + if (isset($_SERVER['HTTP_SEC_CH_UA_PLATFORM']) && $ua) { + $_ch_platform = $_SERVER['HTTP_SEC_CH_UA_PLATFORM']; + + if (strpos($ua, 'Windows') !== false) { + if (strpos($_ch_platform, 'Windows') === false) { + $increase[] = 0.5; + //$log[] = 'CH_BAD_PLATFORM'; + } + } + else if (strpos($ua, 'Mac OS') !== false) { + if (strpos($_ch_platform, 'macOS') === false) { + $increase[] = 0.5; + //$log[] = 'CH_BAD_PLATFORM'; + } + } + else if (strpos($ua, 'Linux') !== false) { + if (preg_match('/Linux|Android|BSD/', $_ch_platform) === false) { + $increase[] = 0.5; + //$log[] = 'CH_BAD_PLATFORM'; + } + } + } + + if (isset($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS']) && $accept_header === '*/*') { + $increase[] = 0.2; + $more[] = 0.1; + //$log[] = 'ACCEPT_UIR'; + } + + if (!isset($_SERVER['HTTP_PRAGMA']) && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') === false) { + $increase[] = 0.09; + //$log[] = 'BAD_IPHONE_WV'; + } + + // Suspicious OS + if (strpos($ua, 'Windows NT ') !== false) { + if (preg_match('/Windows NT ([0-9]+)/', $ua, $m) && strpos($ua, 'Mypal/') === false) { + $v = (int)$m[1]; + + if ($v < 6) { + if (strpos($ua, 'Goanna') === false) { + $increase[] = 0.25; + } + else { + $increase[] = 0.03; + } + //$log[] = 'OLD_WIN'; + } + else if ($v < 10) { + $increase[] = 0.03; + //$log[] = 'OLD_WIN'; + } + else if ($v > 10) { + $increase[] = 0.5; + //$log[] = 'FUTURE_WIN'; + } + } + } + else if (strpos($ua, 'Mac OS X ') !== false) { + if (preg_match('/Mac OS X ([0-9]+)_([0-9]+)/', $ua, $m)) { + $v_maj = (int)$m[1]; + $v_min = (int)$m[2]; + + if ($v_maj < 10) { + $increase[] = 0.5; + //$log[] = 'OLD_OSX'; + } + else if ($v_maj == 10) { + if ($v_min < 7) { + $increase[] = 0.25; + //$log[] = 'OLD_OSX'; + } + else if ($v_min < 12) { + $increase[] = 0.05; + //$log[] = 'OLD_OSX'; + } + } + else if ($v_maj > 10 && strpos($ua, 'Safari') !== false) { + $increase[] = 0.30; + //$log[] = 'FUTURE_OSX'; + } + } + } + else if (strpos($ua, 'Android') !== false) { + if (preg_match('/Android ([0-9]+)/', $ua, $m)) { + $v = (int)$m[1]; + + if ($v < 4) { + $increase[] = 0.25; + //$log[] = 'OLD_DROID'; + } + else if ($v < 8) { + $increase[] = 0.05; + //$log[] = 'OLD_DROID'; + } + else if ($v > 20) { + $increase[] = 0.5; + //$log[] = 'FUTURE_DROID'; + } + } + + if (strpos($ua, 'Win64;') !== false) { + $increase[] = 0.25; + //$log[] = 'OS_SOUP'; + } + } + + // Spoofed OS + if (preg_match('/Mozilla|Firefox|Chrome/', $ua) && !preg_match('/Windows NT|Android|Linux|Mac|iOS|X11;|BSD|Nintendo|PlayStation|Steam/', $ua)) { + $increase[] = 0.20; + //$log[] = 'NO_OS'; + } + + // Non-browser user agents + if (preg_match('/headless|node-fetch|python-|java\/|jakarta|-perl|http-?client|-resty-|awesomium\//i', $ua)) { + $increase[] = 1.0; + //$log[] = 'NOT_BROWSER'; + } + + // Wrong content type + if ($multipart) { + // Posting + if ($_SERVER['HTTP_CONTENT_TYPE'] === 'application/x-www-form-urlencoded') { + $increase[] = 0.75; + //$log[] = 'BAD_CT_MP'; + } + } + else if (!$is_mobile_app) { + // Reporting + if ($_SERVER['HTTP_CONTENT_TYPE'] !== 'application/x-www-form-urlencoded') { + $increase[] = 0.75; + //$log[] = 'BAD_CT_NMP'; + } + } + + // Unusual headers + if (isset($_SERVER['HTTP_VARY'])) { + $increase[] = 0.2; + //$log[] = 'VARY_HDR'; + } + + if (isset($_SERVER['HTTP_PATH']) || isset($_SERVER['HTTP_SAME_ORIGIN']) || isset($_SERVER['HTTP_REFERRER_POLICY'])) { + $increase[] = 0.8; + //$log[] = 'USELESS_HDR'; + } + + if (!$is_mobile_app && !$is_webview) { + if (isset($_SERVER['HTTP_SEC_FETCH_MODE']) && $_SERVER['HTTP_SEC_FETCH_MODE'] === 'navigate') { + // Only threads should be posted using the default form + if (!$is_op && !$is_mobile_ua) { + $increase[] = 0.02; + $more[] = 0.10; + //$log[] = 'BAD_OP_SFM'; + } + + // Model hints are never sent when using the default form + if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && !isset($_SERVER['HTTP_SEC_CH_UA_BITNESS'])) { + $increase[] = 0.1; + $more[] = 0.20; + //$log[] = 'MODEL_NAV'; + } + } + } + + // iPhone fetch site none + if (strpos($ua, 'like Mac OS') && isset($_SERVER['HTTP_SEC_FETCH_SITE']) && $_SERVER['HTTP_SEC_FETCH_SITE'] === 'none') { + $increase[] = 0.08; + $more[] = 0.10; + //$log[] = 'IOS_FSN'; + } + + // No cookies + if (!$is_mobile_app) { + if (!isset($_SERVER['HTTP_COOKIE']) || !$_SERVER['HTTP_COOKIE']) { + $increase[] = 0.05; + $more[] = 0.25; + //$log[] = 'NO_COOKIE'; + } + else if (count($_COOKIE) === 1 && isset($_COOKIE['cf_clearance'])) { + $increase[] = 0.05; + $more[] = 0.25; + //$log[] = 'NO_COOKIE'; + } + } + + // Timezones and Time + if (isset($_COOKIE['_tcs'])) { + list($_time, $_tz, $_time_s, $_tcs_v) = explode('.', $_COOKIE['_tcs']); + + if (!$_tcs_v) { + $increase[] = 0.09; + //$log[] = 'BAD_TCS'; + } + else { + if (!$is_webview && strpos($ua, 'Chrome/') !== false && $_tcs_v != 33) { + $increase[] = 0.09; + //$log[] = 'BAD_TCS_CR'; + } + } + + if ($_time_s && isset($_POST['t-challenge']) && $_POST['t-challenge'] !== 'noop' && $_POST['t-response']) { + $_d = $_SERVER['REQUEST_TIME'] - $_time_s; + + if ($_d > 0 && $_d < 2) { + $increase[] = 1.0; + //$log[] = 'FAST_TCS'; + } + } + + if (isset($_SERVER['HTTP_X_TIMEZONE'])) { + $_tz0 = explode('/', $_tz, 2)[0]; + if ($_tz0) { + if ($_tz0 === 'UTC') { + $increase[] = 0.02; + $more[] = 0.02; + //$log[] = 'UTC_TZ'; + } + else if ($_tz0 === 'Etc') { + $increase[] = 0.01; + $more[] = 0.01; + //$log[] = 'ETC_TZ'; + } + else if (strpos($_SERVER['HTTP_X_TIMEZONE'], $_tz0) !== 0) { + if (strpos($_tz0, 'Atlantic') === false || strpos($_SERVER['HTTP_X_TIMEZONE'], 'Europe') === false) { + $increase[] = 0.03; + $more[] = 0.03; + //$log[] = 'BAD_TZ'; + } + } + } + } + } + + // No Accept + if (!$accept_header && !$is_mobile_app) { + $increase[] = 0.15; + //$log[] = 'NO_ACCEPT'; + } + + // No SEC + if ($check_for_sec_headers && !$is_mobile_app) { + if (!isset($_SERVER['HTTP_SEC_FETCH_DEST']) || !isset($_SERVER['HTTP_SEC_FETCH_MODE']) || !isset($_SERVER['HTTP_SEC_FETCH_SITE'])) { + $increase[] = 0.15; + //$log[] = 'NO_SEC'; + } + } + + // HTTP 1.1 + if (isset($_SERVER['HTTP_X_HTTP_VERSION']) && $_SERVER['HTTP_X_HTTP_VERSION'] === 'HTTP/1.1') { + $more[] = 0.1; + //$log[] = 'HTTP1'; + } + + // Spoofed device model + if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && isset($_SERVER['HTTP_SEC_CH_UA_PLATFORM'])) { + $_k1 = (int)array_search('HTTP_SEC_CH_UA_MODEL', $header_keys); + $_k2 = (int)array_search('HTTP_SEC_CH_UA_PLATFORM', $header_keys); + + if (abs($_k1 - $_k2) > 5) { + $increase[] = 0.05; + $more[] = 0.01; + //$log[] = 'FAR_MODEL'; + } + } + + if ($country) { + // Language is set but doesn't match the country + if ($accept_lang && strpos($accept_lang, 'en') !== 0) { + $lang_regex = get_lang_regex_from_country($country); + + if ($lang_regex && !preg_match($lang_regex, $accept_lang)) { + $more[] = 0.025; + $increase[] = 0.025; + //$log[] = 'LANG_MISMATCH'; + } + + // No quality in lang + if (!preg_match('/iPhone|iPad/', $ua)) { + if ($accept_lang && strpos($accept_lang, ';') === false) { + $more[] = 0.025; + $increase[] = 0.025; + //$log[] = 'LANG_NOQ'; + } + } + } + + // Highly suspicious countries + $countries_0 = array( + 'AD','AE','AF','AG','AI','AL','AM','AN','AO','AQ','AS','AW','AX','AZ', + 'BB','BD','BF','BH','BI','BJ','BL','BM','BN','BO','BQ','BS','BT','BV','BW','BZ', + 'CC','CD','CF','CG','CI','CK','CM','CN','CR','CU','CV','CW','CX', + 'DJ','DM','DO','DZ', + 'EC','EG','EH','ER','ET', + 'FJ','FK','FM','FO', + 'GA','GD','GF','GG','GH','GI','GM','GN','GP','GQ','GS','GT','GU','GW','GY', + 'HK','HM','HN','HT', + 'IM','IO','IQ','IR','JE','JM','JO','KE','KG','KH','KI','KM','KN','KP','KW','KY','KZ', + 'LA','LB','LC','LK','LR','LS','LY', + 'MA','MD','MF','MG','MH','ML','MM','MN','MO','MP','MQ','MR','MS','MU','MV','MW','MZ', + 'NA','NC','NE','NF','NG','NI','NP','NR','NU', + 'OM','PA','PF','PG','PK','PM','PN','PS','PW','PY','QA','RE','RW', + 'SA','SB','SC','SD','SH','SJ','SL','SM','SN','SO','SR','SS','ST','SV','SX','SY','SZ', + 'TC','TD','TF','TG','TJ','TK','TM','TN','TO','TP','TR','TT','TV','TZ', + 'UG','UM','UZ','VA','VC','VG','VI','VU','WF','WS','YE','YT','YU','ZM','ZW','XX' + ); + + // Less suspicious countries + $countries_1 = array( + 'BR','VE','AR','CL','UY','CO','PE','MX', + 'UA','BA','RU','MC','MK','CY','MT','ME','KR','JP','TH','VN','ID' + ); + + if (in_array($country, $countries_0)) { + $more[] = 0.30; + //$log[] = 'SUSP_COUNTRY_0'; + } + else if (in_array($country, $countries_1)) { + $more[] = 0.10; + //$log[] = 'SUSP_COUNTRY_1'; + } + } + + $score = 0.0; + + if (!empty($increase)) { + $score += array_sum($increase); + } + + if ($score > 0.0 && !empty($more)) { + foreach ($more as $r) { + $score *= (1.0 + $r); + } + } + + return round($score, 2); +} + +/** + * Necrobumping prevention checks + */ +function spam_filter_can_bump_thread($thread_root) { + $userpwd = UserPwd::getSession(); + + if (!$userpwd || !$thread_root) { + return true; + } + + if ($userpwd->maskLifetime() >= 21600) { // 6 hours + return true; + } + + $thres = (int)(PAGE_MAX * DEF_PAGES * 0.85); + + if ($thres <= 0) { + return true; + } + + $sql = "SELECT COUNT(*) FROM `%s` WHERE resto = 0 AND archived = 0 AND root > '%s'"; + + $res = mysql_board_call($sql, BOARD_DIR, $thread_root); + + if (!$res) { + return true; + } + + $pos = (int)mysql_fetch_row($res)[0]; + + if ($pos < $thres) { + return true; + } + + // Check the IP if cookies are blocked + if (spam_filter_is_ip_known(ip2long($_SERVER['REMOTE_ADDR']), BOARD_DIR, 0, 720)) { + return true; + } + + return false; +} + +/** + * Returns true if the uploaded file had multiple previous bans for it + * and should be blocked + */ +function check_for_banned_upload($md5) { + if (!$md5 || BOARD_DIR === 'b') { + return false; + } + + // 6 : Global 5 - NWS on Worksafe Board + // 226 : Global 3 - Loli/shota pornography + $template_clause = '226'; + + if (DEFAULT_BURICHAN) { + $template_clause .= ',6'; + } + + $sql = <<= 3) { + return true; + } + + return false; +} + +function spam_filter_is_likely_automated($memcached = null, $threshold = 29) { + if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { + return false; + } + + $ua = $_SERVER['HTTP_USER_AGENT']; + + // Skip Android Webviews + if (strpos($ua, '; wv)') !== false) { + return false; + } + + // Skip iPhone Webviews + if (preg_match('/iPhone|iPad/', $ua) && !preg_match('/Mobile|Safari/', $ua)) { + return false; + } + + $score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + + if ($score > 0 && $score <= $threshold) { + return true; + } + + if ($memcached) { + $key = 'bmbot' . $_SERVER['REMOTE_ADDR']; + + if ($memcached->get($key)) { + return true; + } + } + + return false; +} + +function spam_filter_is_pwd_blocked($pwd, $type, $hours = 24) { + if (!$pwd || !$type || $hours <= 0 || $hours > 720) { + return false; + } + + $hours = (int)$hours; + + $sql =<< DATE_SUB(NOW(), INTERVAL $hours HOUR) +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $type, $pwd); + + if (!$res) { + return false; + } + + return mysql_num_rows($res) === 1; +} + +function spam_filter_has_country_changed($pwd) { + if (!$pwd) { + return false; + } + + $sql =<< DATE_SUB(NOW(), INTERVAL 24 HOUR) +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $pwd); + + if (!$res) { + return false; + } + + return mysql_num_rows($res) === 1; +} + +// Logs posts made by new users. +// Returns 1 if the posting rate is above limits. +function spam_filter_is_post_flood($ip, $userpwd, $board, $thread_id, $phash) { + $user_is_known = $userpwd && $userpwd->isUserKnownOrVerified(60); + + if ($user_is_known) { + return 0; + } + + $thread_id = (int)$thread_id; + + // Per thread reply flood + if ($thread_id) { + $interval_minutes = (int)ANTIFLOOD_INTERVAL_REPLY; + $threshold = (int)ANTIFLOOD_THRES_REPLY; + } + // OP flood + else { + $interval_minutes = (int)ANTIFLOOD_INTERVAL_OP; + $threshold = (int)ANTIFLOOD_THRES_OP; + } + + if (!$interval_minutes || !$threshold) { + return 0; + } + + $long_ip = ip2long($ip); + + if (!$long_ip) { + return 0; + } + + $tbl = 'flood_log'; + + // Prune old entries + $sql = "DELETE FROM `$tbl` WHERE created_on < DATE_SUB(NOW(), INTERVAL 24 HOUR)"; + mysql_global_call($sql); + + // Count flood entries + $ret_val = 0; + + $sql = <<= DATE_SUB(NOW(), INTERVAL $interval_minutes MINUTE) +AND board = '%s' +AND thread_id = $thread_id +SQL; + + $res = mysql_global_call($sql, $ip, $board); + + if ($res) { + $count = (int)mysql_fetch_row($res)[0]; + + if ($count >= $threshold) { + $ret_val = 1; + } + } + + // Insert new entry + $ua_sig = spam_filter_get_browser_id(); + $req_sig = spam_filter_get_req_sig(); + + $sql = << '/\b(?:ca)\b/', + 'AE' => '/\b(?:ar|fa|hi|ur)\b/', + 'AF' => '/\b(?:fa|ps|uz|tk)\b/', + 'AL' => '/\b(?:sq|el)\b/', + 'AM' => '/\b(?:hy)\b/', + 'AO' => '/\b(?:pt)\b/', + 'AR' => '/\b(?:es|it|de|fr|gn)\b/', + 'AS' => '/\b(?:sm|to)\b/', + 'AT' => '/\b(?:de|hr|hu|sl)\b/', + 'AW' => '/\b(?:nl|pap|es)\b/', + 'AX' => '/\b(?:sv)\b/', + 'AZ' => '/\b(?:az|ru|hy)\b/', + 'BA' => '/\b(?:bs|hr|sr)\b/', + 'BD' => '/\b(?:bn)\b/', + 'BE' => '/\b(?:nl|fr|de)\b/', + 'BF' => '/\b(?:fr|mos)\b/', + 'BG' => '/\b(?:bg|tr|rom)\b/', + 'BH' => '/\b(?:ar|fa|ur)\b/', + 'BI' => '/\b(?:fr|rn)\b/', + 'BJ' => '/\b(?:fr)\b/', + 'BL' => '/\b(?:fr)\b/', + 'BM' => '/\b(?:pt)\b/', + 'BN' => '/\b(?:ms)\b/', + 'BO' => '/\b(?:es|qu|ay)\b/', + 'BQ' => '/\b(?:nl|pap)\b/', + 'BR' => '/\b(?:pt|es|fr)\b/', + 'BT' => '/\b(?:dz)\b/', + 'BW' => '/\b(?:tn)\b/', + 'BY' => '/\b(?:be|ru)\b/', + 'BZ' => '/\b(?:es)\b/', + 'CA' => '/\b(?:fr|iu)\b/', + 'CC' => '/\b(?:ms)\b/', + 'CD' => '/\b(?:fr|ln|ktu|kg|sw|lua)\b/', + 'CF' => '/\b(?:fr|sg|ln|kg)\b/', + 'CG' => '/\b(?:fr|kg|ln)\b/', + 'CH' => '/\b(?:de|fr|it|rm)\b/', + 'CI' => '/\b(?:fr)\b/', + 'CK' => '/\b(?:mi)\b/', + 'CL' => '/\b(?:es)\b/', + 'CM' => '/\b(?:fr)\b/', + 'CN' => '/\b(?:zh|yue|wuu|dta|ug|za)\b/', + 'CO' => '/\b(?:es)\b/', + 'CR' => '/\b(?:es)\b/', + 'CU' => '/\b(?:es|pap)\b/', + 'CV' => '/\b(?:pt)\b/', + 'CW' => '/\b(?:nl|pap)\b/', + 'CX' => '/\b(?:zh|ms)\b/', + 'CY' => '/\b(?:el|tr)\b/', + 'CZ' => '/\b(?:cs|sk)\b/', + 'DE' => '/\b(?:de)\b/', + 'DJ' => '/\b(?:fr|ar|so|aa)\b/', + 'DK' => '/\b(?:da|fo|de)\b/', + 'DO' => '/\b(?:es)\b/', + 'DZ' => '/\b(?:ar|fr)\b/', + 'EC' => '/\b(?:es)\b/', + 'EE' => '/\b(?:et|ru)\b/', + 'EG' => '/\b(?:ar|fr)\b/', + 'EH' => '/\b(?:ar|mey)\b/', + 'ER' => '/\b(?:aa|ar|tig|kun|ti)\b/', + 'ES' => '/\b(?:es|ca|gl|eu|oc)\b/', + 'ET' => '/\b(?:am|om|ti|so|sid)\b/', + 'FI' => '/\b(?:fi|sv|smn)\b/', + 'FJ' => '/\b(?:fj)\b/', + 'FM' => '/\b(?:chk|pon|yap|kos|uli|woe|nkr|kpg)\b/', + 'FO' => '/\b(?:fo|da)\b/', + 'FR' => '/\b(?:fr|frp|br|co|ca|eu|oc)\b/', + 'GA' => '/\b(?:fr)\b/', + 'GB' => '/\b(?:en)\b/', + 'GE' => '/\b(?:ka|ru|hy|az)\b/', + 'GF' => '/\b(?:fr)\b/', + 'GG' => '/\b(?:nrf)\b/', + 'GH' => '/\b(?:ak|ee|tw)\b/', + 'GI' => '/\b(?:es|it|pt)\b/', + 'GL' => '/\b(?:kl|da)\b/', + 'GM' => '/\b(?:mnk|wof|wo|ff)\b/', + 'GN' => '/\b(?:fr)\b/', + 'GP' => '/\b(?:fr)\b/', + 'GQ' => '/\b(?:es|fr)\b/', + 'GR' => '/\b(?:el|fr)\b/', + 'GT' => '/\b(?:es)\b/', + 'GU' => '/\b(?:ch)\b/', + 'GW' => '/\b(?:pt|pov)\b/', + 'HK' => '/\b(?:zh|yue|zh)\b/', + 'HN' => '/\b(?:es|cab|miq)\b/', + 'HR' => '/\b(?:hr|sr)\b/', + 'HT' => '/\b(?:ht|fr)\b/', + 'HU' => '/\b(?:hu)\b/', + 'ID' => '/\b(?:id|nl|jv)\b/', + 'IE' => '/\b(?:ga)\b/', + 'IL' => '/\b(?:he|ar)\b/', + 'IM' => '/\b(?:gv)\b/', + 'IN' => '/\b(?:hi|bn|te|mr|ta|ur|gu|kn|ml|or|pa|as|bh|sat|ks|ne|sd|kok|doi|mni|sit|sa|fr|lus|inc)\b/', + 'IQ' => '/\b(?:ar|ku|hy)\b/', + 'IR' => '/\b(?:fa|ku)\b/', + 'IS' => '/\b(?:is|de|da|sv|no)\b/', + 'IT' => '/\b(?:it|de|fr|sc|ca|co|sl)\b/', + 'JE' => '/\b(?:fr|nrf)\b/', + 'JO' => '/\b(?:ar)\b/', + 'JP' => '/\b(?:ja)\b/', + 'KE' => '/\b(?:sw)\b/', + 'KG' => '/\b(?:ky|uz|ru)\b/', + 'KH' => '/\b(?:km|fr)\b/', + 'KI' => '/\b(?:gil)\b/', + 'KM' => '/\b(?:ar|fr)\b/', + 'KP' => '/\b(?:ko)\b/', + 'KR' => '/\b(?:ko)\b/', + 'XK' => '/\b(?:sq|sr)\b/', + 'KW' => '/\b(?:ar)\b/', + 'KZ' => '/\b(?:kk|ru)\b/', + 'LA' => '/\b(?:lo|fr)\b/', + 'LB' => '/\b(?:ar|fr|hy)\b/', + 'LI' => '/\b(?:de)\b/', + 'LK' => '/\b(?:si|ta)\b/', + 'LS' => '/\b(?:st|zu|xh)\b/', + 'LT' => '/\b(?:lt|ru|pl)\b/', + 'LU' => '/\b(?:lb|de|fr)\b/', + 'LV' => '/\b(?:lv|ru|lt)\b/', + 'LY' => '/\b(?:ar|it)\b/', + 'MA' => '/\b(?:ar|ber|fr)\b/', + 'MC' => '/\b(?:fr|it)\b/', + 'MD' => '/\b(?:ro|ru|gag|tr)\b/', + 'ME' => '/\b(?:sr|hu|bs|sq|hr|rom)\b/', + 'MF' => '/\b(?:fr)\b/', + 'MG' => '/\b(?:fr|mg)\b/', + 'MH' => '/\b(?:mh)\b/', + 'MK' => '/\b(?:mk|sq|tr|rmm|sr)\b/', + 'ML' => '/\b(?:fr|bm)\b/', + 'MM' => '/\b(?:my)\b/', + 'MN' => '/\b(?:mn|ru)\b/', + 'MO' => '/\b(?:zh|zh|pt)\b/', + 'MP' => '/\b(?:fil|tl|zh|ch)\b/', + 'MQ' => '/\b(?:fr)\b/', + 'MR' => '/\b(?:ar|fuc|snk|fr|mey|wo)\b/', + 'MT' => '/\b(?:mt)\b/', + 'MU' => '/\b(?:bho|fr)\b/', + 'MV' => '/\b(?:dv)\b/', + 'MW' => '/\b(?:ny|yao|tum|swk)\b/', + 'MX' => '/\b(?:es)\b/', + 'MY' => '/\b(?:ms|zh|ta|te|ml|pa|th)\b/', + 'MZ' => '/\b(?:pt|vmw)\b/', + 'NA' => '/\b(?:af|de|hz|naq)\b/', + 'NC' => '/\b(?:fr)\b/', + 'NE' => '/\b(?:fr|ha|kr|dje)\b/', + 'NG' => '/\b(?:ha|yo|ig|ff)\b/', + 'NI' => '/\b(?:es)\b/', + 'NL' => '/\b(?:nl|fy)\b/', + 'NO' => '/\b(?:no|nb|nn|se|fi)\b/', + 'NP' => '/\b(?:ne)\b/', + 'NR' => '/\b(?:na)\b/', + 'NU' => '/\b(?:niu)\b/', + 'NZ' => '/\b(?:mi)\b/', + 'OM' => '/\b(?:ar|bal|ur)\b/', + 'PA' => '/\b(?:es)\b/', + 'PE' => '/\b(?:es|qu|ay)\b/', + 'PF' => '/\b(?:fr|ty)\b/', + 'PG' => '/\b(?:ho|meu|tpi)\b/', + 'PH' => '/\b(?:tl|fil|ceb|tgl|ilo|hil|war|pam|bik|bcl|pag|mrw|tsg|mdh|cbk|krj|sgd|msb|akl|ibg|yka|mta|abx)\b/', + 'PK' => '/\b(?:ur|pa|sd|ps|brh)\b/', + 'PL' => '/\b(?:pl)\b/', + 'PM' => '/\b(?:fr)\b/', + 'PR' => '/\b(?:es)\b/', + 'PS' => '/\b(?:ar)\b/', + 'PT' => '/\b(?:pt|mwl)\b/', + 'PW' => '/\b(?:pau|sov|tox|ja|fil|zh)\b/', + 'PY' => '/\b(?:es|gn)\b/', + 'QA' => '/\b(?:ar|es)\b/', + 'RE' => '/\b(?:fr)\b/', + 'RO' => '/\b(?:ro|hu|rom)\b/', + 'RS' => '/\b(?:sr|hu|bs|rom)\b/', + 'RU' => '/\b(?:ru|tt|xal|cau|ady|kv|ce|tyv|cv|udm|tut|mns|bua|myv|mdf|chm|ba|inh|tut|kbd|krc|av|sah|nog)\b/', + 'RW' => '/\b(?:rw|fr|sw)\b/', + 'SA' => '/\b(?:ar)\b/', + 'SB' => '/\b(?:tpi)\b/', + 'SC' => '/\b(?:fr)\b/', + 'SD' => '/\b(?:ar|fia)\b/', + 'SE' => '/\b(?:sv|se|sma|fi)\b/', + 'SG' => '/\b(?:cmn|ms|ta|zh)\b/', + 'SI' => '/\b(?:sl|sh)\b/', + 'SJ' => '/\b(?:no|ru)\b/', + 'SK' => '/\b(?:sk|hu)\b/', + 'SL' => '/\b(?:mtem)\b/', + 'SM' => '/\b(?:it)\b/', + 'SN' => '/\b(?:fr|wo|fuc|mnk)\b/', + 'SO' => '/\b(?:so|ar|it)\b/', + 'SR' => '/\b(?:nl|srn|hns|jv)\b/', + 'ST' => '/\b(?:pt)\b/', + 'SV' => '/\b(?:es)\b/', + 'SX' => '/\b(?:nl)\b/', + 'SY' => '/\b(?:ar|ku|hy|arc|fr)\b/', + 'SZ' => '/\b(?:ss)\b/', + 'TD' => '/\b(?:fr|ar|sre)\b/', + 'TF' => '/\b(?:fr)\b/', + 'TG' => '/\b(?:fr|ee|hna|kbp|dag|ha)\b/', + 'TH' => '/\b(?:th)\b/', + 'TJ' => '/\b(?:tg|ru)\b/', + 'TK' => '/\b(?:tkl)\b/', + 'TL' => '/\b(?:tet|pt|id)\b/', + 'TM' => '/\b(?:tk|ru|uz)\b/', + 'TN' => '/\b(?:ar|fr)\b/', + 'TO' => '/\b(?:to)\b/', + 'TR' => '/\b(?:tr|ku|diq|az|av)\b/', + 'TT' => '/\b(?:hns|fr|es|zh)\b/', + 'TV' => '/\b(?:tvl|sm|gil)\b/', + 'TW' => '/\b(?:zh|zh|nan|hak)\b/', + 'TZ' => '/\b(?:sw|ar)\b/', + 'UA' => '/\b(?:uk|ru|rom|pl|hu)\b/', + 'UG' => '/\b(?:lg|sw|ar)\b/', + 'US' => '/\b(?:en|es)\b/', + 'UY' => '/\b(?:es)\b/', + 'UZ' => '/\b(?:uz|ru|tg)\b/', + 'VA' => '/\b(?:la|it|fr)\b/', + 'VC' => '/\b(?:fr)\b/', + 'VE' => '/\b(?:es)\b/', + 'VN' => '/\b(?:vi|fr|zh|km)\b/', + 'VU' => '/\b(?:bi|fr)\b/', + 'WF' => '/\b(?:wls|fud|fr)\b/', + 'WS' => '/\b(?:sm)\b/', + 'YE' => '/\b(?:ar)\b/', + 'YT' => '/\b(?:fr)\b/', + 'ZA' => '/\b(?:zu|xh|af|nso|tn|st|ts|ss|ve|nr)\b/', + 'ZM' => '/\b(?:bem|loz|lun|lue|ny|toi)\b/', + 'ZW' => '/\b(?:sn|nr|nd)\b/', + 'CS' => '/\b(?:cu|hu|sq|sr)\b/', + 'AN' => '/\b(?:nl|es)\b/' + ]; + + if (isset($codes[$country])) { + return $codes[$country]; + } + + return null; +} diff --git a/lib/rpc.php b/lib/rpc.php new file mode 100644 index 0000000..d503121 --- /dev/null +++ b/lib/rpc.php @@ -0,0 +1,390 @@ + true, //...? + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_RETURNTRANSFER => true, + + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => $internal ? $rpc_internal_timeout : $rpc_external_timeout, + CURLOPT_USERAGENT => "4chan.org", + ); + + $curlopts = $options + $curlopts; + + global $rpc_mh; + global $rpc_chs; + + $ch = curl_init($url); + if (!$ch || !is_resource($ch)) { + internal_error_log("rpc", "couldn't curl_init '$ch' '$url': '".curl_error($ch)."'"); + curl_close($ch); + return null; + } + + $optstr = print_r($curlopts, TRUE); + // quick_log_to( "/www/perhost/curls.log", "curl: '$ch' URL: $url\n$optstr\n"); + + foreach ($curlopts as $opt=>$value) { + if (curl_setopt($ch, $opt, $value) === false) { + internal_error_log("rpc", "couldn't curl_setopt '$ch' '$opt' '$value': '".curl_error($ch)."'"); + curl_close($ch); + return null; + } + } + + if (!isset($rpc_mh)) { + rpc_multi_init(); + } + + $ret = curl_multi_add_handle($rpc_mh, $ch); + + if ($ret != 0) { + internal_error_log("rpc", "couldn't add curl handle $ch $ret: ".curl_error($ch)); + return null; + } + + $rpc_chs[] = $ch; + + return $ch; +} + +// returns a request ID, or null if it failed +function rpc_start_request($url, $post, $cookies, $internal) +{ + global $rpc_internal_timeout, $rpc_external_timeout; + + $cookiestr = ''; + if ($cookies) { + $carray = array(); + foreach($cookies as $name=>$value) { + $name = urlencode($name); + $value = urlencode($value); + $carray[] = "$name=$value"; + } + $cookiestr = implode("; ", $carray).";"; + } + + if ($internal) { + $curlopts[CURLOPT_SSL_VERIFYHOST] = false; + $curlopts[CURLOPT_SSL_VERIFYPEER] = false; + } + else { + $curlopts[CURLINFO_HEADER_OUT] = true; + // $curlopts[CURLOPT_VERBOSE] = true; + } + + if ($post) { + $curlopts[CURLOPT_POSTFIELDS] = $post; + } + + if ($cookiestr) { + $curlopts[CURLOPT_COOKIE] = $cookiestr; + } + + return rpc_start_request_with_options($url, $curlopts); +} + +function rpc_start_captcha_request($url, $post, $cookies, $internal) { + global $rpc_internal_timeout, $rpc_external_timeout; + + $cookiestr = ''; + if ($cookies) { + $carray = array(); + foreach($cookies as $name=>$value) { + $name = urlencode($name); + $value = urlencode($value); + $carray[] = "$name=$value"; + } + $cookiestr = implode("; ", $carray).";"; + } + + if ($internal) { + $curlopts[CURLOPT_SSL_VERIFYHOST] = false; + $curlopts[CURLOPT_SSL_VERIFYPEER] = false; + } + else { + $curlopts[CURLINFO_HEADER_OUT] = true; + //$curlopts[CURLOPT_RESOLVE] = array('www.google.com:443:172.217.4.132'); + // $curlopts[CURLOPT_VERBOSE] = true; + } + + if ($post) { + $curlopts[CURLOPT_POSTFIELDS] = $post; + } + + if ($cookiestr) { + $curlopts[CURLOPT_COOKIE] = $cookiestr; + } + + return rpc_start_request_with_options($url, $curlopts); +} + +function rpc_new_multi_handle() +{ + $mh = curl_multi_init(); + + curl_multi_setopt($mh, CURLMOPT_PIPELINING, 1); + curl_multi_setopt($mh, CURLMOPT_MAXCONNECTS, 16); + + return $mh; +} + +function rpc_multi_init() +{ + global $rpc_mh; + global $rpc_chs; + + $rpc_mh = rpc_new_multi_handle(); + $rpc_chs = array(); + + register_shutdown_function('rpc_finish_all'); +} + +// call at idle points, calls curl's task until it stops having immediate work +function rpc_task() +{ + global $rpc_mh; + + if (!is_resource($rpc_mh)) return false; + + $still_running = false; + + do { + $ret = curl_multi_exec($rpc_mh, $still_running); + } while ($ret == CURLM_CALL_MULTI_PERFORM); + + return $still_running; +} + +// blocks till all requests are no longer 'running' and clears rpc_mh +// this can block for a few seconds, watch out! +function rpc_finish_all() +{ + global $rpc_mh; + global $rpc_chs; + + if (!is_resource($rpc_mh) || !count($rpc_chs)) return; + + flush_output_buffers(); + + do { + if (rpc_task() == false) break; + curl_multi_select($rpc_mh); + } while (true); + + // clear out the curl_multi handle + foreach ($rpc_chs as $ch) { + curl_multi_remove_handle($rpc_mh, $ch); + } + + //quick_log_to("/www/perhost/rpc.log", getmypid()." $n rpcs finished in $rpc_mh\n"); + + //deallocate all curl handles + $rpc_chs = array(); + + //hopefully rpc_mh is empty now. + //we don't want to close it because curl uses the state for http pipelining +} + +function rpc_close_multi() +{ + global $rpc_mh; + global $rpc_chs; + + // explicitly close these objects since curl debug seems to not print otherwise? + // don't wait for them to finish. this can be used to prevent double-submits. maybe... + unset($rpc_chs); + unset($rpc_mh); +} + +function rpc_debug_fd() +{ + static $fd = -1; + + if ($fd == -1) { + $fd = fopen( "/www/perhost/curl-debug.log", "a" ); + fwrite($fd, "--------------\n"); + } + + return $fd; +} + +function rpc_debug_request($ch) +{ + $errno = curl_errno($ch); + $error = curl_error($ch); + $sent = curl_getinfo($ch, CURLINFO_HEADER_OUT); + $bsent = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD); + $brec = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD); + + quick_log_to("/www/perhost/rpc-failures.log", getmypid()." curl error $errno '$error'\nbytes up $bsent down $brec\ndata: $sent"); +} + +// returns the response, or sets $error if null +// DANGER: if you call this on a curl after rpc_finish_all() it seems to send the POST over again +function rpc_finish_request($ch, &$error, &$httperror = null) +{ + rpc_task(); + + // Move the request into the foreground and block (hopefully not actually blocking) + global $rpc_mh; + global $rpc_chs; + + if ($rpc_mh===null || !is_resource($ch)) { + $error = "Connections not started ($rpc_mh $ch)"; + return null; + } + + curl_multi_remove_handle($rpc_mh, $ch); + $ret = curl_exec($ch); + + // Get contents + if ($ret === false) { + $errstr = curl_error($ch); + $errno = curl_errno($ch); + $error = "Curl error: $errstr ($errno)"; + if ($httperror !== null) { + $httperror = curl_getinfo($ch, CURLINFO_HTTP_CODE); + } + rpc_debug_request($ch); + $ret = null; + } + + curl_close($ch); + + if (($pos = array_search($ch, $rpc_chs, true)) !== FALSE) { + unset($rpc_chs[$pos]); + } + + return $ret; +} + +// some dumb shit for sending HTTP POST to another server. +// only use this function internally +function rpc_send_request($host, $url, $request, &$error, $internal=true) { + $port = 80; + $proto = 'tcp://'; + $internal_network = preg_match( '#\.int$#', $host ) || strpos( $host, '10.0' ) === 0; + + if(strpos($host, "4chan.org") !== false || $internal_network ) { + if( strpos( $url, 'imgboard.php' ) !== false || strpos( $url, 'admin.php' ) !== false || strpos( $host, 'www.' ) !== false || $internal_network ) { + $proto = 'ssl://'; + $port = 443; + } + } + + $timeout = $internal_network ? 60 : 4; + + $cookie = ''; + foreach($request['COOKIE'] as $name=>$value) { + $name = urlencode($name); + $value = urlencode($value); + $cookie .= "$name=$value;"; + } + + $postbody = ''; + foreach($request['POST'] as $name=>$value) { + $name = urlencode($name); + $value = urlencode($value); + $postbody .= "$name=$value&"; + } + + // POSTing with HTTP 1.1 tends to make responders send + // back Transfer-encoding: chunked, so use 1.0 + + $header = "POST $url HTTP/1.0\r\n"; + $header .= "Host: $host\r\n"; + $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $header .= "Content-Length: ". strlen($postbody) . "\r\n"; + $header .= "User-Agent: 4chan.org\r\n"; + if ($cookie && $internal) $header .= "Cookie: $cookie\r\n"; + $header .= "Connection: close\r\n"; + $header .= "\r\n"; + + $header .= "$postbody\r\n"; + + $rpc_start_time = microtime(true); + $socket = fsockopen($proto.$host, $port, $errno, $errstr, $timeout); + if(!$socket) { + $error = $errstr; return; + } + + if(fwrite($socket, $header) != strlen($header)) { + $error = 'Could not write to socket'; return; + } + + $rpc_connect_time = microtime(true); + $rpc_connect_took = $rpc_connect_time - $rpc_start_time; + stream_set_timeout($socket, $timeout - $rpc_connect_took); + + $response = ''; + do { + $response .= fgets($socket, 1160); + $info = stream_get_meta_data($socket); + } while(!feof($socket) && !$info['timed_out']); + + fclose($socket); + if(!preg_match('!^HTTP/1\.. 200 OK!', $response)) { + $lines = explode("\n", $response); + $error = 'Error response from server ('.strlen($response).' bytes): '. $lines[0]; + $response = null; + } + + // $rpc_end_time = microtime(true); + // $rpc_took = $rpc_end_time - $rpc_start_time; + + /* + if ($error) { + quick_log_to("/www/perhost/rpc-slow.log", "ERROR: $host$url ct $rpc_connect_took took $rpc_took error: ".implode("\n",$lines)."\n".$postbody); + } else + if ($rpc_took > 1) { + quick_log_to("/www/perhost/rpc-slow.log", "SLOW: $host$url ct $rpc_connect_took took $rpc_took errored ".($response?0:1)."\n".$postbody); + } + */ + + return $response; +} + +// a shortcut to create the request object (with cookie set and POST initialized) +function rpc_blank_request() { + return array('COOKIE' => $_COOKIE, 'POST' => array() ); +} + +function rpc_log_url($s,$r) { + $s = nl2br($s); + $r = nl2br($r); + $rh = fopen("/www/perhost/rpc.log", "a"); + flock($rh, LOCK_EX); + fwrite($rh, "$s --> $r\n"); + fclose($rh); +} + +// TODO: This isn't really used since we just block link shorteners instead. +// But it should be optimized into curl_multi if possible. +function rpc_find_real_url($short) { + $cu = curl_init($short); + curl_setopt($cu, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($cu, CURLOPT_MAXREDIRS, 4); + curl_setopt($cu, CURLOPT_NOBODY, true); + curl_setopt($cu, CURLOPT_TIMEOUT, 10); + curl_setopt($cu, CURLOPT_USERAGENT, "4chan.org"); + + if (curl_exec($cu)) { + $ret = curl_getinfo($cu, CURLINFO_EFFECTIVE_URL); + } else $ret = FALSE; + + curl_close($cu); + //rpc_log_url($short,$ret); + return $ret; +} diff --git a/lib/rss.php b/lib/rss.php new file mode 100644 index 0000000..8c438bd --- /dev/null +++ b/lib/rss.php @@ -0,0 +1,72 @@ +}',"\n",$com); + // take the first sentence that's longer than 6 letters... + $sentences = preg_split('{[\n.]+}',$com); + $com = ''; + foreach($sentences as $sent) { + if(strlen($sent) > 6) { + $com = $sent; + break; + } + } + // and get the first 60 chars of it, making sure that words don't get cut off + $com = preg_replace('{^(.{60,}?)(?:[\s\n.]|$).*}','$1',$com); + if(strlen($com) >= 60) $com .= '...'; + return $com; +} +function rss_dump() { + global $log; + $title = htmlspecialchars(TITLE); + $link = "http:" . SELF_PATH2_ABS; + $self = "http:" . DATA_SERVER . BOARD_DIR . '/index.rss'; + $output = "\n\n"; + $output .= "\n$title\n$link\nThreads on $title at 4chan.org.\n"; + $output .= ""; + $query = mysql_board_call("SELECT SQL_NO_CACHE * FROM `".SQLLOG."` WHERE archived=0 and resto=0 ORDER BY no DESC LIMIT 20"); + while($row = mysql_fetch_assoc($query)) { + $output .= "\n"; + $spacing = ''; + $sub = $row['sub']; + + $spoiler = 0; + if(strpos($sub, "SPOILER<>")===0) { + $sub = str_replace("SPOILER<>","",$sub); + $spoiler = 1; + } + if(!$sub) + $sub = strip_tags(summarize($row['com'])); + if(!$sub && $row['name']) { // if they have a name + if(FORCED_ANON!=1) $by = " by ". strip_tags($row['name']); // forced anon doesn't need byline + $sub = "No. {$row['no']}$by"; + } + $link = "http:" . DATA_SERVER . BOARD_DIR . '/' . RES_DIR2 . $row['no'] . PHP_EXT2; + + if(strpos($row['com'], "[spoiler")!==false) + $row['com'] = '(Spoilers)'; + $date = strftime("%a, %d %b %Y %X %Z", $row['time']); + if($sub) // don't include title if empty + $output .= "$spacing$sub\n"; + $output .= "$spacing$link#{$row['no']}\n"; + $output .= "$spacing$link\n"; + $output .= "$spacing$link\n"; + $output .= "$spacing$date\n"; + $srcurl = "http:" . IMG_DIR2 . $row['tim'] . $row['ext']; + $thumburl = "http:" . THUMB_DIR2 . $row['tim'] . 's.jpg'; + if($spoiler) { + $thumburl = "http:" . DATA_SERVER . 'spoiler.png'; + } + $imglink = ""; + + if(FORCED_ANON!=1) + // $output .= "$spacing".strip_tags($row['name'])."\n"; + $output .= "$spacing".strip_tags($row['name'])."\n"; + $output .= "$spacing\n"; + $output .= "\n"; + } + $output .= "\n"; + print_page(INDEX_DIR.'index.rss',$output); +} diff --git a/lib/userpwd-test.php b/lib/userpwd-test.php new file mode 100644 index 0000000..7473377 --- /dev/null +++ b/lib/userpwd-test.php @@ -0,0 +1,956 @@ + self::MAX_B64_SIZE) { + return null; + } + + $bin_data = self::b64_decode($b64_data); + + if (!$bin_data) { + return null; + } + + $version = unpack('C', $bin_data)[1]; + + if (strlen($bin_data) < self::PWD_SIZE + 1) { + return null; + } + + $nonce = substr($bin_data, 1, self::NONCE_SIZE); + $bin_data = substr($bin_data, 1 + self::NONCE_SIZE); + $bin_data = self::decrypt($bin_data, $nonce); + $pwd = substr($bin_data, 0, self::PWD_SIZE); + + if (!$pwd || strlen($pwd) !== self::PWD_SIZE) { + return false; + } + + return bin2hex($pwd); + } + + public function version() { + return $this->version; + } + + public static function getSession() { + return self::$session_instance; + } + + public static function clearSession() { + self::$session_instance = null; + } + + private static function b64_encode($data) { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + private static function b64_decode($data) { + return base64_decode(strtr($data, '-_', '+/')); + } + + // $b64_data is the url-safe version, see b64_encode method. + public function __construct($ip, $domain, $b64_data = null, $start_session = true) { + $this->now = time(); + + if ($start_session) { + self::$session_instance = $this; + } + + $this->ip = $ip; + $this->domain = $domain; + + $this->env_data = $this->collect_env_data(); + + if ($b64_data === null) { + $this->generate(); + return; + } + + if (strlen($b64_data) > self::MAX_B64_SIZE) { + $this->errno = self::E_CORRUPT_LEN; + $this->generate(); + return; + } + + $bin_data = self::b64_decode($b64_data); + + if (!$bin_data) { + $this->errno = self::E_CORRUPT_DEC; + $this->generate(); + return; + } + + $version = unpack('C', $bin_data)[1]; + + $this->version = $version; + + // Version check + if ($version > self::VERSION || $version < self::VERSION_MIN) { + $this->errno = self::E_VERSION; + $this->generate(); + return; + } + + $nonce = substr($bin_data, 1, self::NONCE_SIZE); + $bin_data = substr($bin_data, 1 + self::NONCE_SIZE); + $bin_data = self::decrypt($bin_data, $nonce); + + if (!$bin_data) { + $this->errno = self::E_ENC; + $this->generate(); + return; + } + + // FIXME: Version 2 + if ($version === 2) { + $_data_size = self::DATA_SIZE - 1 - 4 - 1; + + $full_size = self::PWD_SIZE + $_data_size + (self::SIG_SIZE * (self::SIG_COUNT - 1)); + + if (strlen($bin_data) !== $full_size) { + $this->errno = self::E_CORRUPT_LEN2; + $this->generate(); + return; + } + + $pwd_raw = substr($bin_data, 0, self::PWD_SIZE); + + list($creation_ts, $mask_ts, $ip_ts, $activity_ts, $action_ts, + $post_count, $img_count, $thread_count, $report_count, $ip_change_score) + = array_values(unpack('V5t/C5a', substr($bin_data, self::PWD_SIZE, $_data_size))); + + $env_ts = $this->now; + $verified_level = 0; + $action_buffer = 0; + + $sig_start = self::PWD_SIZE + $_data_size; + + $pwd_sig = substr($bin_data, $sig_start, self::SIG_SIZE); + $mask_sig = substr($bin_data, $sig_start + self::SIG_SIZE, self::SIG_SIZE); + $ip_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 2, self::SIG_SIZE); + $env_sig = null; + + // Password signature + $valid_pwd_sig = $this->calc_sig( + [ + $pwd_raw, $creation_ts, $activity_ts, $action_ts, + $post_count, $img_count, $thread_count, $report_count, $ip_change_score, + $domain + ] + ); + } + // Current version + else { + $full_size = self::PWD_SIZE + self::DATA_SIZE + (self::SIG_SIZE * self::SIG_COUNT); + + if (strlen($bin_data) !== $full_size) { + $this->errno = self::E_CORRUPT_LEN; + $this->generate(); + return; + } + + $pwd_raw = substr($bin_data, 0, self::PWD_SIZE); + list($creation_ts, $mask_ts, $ip_ts, $activity_ts, $action_ts, $env_ts, $verified_level, + $post_count, $img_count, $thread_count, $report_count, $action_buffer, $ip_change_score) + = array_values(unpack('V6t/C7a', substr($bin_data, self::PWD_SIZE, self::DATA_SIZE))); + + $sig_start = self::PWD_SIZE + self::DATA_SIZE; + + $pwd_sig = substr($bin_data, $sig_start, self::SIG_SIZE); + $mask_sig = substr($bin_data, $sig_start + self::SIG_SIZE, self::SIG_SIZE); + $ip_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 2, self::SIG_SIZE); + $env_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 3, self::SIG_SIZE); + + // Password signature + $valid_pwd_sig = $this->calc_sig( + [ + $pwd_raw, $creation_ts, $activity_ts, $action_ts, $env_ts, $verified_level, + $post_count, $img_count, $thread_count, $report_count, $action_buffer, $ip_change_score, + $domain + ] + ); + } + + if ($valid_pwd_sig && $valid_pwd_sig === $pwd_sig) { + $this->pwd_raw = $pwd_raw; + + $this->pwd_hex = bin2hex($pwd_raw); + + if ($activity_ts > 0) { + $_act_ts = $activity_ts; + } + else { + $_act_ts = $creation_ts; + } + + if ($this->now - $_act_ts >= self::TTL) { + $this->errno = self::E_EXPIRED; + $this->resetTimestamps(); + return; + } + else { + $this->creation_ts = $creation_ts; + $this->activity_ts = $activity_ts; + $this->action_ts = $action_ts; + $this->env_ts = $env_ts; + + // FIXME: Version 2 + if ($version !== 2) { + $this->pwd_sig = $valid_pwd_sig; + } + + $this->verified_level = $verified_level; + + $this->post_count = $post_count; + $this->img_count = $img_count; + $this->thread_count = $thread_count; + $this->report_count = $report_count; + $this->action_buffer = $action_buffer; + + $this->ip_change_score = $ip_change_score; + } + } + else { + $this->errno = self::E_PWDSIG; + $this->generate(); + return; + } + + // Environment signature + $valid_env_sig = $this->calc_sig([ $pwd_raw, $env_ts, $this->env_data, $domain ]); + + if ($valid_env_sig && $valid_env_sig === $env_sig) { + $this->env_ts = $env_ts; + $this->env_sig = $valid_env_sig; + } + else { + $this->errno = self::E_ENVSIG; + $this->env_ts = $this->now; + $this->pwd_sig = null; // FIXME, env_ts shouldn't be used in the pwd_sig + } + + // Masked IP signature + $valid_mask_sig = $this->calc_sig([ $pwd_raw, $mask_ts, $this->get_ip_mask($ip), $domain ]); + + if ($valid_mask_sig && $valid_mask_sig === $mask_sig) { + $this->mask_ts = $mask_ts; + $this->mask_sig = $valid_mask_sig; + } + else { + $this->mask_ts = $this->now; + $this->ip_ts = $this->now; + + $this->errno = self::E_MASKSIG; + + return; // bail out + } + + // IP signature + $valid_ip_sig = $this->calc_sig([ $pwd_raw, $ip_ts, $ip, $domain ]); + + if ($valid_ip_sig && $valid_ip_sig === $ip_sig) { + $this->ip_ts = $ip_ts; + $this->ip_sig = $valid_ip_sig; + } + else { + $this->errno = self::E_IPSIG; + $this->ip_ts = $this->now; + } + } + + private function get_ip_mask($ip) { + $ip_parts = explode('.', $ip, 3); + return "{$ip_parts[0]}.{$ip_parts[1]}"; + } + + private function collect_env_data() { + if (!isset($_SERVER)) { + return 'noenv'; + } + + // Country + if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { + $data = $_SERVER['HTTP_X_GEO_COUNTRY']; + } + else { + $data = 'XX'; + } + + return $data; + } + + private function calc_sig($arg_array) { + return substr(hash_hmac('sha1', implode(' ', $arg_array), UserPwd::HMAC_SECRET, true), 0, self::SIG_SIZE); + } + + public function getPwd() { + return $this->pwd_hex; + } + + public function pwdLifetime() { + if ($this->creation_ts) { + return $this->now - $this->creation_ts; + } + else { + return 0; + } + } + + public function maskLifetime() { + if ($this->mask_ts) { + return $this->now - $this->mask_ts; + } + else { + return 0; + } + } + + public function ipLifetime() { + if ($this->ip_ts) { + return $this->now - $this->ip_ts; + } + else { + return 0; + } + } + + public function envLifetime() { + if ($this->env_ts) { + return $this->now - $this->env_ts; + } + else { + return 0; + } + } + + public function creationTs() { + return $this->creation_ts; + } + + public function ipTs() { + return $this->ip_ts; + } + + public function maskTs() { + return $this->mask_ts; + } + + public function idleLifetime() { + if ($this->activity_ts) { + return $this->now - $this->activity_ts; + } + else { + return $this->creation_ts; + } + } + + public function lastActionLifetime() { + if ($this->action_ts) { + return $this->now - $this->action_ts; + } + else { + return 0; + } + } + + public function verifiedLevel() { + return $this->verified_level; + } + + public function maskChanged() { + return !$this->isNew() && $this->mask_ts === $this->now; + } + + public function ipChanged() { + return !$this->isNew() && $this->ip_ts === $this->now; + } + + public function envChanged() { + return !$this->isNew() && $this->env_ts === $this->now; + } + + public function isUserKnown($for_minutes = 1440, $since_ts = 0) { + // If the IP changes too often, enforce an IP lifetime of IP_CHANGE_DELAY + if ($this->ipChangeScore() > self::IP_CHANGE_MASK_VAL * 3) { + if ($this->maskLifetime() < self::IP_CHANGE_DELAY) { + return false; + } + } + + // Mask is older than the required lifetime + if ($this->maskLifetime() >= $for_minutes * 60) { + return true; + } + + // Mask was created before the reference time + // ex: user was already posting when a new lenient rangeban was created + if ($since_ts > 0 && $this->mask_ts <= $since_ts) { + if ($this->postCount() > 0 || $this->reportCount() > 5) { + return true; + } + } + + // Password isn't old enough + if ($this->pwdLifetime() < $for_minutes * 60) { + return false; + } + + // Password is old enough + + // For lenient rangebans, this is enough + if ($since_ts > 0) { + return true; + } + + // Otherwise, do some more checks + + // User has enough activity + if ($this->postCount() >= 3 || $this->reportCount() >= 10) { + // Check UA + country + //if ($this->envLifetime() >= self::IP_CHANGE_DELAY) { + // return true; + //} + // Check the mask lifetime + if ($this->maskLifetime() >= self::IP_CHANGE_DELAY) { + return true; + } + // Otherwise do a more strict activity check + if ($this->postCount() >= 9 || $this->reportCount() >= 20) { + return true; + } + } + + // All checks failed + return false; + } + + public function isUserKnownOrVerified($for_minutes = 1440, $since_ts = 0) { + if ($this->verifiedLevel()) { + return true; + } + + return $this->isUserKnown($for_minutes, $since_ts); + } + + public function updatePostActivity($is_thread, $has_file, $is_dummy = false) { + $actions = self::A_POST; + + if ($is_thread) { + $actions = $actions | self::A_THREAD; + } + + if ($has_file) { + $actions = $actions | self::A_IMG; + } + + $this->updateActivity($actions, $is_dummy); + } + + public function updateReportActivity($is_dummy = false) { + $this->updateActivity(self::A_REPORT, $is_dummy); + } + + public function updateActivity($kind, $is_dummy = false) { + $this->action_buffer = $this->action_buffer | $kind; + + $ip_change_delta = -1; + + if ($this->idleLifetime() < self::IP_CHANGE_DELAY) { + if ($this->maskChanged()) { + $ip_change_delta = self::IP_CHANGE_MASK_VAL; + } + else if ($this->ipChanged()) { + $ip_change_delta = self::IP_CHANGE_IP_VAL; + } + } + + $this->ip_change_score = min(max(0, $this->ip_change_score + $ip_change_delta), self::IP_CHANGE_SCORE_MAX); + + if ($this->ip_change_score >= self::IP_CHANGE_SCORE_MAX) { + $this->resetActionCounts(); + } + + if ($this->action_ts === 0) { + $this->action_ts = $this->now; + } + else if (!$is_dummy && $this->lastActionLifetime() >= self::ACTION_DELAY) { + if ($this->action_buffer & self::A_REPORT) { + $this->report_count = min($this->report_count + 1, 0xFF); + } + + if ($this->action_buffer & self::A_POST) { + $this->post_count = min($this->post_count + 1, 0xFF); + } + + if ($this->action_buffer & self::A_IMG) { + $this->img_count = min($this->img_count + 1, 0xFF); + } + + if ($this->action_buffer & self::A_THREAD) { + $this->thread_count = min($this->thread_count + 1, 0xFF); + } + + $this->action_buffer = 0; + + $this->action_ts = $this->now; + } + + $this->activity_ts = $this->now; + + $this->pwd_sig = null; + } + + public function postCount() { + return $this->post_count + ($this->action_buffer & self::A_POST ? 1 : 0); + } + + public function imgCount() { + return $this->img_count + ($this->action_buffer & self::A_IMG ? 1 : 0); + } + + public function threadCount() { + return $this->thread_count + ($this->action_buffer & self::A_THREAD ? 1 : 0); + } + + public function reportCount() { + return $this->report_count + ($this->action_buffer & self::A_REPORT ? 1 : 0); + } + + public function ipChangeScore() { + return $this->ip_change_score; + } + + // Never used + public function isNeverUsed() { + return $this->activity_ts === 0; + } + + // Used only once + public function isUsedOnlyOnce() { + return $this->activity_ts === $this->creation_ts; + } + + // Just created + public function isNew() { + return $this->creation_ts === $this->now; + } + + // Fake or spoofed + public function isFake() { + return $this->errno === self::E_PWDSIG; + } + + public function getEncodedData() { + if (!$this->domain || !$this->ip) { + return false; + } + + $data = []; + + // Raw password + if ($this->pwd_raw) { + $data[] = $this->pwd_raw; + } + else { + return false; + } + + // Creation timestamp + if ($this->creation_ts > 0) { + $data[] = pack('V', $this->creation_ts); + } + else { + return false; + } + + // Mask timestamp + if ($this->mask_ts > 0) { + $data[] = pack('V', $this->mask_ts); + } + else { + return false; + } + + // IP timestamp + if ($this->ip_ts > 0) { + $data[] = pack('V', $this->ip_ts); + } + else { + return false; + } + + // Last ativity timestamp + if ($this->activity_ts < 0) { + return false; + } + + $data[] = pack('V', $this->activity_ts); + + // Last action increment timestamp + if ($this->action_ts < 0) { + return false; + } + + $data[] = pack('V', $this->action_ts); + + // Env timestamp + if ($this->env_ts > 0) { + $data[] = pack('V', $this->env_ts); + } + else { + return false; + } + + // Verified level + if ($this->verified_level < 0) { + return false; + } + + $data[] = pack('C', $this->verified_level); + + // Action counts + $data[] = pack('C5', $this->post_count, $this->img_count, $this->thread_count, $this->report_count, $this->action_buffer); + + // IP change score + $data[] = pack('C', $this->ip_change_score); + + // Password signature + if ($this->pwd_sig) { + $data[] = $this->pwd_sig; + } + else { + $data[] = $this->calc_sig([ + $this->pwd_raw, $this->creation_ts, $this->activity_ts, $this->action_ts, $this->env_ts, $this->verified_level, + $this->post_count, $this->img_count, $this->thread_count, $this->report_count, $this->action_buffer, $this->ip_change_score, + $this->domain + ]); + } + + // Mask signature + if ($this->mask_sig) { + $data[] = $this->mask_sig; + } + else { + $data[] = $this->calc_sig([ $this->pwd_raw, $this->mask_ts, $this->get_ip_mask($this->ip), $this->domain ]); + } + + // IP signature + if ($this->ip_sig) { + $data[] = $this->ip_sig; + } + else { + $data[] = $this->calc_sig([ $this->pwd_raw, $this->ip_ts, $this->ip, $this->domain ]); + } + + // Env signature + if ($this->env_sig) { + $data[] = $this->env_sig; + } + else { + $data[] = $this->calc_sig([ $this->pwd_raw, $this->env_ts, $this->env_data, $this->domain ]); + } + + // --- + + $data = implode('', $data); + + list($data, $nonce) = self::encrypt($data); + + if (!$data) { + return false; + } + + // Version + Nonce + $data = pack('C', self::VERSION) . $nonce . $data; + + return self::b64_encode($data); + } + + private static function encrypt($data) { + $data_len = strlen($data); + + $key = hex2bin(self::XOR_KEY); + $nonce = openssl_random_pseudo_bytes(self::NONCE_SIZE); + + if (!$data_len || !$nonce || $data_len > strlen($key)) { + return false; + } + + $output_nonced = ''; + + // Apply nonce + $ni = 0; + + for ($di = 0; $di < $data_len; ++$di) { + if ($ni >= self::NONCE_SIZE) { + $ni = 0; + } + + $output_nonced = $output_nonced . ($data[$di] ^ $nonce[$ni]); + + $ni++; + } + + $output = ''; + + // XOR Encrypt + for ($i = 0; $i < $data_len; ++$i) { + $output = $output . ($output_nonced[$i] ^ $key[$i]); + } + + return [ $output, $nonce ]; + } + + private static function decrypt($data, $nonce) { + $data_len = strlen($data); + + $nonce_len = strlen($nonce); + + $key = hex2bin(self::XOR_KEY); + + if (!$data_len || !$nonce || $data_len > strlen($key)) { + return false; + } + + $output_nonced = ''; + + // XOR Decrypt + for ($i = 0; $i < $data_len; ++$i) { + $output_nonced = $output_nonced . ($data[$i] ^ $key[$i]); + } + + // Apply nonce + $output = ''; + + $ni = 0; + + for ($di = 0; $di < $data_len; ++$di) { + if ($ni >= $nonce_len) { + $ni = 0; + } + + $output = $output . ($output_nonced[$di] ^ $nonce[$ni]); + + $ni++; + } + + return $output; + } + + private function generate() { + if (!$this->ip || !$this->domain) { + return false; + } + + $pwd_raw = openssl_random_pseudo_bytes(self::PWD_SIZE); + + if (!$pwd_raw) { + return false; + } + + $this->version = self::VERSION; + + $this->pwd_raw = $pwd_raw; + $this->pwd_hex = bin2hex($pwd_raw); + $this->creation_ts = $this->now; + $this->mask_ts = $this->now; + $this->ip_ts = $this->now; + $this->env_ts = $this->now; + + return true; + } + + public function setPwd($pwd_hex) { + if (!$pwd_hex) { + return false; + } + + $pwd_raw = hex2bin($pwd_hex); + + if (!$pwd_raw || strlen($pwd_raw) !== self::PWD_SIZE) { + return false; + } + + $this->pwd_raw = $pwd_raw; + $this->pwd_hex = $pwd_hex; + + $this->resetSignatures(); + + return true; + } + + public function setVerifiedLevel($level) { + if ($level < 0) { + return false; + } + $this->verified_level = $level; + $this->pwd_sig = null; + } + + private function resetTimestamps() { + $this->creation_ts = $this->now; + $this->mask_ts = $this->now; + $this->ip_ts = $this->now; + $this->action_ts = $this->now; + $this->activity_ts = 0; + $this->env_ts = $this->now; + } + + private function resetActionCounts() { + $this->post_count = 0; + $this->img_count = 0; + $this->thread_count = 0; + $this->report_count = 0; + + $this->action_buffer = 0; + } + + private function resetSignatures() { + $this->pwd_sig = null; + $this->mask_sig = null; + $this->ip_sig = null; + $this->env_sig = null; + } + + public function setCookie($domain) { + $data = $this->getEncodedData(); + + if ($data) { + return setcookie(self::COOKIE_NAME, $data, $this->now + self::COOKIE_TTL, '/', $domain, true, true); + } + else { + return false; + } + } + + public static function setFakeCookie($now, $domain) { + $size = self::NONCE_SIZE + self::PWD_SIZE + self::DATA_SIZE + self::SIG_SIZE * self::SIG_COUNT; + + $data = openssl_random_pseudo_bytes($size); + + if (!$data) { + return false; + } + + $data = pack('C', self::VERSION) . $data; + + $data = self::b64_encode($data); + + return setcookie(self::COOKIE_NAME, $data, $now + self::COOKIE_TTL, '/', $domain, true); + } +} diff --git a/lib/userpwd.php b/lib/userpwd.php new file mode 100644 index 0000000..7473377 --- /dev/null +++ b/lib/userpwd.php @@ -0,0 +1,956 @@ + self::MAX_B64_SIZE) { + return null; + } + + $bin_data = self::b64_decode($b64_data); + + if (!$bin_data) { + return null; + } + + $version = unpack('C', $bin_data)[1]; + + if (strlen($bin_data) < self::PWD_SIZE + 1) { + return null; + } + + $nonce = substr($bin_data, 1, self::NONCE_SIZE); + $bin_data = substr($bin_data, 1 + self::NONCE_SIZE); + $bin_data = self::decrypt($bin_data, $nonce); + $pwd = substr($bin_data, 0, self::PWD_SIZE); + + if (!$pwd || strlen($pwd) !== self::PWD_SIZE) { + return false; + } + + return bin2hex($pwd); + } + + public function version() { + return $this->version; + } + + public static function getSession() { + return self::$session_instance; + } + + public static function clearSession() { + self::$session_instance = null; + } + + private static function b64_encode($data) { + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } + + private static function b64_decode($data) { + return base64_decode(strtr($data, '-_', '+/')); + } + + // $b64_data is the url-safe version, see b64_encode method. + public function __construct($ip, $domain, $b64_data = null, $start_session = true) { + $this->now = time(); + + if ($start_session) { + self::$session_instance = $this; + } + + $this->ip = $ip; + $this->domain = $domain; + + $this->env_data = $this->collect_env_data(); + + if ($b64_data === null) { + $this->generate(); + return; + } + + if (strlen($b64_data) > self::MAX_B64_SIZE) { + $this->errno = self::E_CORRUPT_LEN; + $this->generate(); + return; + } + + $bin_data = self::b64_decode($b64_data); + + if (!$bin_data) { + $this->errno = self::E_CORRUPT_DEC; + $this->generate(); + return; + } + + $version = unpack('C', $bin_data)[1]; + + $this->version = $version; + + // Version check + if ($version > self::VERSION || $version < self::VERSION_MIN) { + $this->errno = self::E_VERSION; + $this->generate(); + return; + } + + $nonce = substr($bin_data, 1, self::NONCE_SIZE); + $bin_data = substr($bin_data, 1 + self::NONCE_SIZE); + $bin_data = self::decrypt($bin_data, $nonce); + + if (!$bin_data) { + $this->errno = self::E_ENC; + $this->generate(); + return; + } + + // FIXME: Version 2 + if ($version === 2) { + $_data_size = self::DATA_SIZE - 1 - 4 - 1; + + $full_size = self::PWD_SIZE + $_data_size + (self::SIG_SIZE * (self::SIG_COUNT - 1)); + + if (strlen($bin_data) !== $full_size) { + $this->errno = self::E_CORRUPT_LEN2; + $this->generate(); + return; + } + + $pwd_raw = substr($bin_data, 0, self::PWD_SIZE); + + list($creation_ts, $mask_ts, $ip_ts, $activity_ts, $action_ts, + $post_count, $img_count, $thread_count, $report_count, $ip_change_score) + = array_values(unpack('V5t/C5a', substr($bin_data, self::PWD_SIZE, $_data_size))); + + $env_ts = $this->now; + $verified_level = 0; + $action_buffer = 0; + + $sig_start = self::PWD_SIZE + $_data_size; + + $pwd_sig = substr($bin_data, $sig_start, self::SIG_SIZE); + $mask_sig = substr($bin_data, $sig_start + self::SIG_SIZE, self::SIG_SIZE); + $ip_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 2, self::SIG_SIZE); + $env_sig = null; + + // Password signature + $valid_pwd_sig = $this->calc_sig( + [ + $pwd_raw, $creation_ts, $activity_ts, $action_ts, + $post_count, $img_count, $thread_count, $report_count, $ip_change_score, + $domain + ] + ); + } + // Current version + else { + $full_size = self::PWD_SIZE + self::DATA_SIZE + (self::SIG_SIZE * self::SIG_COUNT); + + if (strlen($bin_data) !== $full_size) { + $this->errno = self::E_CORRUPT_LEN; + $this->generate(); + return; + } + + $pwd_raw = substr($bin_data, 0, self::PWD_SIZE); + list($creation_ts, $mask_ts, $ip_ts, $activity_ts, $action_ts, $env_ts, $verified_level, + $post_count, $img_count, $thread_count, $report_count, $action_buffer, $ip_change_score) + = array_values(unpack('V6t/C7a', substr($bin_data, self::PWD_SIZE, self::DATA_SIZE))); + + $sig_start = self::PWD_SIZE + self::DATA_SIZE; + + $pwd_sig = substr($bin_data, $sig_start, self::SIG_SIZE); + $mask_sig = substr($bin_data, $sig_start + self::SIG_SIZE, self::SIG_SIZE); + $ip_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 2, self::SIG_SIZE); + $env_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 3, self::SIG_SIZE); + + // Password signature + $valid_pwd_sig = $this->calc_sig( + [ + $pwd_raw, $creation_ts, $activity_ts, $action_ts, $env_ts, $verified_level, + $post_count, $img_count, $thread_count, $report_count, $action_buffer, $ip_change_score, + $domain + ] + ); + } + + if ($valid_pwd_sig && $valid_pwd_sig === $pwd_sig) { + $this->pwd_raw = $pwd_raw; + + $this->pwd_hex = bin2hex($pwd_raw); + + if ($activity_ts > 0) { + $_act_ts = $activity_ts; + } + else { + $_act_ts = $creation_ts; + } + + if ($this->now - $_act_ts >= self::TTL) { + $this->errno = self::E_EXPIRED; + $this->resetTimestamps(); + return; + } + else { + $this->creation_ts = $creation_ts; + $this->activity_ts = $activity_ts; + $this->action_ts = $action_ts; + $this->env_ts = $env_ts; + + // FIXME: Version 2 + if ($version !== 2) { + $this->pwd_sig = $valid_pwd_sig; + } + + $this->verified_level = $verified_level; + + $this->post_count = $post_count; + $this->img_count = $img_count; + $this->thread_count = $thread_count; + $this->report_count = $report_count; + $this->action_buffer = $action_buffer; + + $this->ip_change_score = $ip_change_score; + } + } + else { + $this->errno = self::E_PWDSIG; + $this->generate(); + return; + } + + // Environment signature + $valid_env_sig = $this->calc_sig([ $pwd_raw, $env_ts, $this->env_data, $domain ]); + + if ($valid_env_sig && $valid_env_sig === $env_sig) { + $this->env_ts = $env_ts; + $this->env_sig = $valid_env_sig; + } + else { + $this->errno = self::E_ENVSIG; + $this->env_ts = $this->now; + $this->pwd_sig = null; // FIXME, env_ts shouldn't be used in the pwd_sig + } + + // Masked IP signature + $valid_mask_sig = $this->calc_sig([ $pwd_raw, $mask_ts, $this->get_ip_mask($ip), $domain ]); + + if ($valid_mask_sig && $valid_mask_sig === $mask_sig) { + $this->mask_ts = $mask_ts; + $this->mask_sig = $valid_mask_sig; + } + else { + $this->mask_ts = $this->now; + $this->ip_ts = $this->now; + + $this->errno = self::E_MASKSIG; + + return; // bail out + } + + // IP signature + $valid_ip_sig = $this->calc_sig([ $pwd_raw, $ip_ts, $ip, $domain ]); + + if ($valid_ip_sig && $valid_ip_sig === $ip_sig) { + $this->ip_ts = $ip_ts; + $this->ip_sig = $valid_ip_sig; + } + else { + $this->errno = self::E_IPSIG; + $this->ip_ts = $this->now; + } + } + + private function get_ip_mask($ip) { + $ip_parts = explode('.', $ip, 3); + return "{$ip_parts[0]}.{$ip_parts[1]}"; + } + + private function collect_env_data() { + if (!isset($_SERVER)) { + return 'noenv'; + } + + // Country + if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { + $data = $_SERVER['HTTP_X_GEO_COUNTRY']; + } + else { + $data = 'XX'; + } + + return $data; + } + + private function calc_sig($arg_array) { + return substr(hash_hmac('sha1', implode(' ', $arg_array), UserPwd::HMAC_SECRET, true), 0, self::SIG_SIZE); + } + + public function getPwd() { + return $this->pwd_hex; + } + + public function pwdLifetime() { + if ($this->creation_ts) { + return $this->now - $this->creation_ts; + } + else { + return 0; + } + } + + public function maskLifetime() { + if ($this->mask_ts) { + return $this->now - $this->mask_ts; + } + else { + return 0; + } + } + + public function ipLifetime() { + if ($this->ip_ts) { + return $this->now - $this->ip_ts; + } + else { + return 0; + } + } + + public function envLifetime() { + if ($this->env_ts) { + return $this->now - $this->env_ts; + } + else { + return 0; + } + } + + public function creationTs() { + return $this->creation_ts; + } + + public function ipTs() { + return $this->ip_ts; + } + + public function maskTs() { + return $this->mask_ts; + } + + public function idleLifetime() { + if ($this->activity_ts) { + return $this->now - $this->activity_ts; + } + else { + return $this->creation_ts; + } + } + + public function lastActionLifetime() { + if ($this->action_ts) { + return $this->now - $this->action_ts; + } + else { + return 0; + } + } + + public function verifiedLevel() { + return $this->verified_level; + } + + public function maskChanged() { + return !$this->isNew() && $this->mask_ts === $this->now; + } + + public function ipChanged() { + return !$this->isNew() && $this->ip_ts === $this->now; + } + + public function envChanged() { + return !$this->isNew() && $this->env_ts === $this->now; + } + + public function isUserKnown($for_minutes = 1440, $since_ts = 0) { + // If the IP changes too often, enforce an IP lifetime of IP_CHANGE_DELAY + if ($this->ipChangeScore() > self::IP_CHANGE_MASK_VAL * 3) { + if ($this->maskLifetime() < self::IP_CHANGE_DELAY) { + return false; + } + } + + // Mask is older than the required lifetime + if ($this->maskLifetime() >= $for_minutes * 60) { + return true; + } + + // Mask was created before the reference time + // ex: user was already posting when a new lenient rangeban was created + if ($since_ts > 0 && $this->mask_ts <= $since_ts) { + if ($this->postCount() > 0 || $this->reportCount() > 5) { + return true; + } + } + + // Password isn't old enough + if ($this->pwdLifetime() < $for_minutes * 60) { + return false; + } + + // Password is old enough + + // For lenient rangebans, this is enough + if ($since_ts > 0) { + return true; + } + + // Otherwise, do some more checks + + // User has enough activity + if ($this->postCount() >= 3 || $this->reportCount() >= 10) { + // Check UA + country + //if ($this->envLifetime() >= self::IP_CHANGE_DELAY) { + // return true; + //} + // Check the mask lifetime + if ($this->maskLifetime() >= self::IP_CHANGE_DELAY) { + return true; + } + // Otherwise do a more strict activity check + if ($this->postCount() >= 9 || $this->reportCount() >= 20) { + return true; + } + } + + // All checks failed + return false; + } + + public function isUserKnownOrVerified($for_minutes = 1440, $since_ts = 0) { + if ($this->verifiedLevel()) { + return true; + } + + return $this->isUserKnown($for_minutes, $since_ts); + } + + public function updatePostActivity($is_thread, $has_file, $is_dummy = false) { + $actions = self::A_POST; + + if ($is_thread) { + $actions = $actions | self::A_THREAD; + } + + if ($has_file) { + $actions = $actions | self::A_IMG; + } + + $this->updateActivity($actions, $is_dummy); + } + + public function updateReportActivity($is_dummy = false) { + $this->updateActivity(self::A_REPORT, $is_dummy); + } + + public function updateActivity($kind, $is_dummy = false) { + $this->action_buffer = $this->action_buffer | $kind; + + $ip_change_delta = -1; + + if ($this->idleLifetime() < self::IP_CHANGE_DELAY) { + if ($this->maskChanged()) { + $ip_change_delta = self::IP_CHANGE_MASK_VAL; + } + else if ($this->ipChanged()) { + $ip_change_delta = self::IP_CHANGE_IP_VAL; + } + } + + $this->ip_change_score = min(max(0, $this->ip_change_score + $ip_change_delta), self::IP_CHANGE_SCORE_MAX); + + if ($this->ip_change_score >= self::IP_CHANGE_SCORE_MAX) { + $this->resetActionCounts(); + } + + if ($this->action_ts === 0) { + $this->action_ts = $this->now; + } + else if (!$is_dummy && $this->lastActionLifetime() >= self::ACTION_DELAY) { + if ($this->action_buffer & self::A_REPORT) { + $this->report_count = min($this->report_count + 1, 0xFF); + } + + if ($this->action_buffer & self::A_POST) { + $this->post_count = min($this->post_count + 1, 0xFF); + } + + if ($this->action_buffer & self::A_IMG) { + $this->img_count = min($this->img_count + 1, 0xFF); + } + + if ($this->action_buffer & self::A_THREAD) { + $this->thread_count = min($this->thread_count + 1, 0xFF); + } + + $this->action_buffer = 0; + + $this->action_ts = $this->now; + } + + $this->activity_ts = $this->now; + + $this->pwd_sig = null; + } + + public function postCount() { + return $this->post_count + ($this->action_buffer & self::A_POST ? 1 : 0); + } + + public function imgCount() { + return $this->img_count + ($this->action_buffer & self::A_IMG ? 1 : 0); + } + + public function threadCount() { + return $this->thread_count + ($this->action_buffer & self::A_THREAD ? 1 : 0); + } + + public function reportCount() { + return $this->report_count + ($this->action_buffer & self::A_REPORT ? 1 : 0); + } + + public function ipChangeScore() { + return $this->ip_change_score; + } + + // Never used + public function isNeverUsed() { + return $this->activity_ts === 0; + } + + // Used only once + public function isUsedOnlyOnce() { + return $this->activity_ts === $this->creation_ts; + } + + // Just created + public function isNew() { + return $this->creation_ts === $this->now; + } + + // Fake or spoofed + public function isFake() { + return $this->errno === self::E_PWDSIG; + } + + public function getEncodedData() { + if (!$this->domain || !$this->ip) { + return false; + } + + $data = []; + + // Raw password + if ($this->pwd_raw) { + $data[] = $this->pwd_raw; + } + else { + return false; + } + + // Creation timestamp + if ($this->creation_ts > 0) { + $data[] = pack('V', $this->creation_ts); + } + else { + return false; + } + + // Mask timestamp + if ($this->mask_ts > 0) { + $data[] = pack('V', $this->mask_ts); + } + else { + return false; + } + + // IP timestamp + if ($this->ip_ts > 0) { + $data[] = pack('V', $this->ip_ts); + } + else { + return false; + } + + // Last ativity timestamp + if ($this->activity_ts < 0) { + return false; + } + + $data[] = pack('V', $this->activity_ts); + + // Last action increment timestamp + if ($this->action_ts < 0) { + return false; + } + + $data[] = pack('V', $this->action_ts); + + // Env timestamp + if ($this->env_ts > 0) { + $data[] = pack('V', $this->env_ts); + } + else { + return false; + } + + // Verified level + if ($this->verified_level < 0) { + return false; + } + + $data[] = pack('C', $this->verified_level); + + // Action counts + $data[] = pack('C5', $this->post_count, $this->img_count, $this->thread_count, $this->report_count, $this->action_buffer); + + // IP change score + $data[] = pack('C', $this->ip_change_score); + + // Password signature + if ($this->pwd_sig) { + $data[] = $this->pwd_sig; + } + else { + $data[] = $this->calc_sig([ + $this->pwd_raw, $this->creation_ts, $this->activity_ts, $this->action_ts, $this->env_ts, $this->verified_level, + $this->post_count, $this->img_count, $this->thread_count, $this->report_count, $this->action_buffer, $this->ip_change_score, + $this->domain + ]); + } + + // Mask signature + if ($this->mask_sig) { + $data[] = $this->mask_sig; + } + else { + $data[] = $this->calc_sig([ $this->pwd_raw, $this->mask_ts, $this->get_ip_mask($this->ip), $this->domain ]); + } + + // IP signature + if ($this->ip_sig) { + $data[] = $this->ip_sig; + } + else { + $data[] = $this->calc_sig([ $this->pwd_raw, $this->ip_ts, $this->ip, $this->domain ]); + } + + // Env signature + if ($this->env_sig) { + $data[] = $this->env_sig; + } + else { + $data[] = $this->calc_sig([ $this->pwd_raw, $this->env_ts, $this->env_data, $this->domain ]); + } + + // --- + + $data = implode('', $data); + + list($data, $nonce) = self::encrypt($data); + + if (!$data) { + return false; + } + + // Version + Nonce + $data = pack('C', self::VERSION) . $nonce . $data; + + return self::b64_encode($data); + } + + private static function encrypt($data) { + $data_len = strlen($data); + + $key = hex2bin(self::XOR_KEY); + $nonce = openssl_random_pseudo_bytes(self::NONCE_SIZE); + + if (!$data_len || !$nonce || $data_len > strlen($key)) { + return false; + } + + $output_nonced = ''; + + // Apply nonce + $ni = 0; + + for ($di = 0; $di < $data_len; ++$di) { + if ($ni >= self::NONCE_SIZE) { + $ni = 0; + } + + $output_nonced = $output_nonced . ($data[$di] ^ $nonce[$ni]); + + $ni++; + } + + $output = ''; + + // XOR Encrypt + for ($i = 0; $i < $data_len; ++$i) { + $output = $output . ($output_nonced[$i] ^ $key[$i]); + } + + return [ $output, $nonce ]; + } + + private static function decrypt($data, $nonce) { + $data_len = strlen($data); + + $nonce_len = strlen($nonce); + + $key = hex2bin(self::XOR_KEY); + + if (!$data_len || !$nonce || $data_len > strlen($key)) { + return false; + } + + $output_nonced = ''; + + // XOR Decrypt + for ($i = 0; $i < $data_len; ++$i) { + $output_nonced = $output_nonced . ($data[$i] ^ $key[$i]); + } + + // Apply nonce + $output = ''; + + $ni = 0; + + for ($di = 0; $di < $data_len; ++$di) { + if ($ni >= $nonce_len) { + $ni = 0; + } + + $output = $output . ($output_nonced[$di] ^ $nonce[$ni]); + + $ni++; + } + + return $output; + } + + private function generate() { + if (!$this->ip || !$this->domain) { + return false; + } + + $pwd_raw = openssl_random_pseudo_bytes(self::PWD_SIZE); + + if (!$pwd_raw) { + return false; + } + + $this->version = self::VERSION; + + $this->pwd_raw = $pwd_raw; + $this->pwd_hex = bin2hex($pwd_raw); + $this->creation_ts = $this->now; + $this->mask_ts = $this->now; + $this->ip_ts = $this->now; + $this->env_ts = $this->now; + + return true; + } + + public function setPwd($pwd_hex) { + if (!$pwd_hex) { + return false; + } + + $pwd_raw = hex2bin($pwd_hex); + + if (!$pwd_raw || strlen($pwd_raw) !== self::PWD_SIZE) { + return false; + } + + $this->pwd_raw = $pwd_raw; + $this->pwd_hex = $pwd_hex; + + $this->resetSignatures(); + + return true; + } + + public function setVerifiedLevel($level) { + if ($level < 0) { + return false; + } + $this->verified_level = $level; + $this->pwd_sig = null; + } + + private function resetTimestamps() { + $this->creation_ts = $this->now; + $this->mask_ts = $this->now; + $this->ip_ts = $this->now; + $this->action_ts = $this->now; + $this->activity_ts = 0; + $this->env_ts = $this->now; + } + + private function resetActionCounts() { + $this->post_count = 0; + $this->img_count = 0; + $this->thread_count = 0; + $this->report_count = 0; + + $this->action_buffer = 0; + } + + private function resetSignatures() { + $this->pwd_sig = null; + $this->mask_sig = null; + $this->ip_sig = null; + $this->env_sig = null; + } + + public function setCookie($domain) { + $data = $this->getEncodedData(); + + if ($data) { + return setcookie(self::COOKIE_NAME, $data, $this->now + self::COOKIE_TTL, '/', $domain, true, true); + } + else { + return false; + } + } + + public static function setFakeCookie($now, $domain) { + $size = self::NONCE_SIZE + self::PWD_SIZE + self::DATA_SIZE + self::SIG_SIZE * self::SIG_COUNT; + + $data = openssl_random_pseudo_bytes($size); + + if (!$data) { + return false; + } + + $data = pack('C', self::VERSION) . $data; + + $data = self::b64_encode($data); + + return setcookie(self::COOKIE_NAME, $data, $now + self::COOKIE_TTL, '/', $domain, true); + } +} diff --git a/lib/util.php b/lib/util.php new file mode 100644 index 0000000..982d72b --- /dev/null +++ b/lib/util.php @@ -0,0 +1,516 @@ +true,'b'=>true,'bant'=>true,'d'=>true,'e'=>true,'f'=>true,'gif'=>true,'h'=>true,'hc'=>true,'hm'=>true,'hr'=>true,'i'=>true,'ic'=>true,'pol'=>true,'r'=>true,'r9k'=>true,'s'=>true,'s4s'=>true,'soc'=>true,'t'=>true,'trash'=>true,'u'=>true,'wg'=>true,'y'=>true); + + private static $blue = '4chan.org'; // Domain for worksafe boards + private static $red = '4chan.org'; // Domain for nws boards + + static public function d($board) { + return isset(self::$nws[$board]) ? self::$red : self::$blue; + } +} + +// FIXME ipv6 +function cidrtest ($longip, $CIDR) { + list ($net, $mask) = explode("/", $CIDR); + $mask = (int)$mask; + + if (!$mask) return false; + + $ip_net = ip2long ($net); + + if (!$ip_net) return false; + + $ip_mask = ~((1 << (32 - $mask)) - 1); + + $ip_ip = $longip; + + $ip_ip_net = $ip_ip & $ip_mask; + + return ($ip_ip_net == $ip_net); +} + +function internal_error_log($cat, $err, $trace=true) { + $err = sprintf("[%s error] %s", $cat, $err); + + error_log($err); + if ($trace) { + ob_start(); + debug_print_backtrace(); + error_log(ob_get_contents()); + ob_end_clean(); + } +} + +function quick_log_to( $f, $s, $do_bt=false ) +{ + if ($do_bt) { + ob_start(); + debug_print_backtrace(); + $bt = ob_get_contents(); + ob_end_clean(); + } + + $out = ($do_bt ? $bt : date("r")." ".$s)."\n\n"; + + $h = fopen( $f, "a" ); + flock( $h, LOCK_EX ); + fwrite( $h, $out); + fclose( $h ); +} + +function post_filter_get($base) +{ + $path = "/www/global/yotsuba/filters/"; + + $strs = @file("$path$base.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $res = @file("$path$base-re.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + + if (!is_array($strs)) $strs = array(); + if (!is_array($res)) $res = array(); + + return array($strs, $res); +} + +function find_ipxff_in($longip,$longxff,$ips) +{ + $badip = false; + + foreach($ips as $cidr) + { + if ($cidr[0] == '#' || !$cidr) continue; + + if(cidrtest($longip, $cidr) || cidrtest($longxff, $cidr)) + { + $badip = true; + break; + } + } + + return $badip; +} + +function utf8_wordwrap( $string, $width = 75, $break = "\n", $cut = false ) +{ + if( $cut ) { + // Cut lines that are too long by hand, even if they aren't official break opportunities + $search = '/(.{' . $width . '})/uS'; + $replace = '$1$2' . $break; + } + + return preg_replace( $search, $replace, $string ); +} + +function xhprof_save() +{ + $xhprof_data = xhprof_disable(); + + include_once XHPROF_ROOT . "/xhprof_lib/utils/xhprof_lib.php"; + include_once XHPROF_ROOT . "/xhprof_lib/utils/xhprof_runs.php"; + + $xhprof_runs = new XHProfRuns_Default(); + + $name = basename($_SERVER["SELF_PATH"]); + $name = preg_replace("/\..*$/","",$name); + + $run_id = $xhprof_runs->save_run($xhprof_data, $name); +} + +function record_post_info($fn, $ex="") +{ + $rh = fopen($fn, "a"); + flock($rh, LOCK_EX); + fwrite($rh, print_r($_SERVER, TRUE)); + fwrite($rh, print_r($_REQUEST, TRUE)); + fwrite($rh, print_r($HTTP_RAW_POST_DATA, TRUE)); + fwrite($rh, print_r($_FILES, TRUE)); + if ($ex) fwrite($rh, $ex); + fwrite($rh, "---\n"); + fclose($rh); +} + +function sec2hms($sec, $padTime = false, $showSeconds = false) +{ + $hms = ""; + $hours = intval(intval($sec) / 3600); + $minutes = intval(intval($sec) / 60); + + if ($hours) { + $hms .= $padTime + ? str_pad($hours, 2, "0", STR_PAD_LEFT). " hour" + : $hours. " hour"; + if( $hours != 1 ) $hms .= 's'; + + $hms .= ' '; + } + + if ($minutes) { + $minutes = intval(($sec / 60) % 60); + + $hms .= $padTime + ? str_pad($minutes, 2, "0", STR_PAD_LEFT). " minute" + : $minutes. " minute"; + if( $minutes != 1 ) $hms .= 's'; + } + + $seconds = intval($sec % 60); + + if ($showSeconds && $seconds) { + $hms .= ' '; + $hms .= $padTime + ? str_pad($seconds, 2, "0", STR_PAD_LEFT). " second" + : $seconds. " second"; + if ($seconds != 1) $hms .= "s"; + } + + return $hms; +} + +function flush_output_buffers() +{ + ob_flush(); + flush(); +} + +function country_table() +{ + /** COUNTRY CODE */ + static $countryLookupArray = array( + 'AD' => 'Andorra', + 'AE' => 'United Arab Emirates', + 'AF' => 'Afghanistan', + 'AG' => 'Antigua and Barbuda', + 'AI' => 'Anguilla', + 'AL' => 'Albania', + 'AM' => 'Armenia', + 'AN' => 'Netherlands Antilles', + 'AO' => 'Angola', + 'AQ' => 'Antarctica', + 'AR' => 'Argentina', + 'AS' => 'American Samoa', + 'AT' => 'Austria', + 'AU' => 'Australia', + 'AW' => 'Aruba', + 'AX' => 'Aland', + 'AZ' => 'Azerbaijan', + 'BA' => 'Bosnia and Herzegovina', + 'BB' => 'Barbados', + 'BD' => 'Bangladesh', + 'BE' => 'Belgium', + 'BF' => 'Burkina Faso', + 'BG' => 'Bulgaria', + 'BH' => 'Bahrain', + 'BI' => 'Burundi', + 'BJ' => 'Benin', + 'BL' => 'Saint Barthélemy', + 'BM' => 'Bermuda', + 'BN' => 'Brunei', + 'BO' => 'Bolivia', + 'BQ' => 'Bonaire, Sint Eustatius and Saba', + 'BR' => 'Brazil', + 'BS' => 'Bahamas', + 'BT' => 'Bhutan', + 'BV' => 'Bouvet Island', + 'BW' => 'Botswana', + 'BY' => 'Belarus', + 'BZ' => 'Belize', + 'CA' => 'Canada', + 'CC' => 'Cocos (Keeling) Islands', + 'CD' => 'The Democratic Republic of the Congo', + 'CF' => 'Central African Republic', + 'CG' => 'Congo', + 'CH' => 'Switzerland', + 'CI' => 'Côte d\'Ivoire', + 'CK' => 'Cook Islands', + 'CL' => 'Chile', + 'CM' => 'Cameroon', + 'CN' => 'China', + 'CO' => 'Colombia', + 'CR' => 'Costa Rica', + 'CU' => 'Cuba', + 'CV' => 'Cape Verde', + 'CW' => 'Curaçao', + 'CX' => 'Christmas Island', + 'CY' => 'Cyprus', + 'CZ' => 'Czech Republic', + 'DE' => 'Germany', + 'DJ' => 'Djibouti', + 'DK' => 'Denmark', + 'DM' => 'Dominica', + 'DO' => 'Dominican Republic', + 'DZ' => 'Algeria', + 'EC' => 'Ecuador', + 'EE' => 'Estonia', + 'EG' => 'Egypt', + 'EH' => 'Western Sahara', + 'ER' => 'Eritrea', + 'ES' => 'Spain', + 'ET' => 'Ethiopia', + 'EU' => 'Europe', + 'FI' => 'Finland', + 'FJ' => 'Fiji Islands', + 'FK' => 'Falkland Islands', + 'FM' => 'Federated States of Micronesia', + 'FO' => 'Faroe Islands', + 'FR' => 'France', + 'GA' => 'Gabon', + 'GB' => 'United Kingdom', + 'GD' => 'Grenada', + 'GE' => 'Georgia', + 'GF' => 'French Guiana', + 'GG' => 'Guernsey', + 'GH' => 'Ghana', + 'GI' => 'Gibraltar', + 'GL' => 'Greenland', + 'GM' => 'Gambia', + 'GN' => 'Guinea', + 'GP' => 'Guadeloupe', + 'GQ' => 'Equatorial Guinea', + 'GR' => 'Greece', + 'GS' => 'South Georgia and the South Sandwich Islands', + 'GT' => 'Guatemala', + 'GU' => 'Guam', + 'GW' => 'Guinea-Bissau', + 'GY' => 'Guyana', + 'HK' => 'Hong Kong', + 'HM' => 'Heard Island and McDonald Islands', + 'HN' => 'Honduras', + 'HR' => 'Croatia', + 'HT' => 'Haiti', + 'HU' => 'Hungary', + 'ID' => 'Indonesia', + 'IE' => 'Ireland', + 'IL' => 'Israel', + 'IM' => 'Isle of Man', + 'IN' => 'India', + 'IO' => 'British Indian Ocean Territory', + 'IQ' => 'Iraq', + 'IR' => 'Iran', + 'IS' => 'Iceland', + 'IT' => 'Italy', + 'JE' => 'Jersey', + 'JM' => 'Jamaica', + 'JO' => 'Jordan', + 'JP' => 'Japan', + 'KE' => 'Kenya', + 'KG' => 'Kyrgyzstan', + 'KH' => 'Cambodia', + 'KI' => 'Kiribati', + 'KM' => 'Comoros', + 'KN' => 'Saint Kitts and Nevis', + 'KP' => 'North Korea', + 'KR' => 'South Korea', + 'KW' => 'Kuwait', + 'KY' => 'Cayman Islands', + 'KZ' => 'Kazakhstan', + 'LA' => 'Laos', + 'LB' => 'Lebanon', + 'LC' => 'Saint Lucia', + 'LI' => 'Liechtenstein', + 'LK' => 'Sri Lanka', + 'LR' => 'Liberia', + 'LS' => 'Lesotho', + 'LT' => 'Lithuania', + 'LU' => 'Luxembourg', + 'LV' => 'Latvia', + 'LY' => 'Libya', + 'MA' => 'Morocco', + 'MC' => 'Monaco', + 'MD' => 'Moldova', + 'ME' => 'Montenegro', + 'MF' => 'Saint Martin', + 'MG' => 'Madagascar', + 'MH' => 'Marshall Islands', + 'MK' => 'Macedonia', + 'ML' => 'Mali', + 'MM' => 'Myanmar', + 'MN' => 'Mongolia', + 'MO' => 'Macao', + 'MP' => 'Northern Mariana Islands', + 'MQ' => 'Martinique', + 'MR' => 'Mauritania', + 'MS' => 'Montserrat', + 'MT' => 'Malta', + 'MU' => 'Mauritius', + 'MV' => 'Maldives', + 'MW' => 'Malawi', + 'MX' => 'Mexico', + 'MY' => 'Malaysia', + 'MZ' => 'Mozambique', + 'NA' => 'Namibia', + 'NC' => 'New Caledonia', + 'NE' => 'Niger', + 'NF' => 'Norfolk Island', + 'NG' => 'Nigeria', + 'NI' => 'Nicaragua', + 'NL' => 'Netherlands', + 'NO' => 'Norway', + 'NP' => 'Nepal', + 'NR' => 'Nauru', + 'NU' => 'Niue', + 'NZ' => 'New Zealand', + 'OM' => 'Oman', + 'PA' => 'Panama', + 'PE' => 'Peru', + 'PF' => 'French Polynesia', + 'PG' => 'Papua New Guinea', + 'PH' => 'Philippines', + 'PK' => 'Pakistan', + 'PL' => 'Poland', + 'PM' => 'Saint Pierre and Miquelon', + 'PN' => 'Pitcairn', + 'PR' => 'Puerto Rico', + 'PS' => 'Palestine', + 'PT' => 'Portugal', + 'PW' => 'Palau', + 'PY' => 'Paraguay', + 'QA' => 'Qatar', + 'RE' => 'Réunion', + 'RO' => 'Romania', + 'RS' => 'Serbia', + 'RU' => 'Russian Federation', + 'RW' => 'Rwanda', + 'SA' => 'Saudi Arabia', + 'SB' => 'Solomon Islands', + 'SC' => 'Seychelles', + 'SD' => 'Sudan', + 'SE' => 'Sweden', + 'SG' => 'Singapore', + 'SH' => 'Saint Helena, Ascension, and Tristan da Cunha', + 'SI' => 'Slovenia', + 'SJ' => 'Svalbard and Jan Mayen', + 'SK' => 'Slovakia', + 'SL' => 'Sierra Leone', + 'SM' => 'San Marino', + 'SN' => 'Senegal', + 'SO' => 'Somalia', + 'SR' => 'Suriname', + 'SS' => 'South Sudan', + 'ST' => 'Sao Tome and Principe', + 'SV' => 'El Salvador', + 'SX' => 'Sint Maarten', + 'SY' => 'Syria', + 'SZ' => 'Swaziland', + 'TC' => 'Turks and Caicos Islands', + 'TD' => 'Chad', + 'TF' => 'French Southern Territories', + 'TG' => 'Togo', + 'TH' => 'Thailand', + 'TJ' => 'Tajikistan', + 'TK' => 'Tokelau', + 'TM' => 'Turkmenistan', + 'TN' => 'Tunisia', + 'TO' => 'Tonga', + 'TP' => 'East Timor', + 'TR' => 'Turkey', + 'TT' => 'Trinidad and Tobago', + 'TV' => 'Tuvalu', + 'TW' => 'Taiwan', + 'TZ' => 'Tanzania', + 'UA' => 'Ukraine', + 'UG' => 'Uganda', + 'UM' => 'United States Minor Outlying Islands', + 'US' => 'United States', + 'UY' => 'Uruguay', + 'UZ' => 'Uzbekistan', + 'VA' => 'Holy See (Vatican City State)', + 'VC' => 'Saint Vincent and the Grenadines', + 'VE' => 'Venezuela', + 'VG' => 'British Virgin Islands', + 'VI' => 'U.S. Virgin Islands', + 'VN' => 'Vietnam', + 'VU' => 'Vanuatu', + 'WF' => 'Wallis and Futuna', + 'WS' => 'Samoa', + 'XE' => 'England', + 'XK' => 'Kosovo', + 'XS' => 'Scotland', + 'XW' => 'Wales', + 'YE' => 'Yemen', + 'YT' => 'Mayotte', + 'YU' => 'Yugoslavia', + 'ZA' => 'South Africa', + 'ZM' => 'Zambia', + 'ZW' => 'Zimbabwe', + 'XX' => 'Unknown'); + + return $countryLookupArray; +} + +function country_code_to_name( $code ) +{ + $countryLookupArray = country_table(); + + return isset( $countryLookupArray[$code] ) ? $countryLookupArray[$code] : $countryLookupArray['XX']; +} + +function troll_countries() { + static $trollCountries = array( + 'AC' => 'Anarcho-Capitalist', + 'AN' => 'Anarchist', + 'BL' => 'Black Lives Matter', + 'CF' => 'Confederate', + 'CM' => 'Commie', + 'CT' => 'Catalonia', + 'DM' => 'Democrat', + 'EU' => 'European', + 'FC' => 'Fascist', + 'GN' => 'Gadsden', + 'GY' => 'LGBT', + 'JH' => 'Jihadi', + 'KN' => 'Kekistani', + 'MF' => 'Muslim', + 'NB' => 'National Bolshevik', + 'NZ' => 'Nazi', + 'PC' => 'Hippie', + 'PR' => 'Pirate', + 'RE' => 'Republican', + 'TM' => 'DEUS VULT', + 'TR' => 'Tree Hugger', + 'UN' => 'United Nations', + 'WP' => 'White Supremacist' + ); + + return $trollCountries; +} + +// For flag selection dropdown menu +function troll_countries_selector() { + static $trollCountries = array( + 'AC' => 'Anarcho-Capitalist', + 'AN' => 'Anarchist', + 'BL' => 'Black Nationalist', + 'CF' => 'Confederate', + 'CM' => 'Communist', + 'CT' => 'Catalonia', + 'DM' => 'Democrat', + 'EU' => 'European', + 'FC' => 'Fascist', + 'GN' => 'Gadsden', + 'GY' => 'Gay', + 'JH' => 'Jihadi', + 'KN' => 'Kekistani', + 'MF' => 'Muslim', + 'NB' => 'National Bolshevik', + 'NZ' => 'Nazi', + 'PC' => 'Hippie', + 'PR' => 'Pirate', + 'RE' => 'Republican', + 'TM' => 'Templar', + 'TR' => 'Tree Hugger', + 'UN' => 'United Nations', + 'WP' => 'White Supremacist' + ); + + return $trollCountries; +} + +function country_code_to_name_troll( $code ) +{ + $trollCountries = troll_countries(); + + return isset( $trollCountries[$code] ) ? $trollCountries[$code] : $trollCountries['IL']; +} + diff --git a/modes/report-test.php b/modes/report-test.php new file mode 100644 index 0000000..fbe6765 --- /dev/null +++ b/modes/report-test.php @@ -0,0 +1,851 @@ + STATIC_SERVER.'css/yotsuba.css', + 'Yotsuba B' => STATIC_SERVER.'css/yotsublue.css', + 'Futaba' => STATIC_SERVER.'css/futaba.css', + 'Burichan' => STATIC_SERVER.'css/burichan.css', + ); + $board = mysql_real_escape_string($board); + $query = mysql_global_call("SELECT domain FROM boardlist where dir='$board'"); + list($domain) = mysql_fetch_row($query); + if(DEFAULT_BURICHAN == 1) + $styletitle = ($_COOKIE['ws_style']?$_COOKIE['ws_style']:'Yotsuba B'); + elseif($domain == 'may') + $styletitle = 'not4chan'; + else + $styletitle = ($_COOKIE['nws_style']?$_COOKIE['nws_style']:'Yotsuba'); + return $styles[$styletitle]; + } + +function log_cleared_reporter($long_ip, $pwd, $pass_id, $cat_id, $weight) { + $sql = << 0; + + if (!$is_illegal && $tpl['save_post'] === 'everything') { + $salt = file_get_contents(SALTFILE); + $hash = sha1($board . $post['no'] . $salt); + + if (file_exists($img_filepath)) { + copy( + $img_filepath, + BANIMG_ROOT . "$board/$hash{$post['ext']}" + ); + + copy( + $thumb_filepath, + BANTHUMB_DIR . "{$hash}s.jpg" + ); + } + } + else { + $post['raw_md5'] = $post['md5']; + } + } + + // Get the subject of the thread + if ($post['resto']) { + $sql = "SELECT sub FROM `%s` WHERE no = %d"; + $res = mysql_board_call($sql, $board, $post['resto']); + $_sub = mysql_fetch_assoc($res); + if ($_sub) { + $rel_sub = $_sub['sub']; + + if (strpos($rel_sub, 'SPOILER<>') === 0) { + $rel_sub = substr($rel_sub, 9); + } + + if ($rel_sub !== '') { + $post['rel_sub'] = $rel_sub; + } + } + } + + // Insert the ban request + $tpl_name = $tpl['name']; + $tpl_global = $tpl['bantype'] !== 'local' ? 1 : 0; + + $sql = <<ipLifetime() < 604800) { // 7 days + return false; + } + + if (!$post['fsize']) { // only posts with images + return false; + } + + $allowance = 3; + + $long_ip = ip2long($ip); + + if (!$long_ip) { + return false; + } + + // Allow $allowance no-captcha reports for every hour of inactivity + $sql = << DATE_SUB(NOW(), INTERVAL 1 HOUR) +SQL; + + $res = mysql_global_call($sql); + + if (!$res) { + return false; + } + + $row = mysql_fetch_row($res); + + if (!$row || $row[0] >= $allowance) { + return false; + } + + // Don't allow ips with 1 cleared reports in the past 72 hours + $sql = << DATE_SUB(NOW(), INTERVAL 72 HOUR) +SQL; + + $res = mysql_global_call($sql); + + if (!$res) { + return false; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count >= 1) { + return false; + } + + // Don't allow ips with recent warn/ban history + $sql = << DATE_SUB(NOW(), INTERVAL 30 DAY) +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $ip); + + if (!$res) { + return false; + } + + if (mysql_num_rows($res) > 0) { + return false; + } + + return true; +} + + function report_check_ip($board, $no, $check_ban = false, $is_illegal = false) { + global $captcha_bypass, $passid; + + $board = mysql_real_escape_string($board); + + $no = mysql_real_escape_string($no); + + $ip = ip2long($_SERVER['REMOTE_ADDR']); + + $pass_sql = false; + + $pwd_sql = false; + + // Check if already reported + // by IP + $rep_clauses = array("ip = '$ip'"); + + // by 4chan pass + if ($captcha_bypass && $passid) { + $pass_sql = mysql_real_escape_string($passid); + $rep_clauses[] = "4pass_id = '$pass_sql'"; + } + + // by password + $userpwd = UserPwd::getSession(); + + if ($userpwd && $userpwd->getPwd()) { + $pwd_sql = mysql_real_escape_string($userpwd->getPwd()); + $rep_clauses[] = "pwd = '$pwd_sql'"; + } + + $rep_clauses_sql = implode(' OR ', $rep_clauses); + + $res = mysql_global_call("SELECT no FROM reports WHERE ($rep_clauses_sql) AND board = '$board' AND no = '$no'"); + + if ($res && mysql_num_rows($res) > 0) { + fancydie('You have already reported this post.'); + } + + // Check cooldown + $res = mysql_global_call("SELECT no FROM reports WHERE ($rep_clauses_sql) AND ts > DATE_SUB(NOW(), INTERVAL 15 SECOND) LIMIT 1"); + + if ($res && mysql_num_rows($res) > 0) { + fancydie('You have to wait a while before reporting another post.'); + } + + // Check hourly limits + $res = mysql_global_call("SELECT COUNT(*) FROM reports WHERE ($rep_clauses_sql) AND ts > DATE_SUB(NOW(), INTERVAL 1 HOUR) LIMIT 1"); + + if ($res && mysql_fetch_row($res)[0] >= RENZOKU_REP_HOURLY) { + fancydie('You have to wait a while before reporting another post.'); + } + + // Check daily limits + $res = mysql_global_call("SELECT COUNT(*) FROM reports WHERE ($rep_clauses_sql) AND ts > DATE_SUB(NOW(), INTERVAL 24 HOUR) LIMIT 1"); + + if ($res && mysql_fetch_row($res)[0] >= RENZOKU_REP_DAILY) { + fancydie('You have to wait a while before reporting another post.'); + } + + // Check if banned + if ($check_ban) { + $ip_sql = mysql_real_escape_string($_SERVER['REMOTE_ADDR']); + + // by ip + $ban_clauses = array("host = '$ip_sql'"); + + // by 4chan pass + if ($pass_sql) { + $ban_clauses[] = "4pass_id = '$pass_sql'"; + } + + // by password + if ($pwd_sql) { + $ban_clauses[] = "password = '$pwd_sql'"; + } + + $ban_clauses_sql = implode(' OR ', $ban_clauses); + + $res = mysql_global_call("SELECT COUNT(*) FROM banned_users WHERE ($ban_clauses_sql) AND active = 1 AND (global = 1 OR board = '$board')"); + + if ($res && mysql_fetch_row($res)[0] > 0) { + fancydie('You can\'t report posts because you are banned.'); + } + + if ($captcha_bypass !== true) { + $longip = ip2long($_SERVER['REMOTE_ADDR']); + + if (isset($_SERVER['HTTP_X_GEO_ASN'])) { + $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; + } + else { + $_asninfo = GeoIP2::get_asn($_SERVER['REMOTE_ADDR']); + + if ($_asninfo) { + $asn = (int)$_asninfo['asn']; + } + else { + $asn = 0; + } + } + + if (isIPRangeBannedReport($longip, $asn, BOARD_DIR, $userpwd)) { + fancydie('Reporting from this IP range has been blocked due to abuse. [More Info]
    4chan Pass users can bypass this block. [Learn More]'); + } + } + } + } + + function report_increment_counter() { + return; // broken lol + $count = @file_get_contents('reports/report.count'); + if(!$count) $count = 0; + $count++; + file_put_contents('reports/report.count',$count); + } + + function report_post_exists($no) { + $query=mysql_board_call("SELECT COUNT(*) FROM `".SQLLOG."` WHERE no='$no'"); + return mysql_result($query,0,0); + } + + function report_is_capcoded_post( $no ) + { + $query = mysql_board_call( "SELECT COUNT(*) FROM `%s` WHERE capcode != 'none' AND no=%d", SQLLOG, $no ); + return mysql_result( $query, 0, 0 ); + } + + function report_check_autodelete($board,$no) { + $query = mysql_global_do("SELECT COUNT(*) FROM reports WHERE board='$board' AND no='$no'"); + $count = mysql_result($query,0,0); + + if(defined('REPORTS_AUTODELETE') && $count >= REPORTS_AUTODELETE) { + report_do_autodelete($board,$no,1); + return; + } + + $query = mysql_global_do("SELECT COUNT(*) FROM reports WHERE cat='2' AND board='$board' AND no='$no'"); + $count = mysql_result($query,0,0); + if(defined('REPORTS_AUTODELETE_ILLEGAL') && $count >= REPORTS_AUTODELETE_ILLEGAL) { + report_do_autodelete($board,$no,2); + return; + } + } + function report_do_autodelete($board,$no,$cat) { + $query = mysql_board_call("SELECT * FROM `".SQLLOG."` WHERE no='$no'"); + $row = mysql_fetch_assoc($query); + if(!$row) return; + $auser = 'Auto-del'; + $adfsize=($row['fsize']>0)?1:0; + $adname=str_replace(' !','#',$row['name']); + $imgonly = 0; + $row['sub'] = mysql_escape_string($row['sub']); + $row['com'] = mysql_escape_string($row['com']); + $row['filename'] = mysql_escape_string($row['filename']); + mysql_global_do("INSERT INTO ".SQLLOGDEL." (imgonly,postno,board,name,sub,com,img,filename,admin) values('$imgonly','$no','".SQLLOG."','$adname','{$row['sub']}','{$row['com']}','$adfsize','{$row['filename']}','$auser')"); + delete_post($no, '', 0, 1, 1); + } + function report_log_action($board,$no) { + mysql_global_call("insert into user_actions (ip,board,action,postno,time) values (%d,'%s','report',%d,now())", ip2long($_SERVER["REMOTE_ADDR"]), $board, $no); + } + + function report_post_sticky($no) { + $query=mysql_board_call("SELECT sticky FROM `".SQLLOG."` WHERE no='$no'"); + return mysql_result($query,0,0); + } + +function report_check_post($board, $post_id) { + $sql = "SELECT * FROM `%s` WHERE no = %d"; + + $res = mysql_board_call($sql, $board, $post_id); + + if (!$res) { + fancydie(S_POST_DEAD); + } + + $post = mysql_fetch_assoc($res); + + if (!$post) { + fancydie(S_POST_DEAD); + } + + if ($post['sticky']) { + fancydie(S_CANNOTREPORTSTICKY); + } + + if ($post['capcode'] !== 'none') { + fancydie(S_CANNOTREPORT); + } + + return $post; +} + +function get_report_categories($board, $post_id, $is_worksafe) { + $query = "SELECT * FROM report_categories ORDER BY board ASC"; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $query = "SELECT resto, fsize, filedeleted FROM `%s` WHERE no = %d"; + + $res2 = mysql_board_call($query, $board, $post_id); + + if (!$res2) { + return false; + } + + $post = mysql_fetch_assoc($res2); + + if (!$post) { + return false; + } + + $is_op = !$post['resto']; + $has_image = $post['fsize'] && !$post['filedeleted']; + + // ID of the category which will be used for the Illegal radio button + $illegal_cat_id = 31; + + // Rule violations + one illegal category + $data = array('rule' => null, 'illegal' => null); + + // Sorting, board specific categories go on top + $data_rule_top = array(); + $data_rule_bottom = array(); + + $match_board = ',' . $board . ','; + + while ($cat = mysql_fetch_assoc($res)) { + if ($cat['id'] == $illegal_cat_id) { + $data['illegal'] = $cat; + continue; + } + + if ($cat['board'] !== '') { + if ($cat['board'] === '_ws_') { + if (!$is_worksafe) { + continue; + } + } + else if ($cat['board'] === '_nws_') { + if ($is_worksafe) { + continue; + } + } + else if ($cat['board'] !== $board) { + continue; + } + } + + if ($cat['op_only'] && !$is_op) { + continue; + } + + if ($cat['reply_only'] && $is_op) { + continue; + } + + if ($cat['image_only'] && !$has_image) { + continue; + } + + if ($cat['exclude_boards'] && strpos(",{$cat['exclude_boards']},", $match_board) !== false) { + continue; + } + + if ($cat['board']) { + $data_rule_top[$cat['id']] = $cat; + } + else { + $data_rule_bottom[$cat['id']] = $cat; + } + } + + $data['rule'] = $data_rule_top + $data_rule_bottom; + + return $data; +} + +/** + * Checks if the report should have a different priority + * based on the number of cleared reports in the past X days and ban history. + */ +function is_report_filtered($filter_thres, $ip, $long_ip, $pass_id = null, $pwd = null) { + if ($filter_thres < 1) { + return false; + } + + // only count reports made in the past X days + $cleared_days_lim = 2; + // number of cleared reports for the IP to be considered 'abusive' + $cleared_count_lim = (int)$filter_thres; + // only count bans made in the past X days + $ban_days_lim = 30; + // number of bans/warnings for the IP to be considered 'abusive' + $ban_count_lim = 3; + + $rep_abuse_tpl = 190; // ban template for report abusing + + $long_ip = (int)$long_ip; + + $ban_clauses = array(); + $rep_clauses = array(); + + // 4chan Pass + if ($pass_id) { + $pass_id_sql = mysql_real_escape_string($pass_id); + $ban_clauses[] = "4pass_id = '$pass_id_sql'"; + $rep_clauses[] = "pass_id = '$pass_id_sql'"; + + $pwd_and_ban = "4pass_id != '$pass_id_sql'"; + $pwd_and_rep = "pass_id != '$pass_id_sql'"; + } + // IP + else { + $ip_sql = mysql_real_escape_string($ip); + $ban_clauses[] = "host = '$ip_sql'"; + $rep_clauses[] = "long_ip = $long_ip"; + + $pwd_and_ban = "host != '$ip_sql'"; + $pwd_and_rep = "long_ip != $long_ip"; + } + + // Password + if ($pwd) { + $pwd_sql = mysql_real_escape_string($pwd); + $ban_clauses[] = "password = '$pwd_sql' AND $pwd_and_ban"; + $rep_clauses[] = "pwd = '$pwd_sql' AND $pwd_and_rep"; + } + + // --- + // Check cleared reports + // --- + $clear_count = 0; + + foreach ($rep_clauses as $clause) { + $query = << DATE_SUB(NOW(), INTERVAL $cleared_days_lim DAY) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $clear_count += (int)mysql_fetch_row($res)[0]; + + if ($clear_count >= $cleared_count_lim) { + return true; + } + } + + // --- + // Check ban history + // --- + $ban_count = 0; + + foreach ($ban_clauses as $clause) { + $query = << DATE_SUB(NOW(), INTERVAL $ban_days_lim DAY) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $ban_count += (int)mysql_fetch_row($res)[0]; + + if ($ban_count >= $ban_count_lim) { + return true; + } + } + + return false; +} + +function report_get_rel_sub($board, $thread_id) { + if (!$board || !$thread_id) { + return ''; + } + + $thread_id = (int)$thread_id; + + $query = "SELECT sub FROM `%s` WHERE no = $thread_id"; + + $res = mysql_board_call($query, $board); + + if (!$res || mysql_num_rows($res) !== 1) { + return ''; + } + + return mysql_fetch_row($res)[0]; +} + +function report_submit($board, $no, $cat_id) { + global $log, $passid; + + $board = mysql_real_escape_string($board); + $no = (int)$no; + $long_ip = ip2long($_SERVER['REMOTE_ADDR']); + + // check if the category is valid + $cats = get_report_categories($board, $no, DEFAULT_BURICHAN == 1); + + if ($cats['illegal']['id'] == $cat_id) { + $old_cat = 2; // todo: remove later + $old_field = 'num_illegal'; // todo: remove later + $rep_cat = $cats['illegal']; + } + else if (isset($cats['rule'][$cat_id])) { + $old_cat = 1; + $old_field = 'num_rule'; + $rep_cat = $cats['rule'][$cat_id]; + } + else { + fancydie('Invalid category selected.'); + } + + if (!$no) { + fancydie(S_POST_DEAD); + } + + log_cache(0, $no, 2); + + if ($log[$no]['archived']) { + $extra = array('archived' => 1); + $is_archived = true; + } + else { + $extra = array(); + $is_archived = false; + } + + $resto = (int)$log[$no]['resto']; + + $post_data = generate_post_json($log[$no], $log[$no]['resto'] ? $log[$no]['resto'] : $no, $extra); + + if ($log[$no]['resto']) { + $rel_sub = report_get_rel_sub($board, $log[$no]['resto']); + + if ($rel_sub !== '') { + $post_data['rel_sub'] = $rel_sub; + } + } + + $json = json_encode($post_data); + + $weight = $rep_cat['weight']; + + $is_staff = has_level('janitor'); + + $req_sig = spam_filter_get_req_sig(); + + $userpwd = UserPwd::getSession(); + + if ($userpwd) { + $pwd = $userpwd->getPwd(); + $is_new_pwd = $userpwd->isNew(); + $is_known_pwd = $userpwd->isUserKnownOrVerified(60); + } + else { + $pwd = null; + $is_new_pwd = true; + $is_known_pwd = false; + } + + if (!$is_staff) { + $ignore_reason = 0; + + $_threat_score = spam_filter_get_threat_score(null, true, false); + + if (!$is_known_pwd) { + $ignore_reason = 1; + } + else if ($_threat_score >= 0.4) { + $ignore_reason = 2; + } + else if ($rep_cat['filtered']) { + if (is_report_filtered($rep_cat['filtered'], $_SERVER['REMOTE_ADDR'], $long_ip, $passid, $is_new_pwd ? null : $pwd)) { + $ignore_reason = 3; + } + } + } + + if ($ignore_reason > 0) { + $weight = 0.5; + if ($ignore_reason == 2) { + $_bot_headers = spam_filter_format_http_headers($log[$no]['com'], '', '', $_threat_score, $req_sig); + log_spam_filter_trigger('ignore_report_score', $board, $no, $_SERVER['REMOTE_ADDR'], $ignore_reason, $_bot_headers); + } + } + + // Check if the post was already reported and cleared + $is_cleared = 0; + $cleared_by = ''; + + $query = "SELECT cleared_by FROM reports WHERE board = '$board' AND no = $no AND cleared = 1 LIMIT 1"; + + $res = mysql_global_call($query); + + if ($res) { + $row = mysql_fetch_row($res); + + if ($row) { + $is_cleared = 1; + $cleared_by = $row[0]; + log_cleared_reporter($long_ip, $pwd, $passid, $rep_cat['id'], $weight); + } + } + /* + if ($board === 'test' && $resto && !$is_archived) { + if ($cat_id == 39) { + $template_id = 6; // Global 5 - NWS on Worksafe Board + } + else if ($cat_id == 35) { + $template_id = 226; // Global 3 - Loli/shota pornography + } + else { + $template_id = 0; + } + + if ($template_id && $userpwd && $userpwd->isUserKnown(1440)) { + $_res = mysql_board_call("SELECT email FROM `$board` WHERE no = $no LIMIT 1"); + $_ua = mysql_fetch_row($_res)[0]; + $_userinfo = decode_user_meta($_ua); + + if ($_userinfo['is_new']) { + $_ret = report_create_ban_req($board, $no, $template_id); + + if ($_ret < 0) { + fancydie('Error: ' . $_ret); + } + + report_delete_post($no); + + if ($userpwd) { + $userpwd->updateReportActivity(); + $userpwd->setCookie('.' . MAIN_DOMAIN); + } + + fancydie('Report submitted! This window will close in 3 seconds...', 1); + return; + } + } + }*/ + + $is_ws = DEFAULT_BURICHAN == 1 ? 1 : 0; + + $query = <<= num_rule, 2, 1) +SQL; + + $res = mysql_global_call($query); + + report_log_action($board, $no); + + if ($userpwd) { + $userpwd->updateReportActivity(); + $userpwd->setCookie('.' . MAIN_DOMAIN); + } + + fancydie('Report submitted! This window will close in 3 seconds...', 1); +} diff --git a/modes/report.php b/modes/report.php new file mode 100644 index 0000000..c857113 --- /dev/null +++ b/modes/report.php @@ -0,0 +1,665 @@ + STATIC_SERVER.'css/yotsuba.css', + 'Yotsuba B' => STATIC_SERVER.'css/yotsublue.css', + 'Futaba' => STATIC_SERVER.'css/futaba.css', + 'Burichan' => STATIC_SERVER.'css/burichan.css', + ); + $board = mysql_real_escape_string($board); + $query = mysql_global_call("SELECT domain FROM boardlist where dir='$board'"); + list($domain) = mysql_fetch_row($query); + if(DEFAULT_BURICHAN == 1) + $styletitle = ($_COOKIE['ws_style']?$_COOKIE['ws_style']:'Yotsuba B'); + elseif($domain == 'may') + $styletitle = 'not4chan'; + else + $styletitle = ($_COOKIE['nws_style']?$_COOKIE['nws_style']:'Yotsuba'); + return $styles[$styletitle]; + } + +function log_cleared_reporter($long_ip, $pwd, $pass_id, $cat_id, $weight) { + $sql = <<ipLifetime() < 604800) { // 7 days + return false; + } + + if (!$post['fsize']) { // only posts with images + return false; + } + + $allowance = 3; + + $long_ip = ip2long($ip); + + if (!$long_ip) { + return false; + } + + // Allow $allowance no-captcha reports for every hour of inactivity + $sql = << DATE_SUB(NOW(), INTERVAL 1 HOUR) +SQL; + + $res = mysql_global_call($sql); + + if (!$res) { + return false; + } + + $row = mysql_fetch_row($res); + + if (!$row || $row[0] >= $allowance) { + return false; + } + + // Don't allow ips with 1 cleared reports in the past 72 hours + $sql = << DATE_SUB(NOW(), INTERVAL 72 HOUR) +SQL; + + $res = mysql_global_call($sql); + + if (!$res) { + return false; + } + + $count = (int)mysql_fetch_row($res)[0]; + + if ($count >= 1) { + return false; + } + + // Don't allow ips with recent warn/ban history + $sql = << DATE_SUB(NOW(), INTERVAL 30 DAY) +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $ip); + + if (!$res) { + return false; + } + + if (mysql_num_rows($res) > 0) { + return false; + } + + return true; +} + + function report_check_ip($board, $no, $check_ban = false, $is_illegal = false) { + global $captcha_bypass, $passid; + + $board = mysql_real_escape_string($board); + + $no = mysql_real_escape_string($no); + + $ip = ip2long($_SERVER['REMOTE_ADDR']); + + $pass_sql = false; + + $pwd_sql = false; + + // Check if already reported + // by IP + $rep_clauses = array("ip = '$ip'"); + + // by 4chan pass + if ($captcha_bypass && $passid) { + $pass_sql = mysql_real_escape_string($passid); + $rep_clauses[] = "4pass_id = '$pass_sql'"; + } + + // by password + $userpwd = UserPwd::getSession(); + + if ($userpwd && $userpwd->getPwd()) { + $pwd_sql = mysql_real_escape_string($userpwd->getPwd()); + $rep_clauses[] = "pwd = '$pwd_sql'"; + } + + $rep_clauses_sql = implode(' OR ', $rep_clauses); + + $res = mysql_global_call("SELECT no FROM reports WHERE ($rep_clauses_sql) AND board = '$board' AND no = '$no'"); + + if ($res && mysql_num_rows($res) > 0) { + fancydie('You have already reported this post.'); + } + + // Check cooldown + $res = mysql_global_call("SELECT no FROM reports WHERE ($rep_clauses_sql) AND ts > DATE_SUB(NOW(), INTERVAL 15 SECOND) LIMIT 1"); + + if ($res && mysql_num_rows($res) > 0) { + fancydie('You have to wait a while before reporting another post.'); + } + + // Check hourly limits + $res = mysql_global_call("SELECT COUNT(*) FROM reports WHERE ($rep_clauses_sql) AND ts > DATE_SUB(NOW(), INTERVAL 1 HOUR) LIMIT 1"); + + if ($res && mysql_fetch_row($res)[0] >= RENZOKU_REP_HOURLY) { + fancydie('You have to wait a while before reporting another post.'); + } + + // Check daily limits + $res = mysql_global_call("SELECT COUNT(*) FROM reports WHERE ($rep_clauses_sql) AND ts > DATE_SUB(NOW(), INTERVAL 24 HOUR) LIMIT 1"); + + if ($res && mysql_fetch_row($res)[0] >= RENZOKU_REP_DAILY) { + fancydie('You have to wait a while before reporting another post.'); + } + + // Check if banned + if ($check_ban) { + $ip_sql = mysql_real_escape_string($_SERVER['REMOTE_ADDR']); + + // by ip + $ban_clauses = array("host = '$ip_sql'"); + + // by 4chan pass + if ($pass_sql) { + $ban_clauses[] = "4pass_id = '$pass_sql'"; + } + + // by password + if ($pwd_sql) { + $ban_clauses[] = "password = '$pwd_sql'"; + } + + $ban_clauses_sql = implode(' OR ', $ban_clauses); + + $res = mysql_global_call("SELECT COUNT(*) FROM banned_users WHERE ($ban_clauses_sql) AND active = 1 AND (global = 1 OR board = '$board')"); + + if ($res && mysql_fetch_row($res)[0] > 0) { + fancydie('You can\'t report posts because you are banned.'); + } + + if ($captcha_bypass !== true) { + $longip = ip2long($_SERVER['REMOTE_ADDR']); + + if (isset($_SERVER['HTTP_X_GEO_ASN'])) { + $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; + } + else { + $_asninfo = GeoIP2::get_asn($_SERVER['REMOTE_ADDR']); + + if ($_asninfo) { + $asn = (int)$_asninfo['asn']; + } + else { + $asn = 0; + } + } + + if (isIPRangeBannedReport($longip, $asn, BOARD_DIR, $userpwd)) { + fancydie('Reporting from this IP range has been blocked due to abuse. [More Info]
    4chan Pass users can bypass this block. [Learn More]'); + } + } + } + } + + function report_increment_counter() { + return; // broken lol + $count = @file_get_contents('reports/report.count'); + if(!$count) $count = 0; + $count++; + file_put_contents('reports/report.count',$count); + } + + function report_post_exists($no) { + $query=mysql_board_call("SELECT COUNT(*) FROM `".SQLLOG."` WHERE no='$no'"); + return mysql_result($query,0,0); + } + + function report_is_capcoded_post( $no ) + { + $query = mysql_board_call( "SELECT COUNT(*) FROM `%s` WHERE capcode != 'none' AND no=%d", SQLLOG, $no ); + return mysql_result( $query, 0, 0 ); + } + + function report_check_autodelete($board,$no) { + $query = mysql_global_do("SELECT COUNT(*) FROM reports WHERE board='$board' AND no='$no'"); + $count = mysql_result($query,0,0); + + if(defined('REPORTS_AUTODELETE') && $count >= REPORTS_AUTODELETE) { + report_do_autodelete($board,$no,1); + return; + } + + $query = mysql_global_do("SELECT COUNT(*) FROM reports WHERE cat='2' AND board='$board' AND no='$no'"); + $count = mysql_result($query,0,0); + if(defined('REPORTS_AUTODELETE_ILLEGAL') && $count >= REPORTS_AUTODELETE_ILLEGAL) { + report_do_autodelete($board,$no,2); + return; + } + } + function report_do_autodelete($board,$no,$cat) { + $query = mysql_board_call("SELECT * FROM `".SQLLOG."` WHERE no='$no'"); + $row = mysql_fetch_assoc($query); + if(!$row) return; + $auser = 'Auto-del'; + $adfsize=($row['fsize']>0)?1:0; + $adname=str_replace('
    !','#',$row['name']); + $imgonly = 0; + $row['sub'] = mysql_escape_string($row['sub']); + $row['com'] = mysql_escape_string($row['com']); + $row['filename'] = mysql_escape_string($row['filename']); + mysql_global_do("INSERT INTO ".SQLLOGDEL." (imgonly,postno,board,name,sub,com,img,filename,admin) values('$imgonly','$no','".SQLLOG."','$adname','{$row['sub']}','{$row['com']}','$adfsize','{$row['filename']}','$auser')"); + delete_post($no, '', 0, 1, 1); + } + function report_log_action($board,$no) { + mysql_global_call("insert into user_actions (ip,board,action,postno,time) values (%d,'%s','report',%d,now())", ip2long($_SERVER["REMOTE_ADDR"]), $board, $no); + } + + function report_post_sticky($no) { + $query=mysql_board_call("SELECT sticky FROM `".SQLLOG."` WHERE no='$no'"); + return mysql_result($query,0,0); + } + +function report_check_post($board, $post_id) { + $sql = "SELECT * FROM `%s` WHERE no = %d"; + + $res = mysql_board_call($sql, $board, $post_id); + + if (!$res) { + fancydie(S_POST_DEAD); + } + + $post = mysql_fetch_assoc($res); + + if (!$post) { + fancydie(S_POST_DEAD); + } + + if ($post['sticky']) { + fancydie(S_CANNOTREPORTSTICKY); + } + + if ($post['capcode'] !== 'none') { + fancydie(S_CANNOTREPORT); + } + + return $post; +} + +function get_report_categories($board, $post_id, $is_worksafe) { + $query = "SELECT * FROM report_categories ORDER BY board ASC"; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $query = "SELECT resto, fsize, filedeleted FROM `%s` WHERE no = %d"; + + $res2 = mysql_board_call($query, $board, $post_id); + + if (!$res2) { + return false; + } + + $post = mysql_fetch_assoc($res2); + + if (!$post) { + return false; + } + + $is_op = !$post['resto']; + $has_image = $post['fsize'] && !$post['filedeleted']; + + // ID of the category which will be used for the Illegal radio button + $illegal_cat_id = 31; + + // Rule violations + one illegal category + $data = array('rule' => null, 'illegal' => null); + + // Sorting, board specific categories go on top + $data_rule_top = array(); + $data_rule_bottom = array(); + + $match_board = ',' . $board . ','; + + while ($cat = mysql_fetch_assoc($res)) { + if ($cat['id'] == $illegal_cat_id) { + $data['illegal'] = $cat; + continue; + } + + if ($cat['board'] !== '') { + if ($cat['board'] === '_ws_') { + if (!$is_worksafe) { + continue; + } + } + else if ($cat['board'] === '_nws_') { + if ($is_worksafe) { + continue; + } + } + else if ($cat['board'] !== $board) { + continue; + } + } + + if ($cat['op_only'] && !$is_op) { + continue; + } + + if ($cat['reply_only'] && $is_op) { + continue; + } + + if ($cat['image_only'] && !$has_image) { + continue; + } + + if ($cat['exclude_boards'] && strpos(",{$cat['exclude_boards']},", $match_board) !== false) { + continue; + } + + if ($cat['board']) { + $data_rule_top[$cat['id']] = $cat; + } + else { + $data_rule_bottom[$cat['id']] = $cat; + } + } + + $data['rule'] = $data_rule_top + $data_rule_bottom; + + return $data; +} + +/** + * Checks if the report should have a different priority + * based on the number of cleared reports in the past X days and ban history. + */ +function is_report_filtered($filter_thres, $ip, $long_ip, $pass_id = null, $pwd = null) { + if ($filter_thres < 1) { + return false; + } + + // only count reports made in the past X days + $cleared_days_lim = 2; + // number of cleared reports for the IP to be considered 'abusive' + $cleared_count_lim = (int)$filter_thres; + // only count bans made in the past X days + $ban_days_lim = 30; + // number of bans/warnings for the IP to be considered 'abusive' + $ban_count_lim = 3; + + $rep_abuse_tpl = 190; // ban template for report abusing + + $long_ip = (int)$long_ip; + + $ban_clauses = array(); + $rep_clauses = array(); + + // 4chan Pass + if ($pass_id) { + $pass_id_sql = mysql_real_escape_string($pass_id); + $ban_clauses[] = "4pass_id = '$pass_id_sql'"; + $rep_clauses[] = "pass_id = '$pass_id_sql'"; + + $pwd_and_ban = "4pass_id != '$pass_id_sql'"; + $pwd_and_rep = "pass_id != '$pass_id_sql'"; + } + // IP + else { + $ip_sql = mysql_real_escape_string($ip); + $ban_clauses[] = "host = '$ip_sql'"; + $rep_clauses[] = "long_ip = $long_ip"; + + $pwd_and_ban = "host != '$ip_sql'"; + $pwd_and_rep = "long_ip != $long_ip"; + } + + // Password + if ($pwd) { + $pwd_sql = mysql_real_escape_string($pwd); + $ban_clauses[] = "password = '$pwd_sql' AND $pwd_and_ban"; + $rep_clauses[] = "pwd = '$pwd_sql' AND $pwd_and_rep"; + } + + // --- + // Check cleared reports + // --- + $clear_count = 0; + + foreach ($rep_clauses as $clause) { + $query = << DATE_SUB(NOW(), INTERVAL $cleared_days_lim DAY) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $clear_count += (int)mysql_fetch_row($res)[0]; + + if ($clear_count >= $cleared_count_lim) { + return true; + } + } + + // --- + // Check ban history + // --- + $ban_count = 0; + + foreach ($ban_clauses as $clause) { + $query = << DATE_SUB(NOW(), INTERVAL $ban_days_lim DAY) +SQL; + + $res = mysql_global_call($query); + + if (!$res) { + return false; + } + + $ban_count += (int)mysql_fetch_row($res)[0]; + + if ($ban_count >= $ban_count_lim) { + return true; + } + } + + return false; +} + +function report_get_rel_sub($board, $thread_id) { + if (!$board || !$thread_id) { + return ''; + } + + $thread_id = (int)$thread_id; + + $query = "SELECT sub FROM `%s` WHERE no = $thread_id"; + + $res = mysql_board_call($query, $board); + + if (!$res || mysql_num_rows($res) !== 1) { + return ''; + } + + return mysql_fetch_row($res)[0]; +} + +function report_submit($board, $no, $cat_id) { + global $log, $passid; + + $board = mysql_real_escape_string($board); + $no = (int)$no; + $long_ip = ip2long($_SERVER['REMOTE_ADDR']); + + // check if the category is valid + $cats = get_report_categories($board, $no, DEFAULT_BURICHAN == 1); + + if ($cats['illegal']['id'] == $cat_id) { + $old_cat = 2; // todo: remove later + $old_field = 'num_illegal'; // todo: remove later + $rep_cat = $cats['illegal']; + } + else if (isset($cats['rule'][$cat_id])) { + $old_cat = 1; + $old_field = 'num_rule'; + $rep_cat = $cats['rule'][$cat_id]; + } + else { + fancydie('Invalid category selected.'); + } + + if (!$no) { + fancydie(S_POST_DEAD); + } + + log_cache(0, $no, 2); + + if ($log[$no]['archived']) { + $extra = array('archived' => 1); + } + else { + $extra = array(); + } + + $resto = (int)$log[$no]['resto']; + + $post_data = generate_post_json($log[$no], $log[$no]['resto'] ? $log[$no]['resto'] : $no, $extra); + + if ($log[$no]['resto']) { + $rel_sub = report_get_rel_sub($board, $log[$no]['resto']); + + if ($rel_sub !== '') { + $post_data['rel_sub'] = $rel_sub; + } + } + + $json = json_encode($post_data); + + $weight = $rep_cat['weight']; + + $is_staff = has_level('janitor'); + + $req_sig = spam_filter_get_req_sig(); + + $userpwd = UserPwd::getSession(); + + if ($userpwd) { + $pwd = $userpwd->getPwd(); + $is_new_pwd = $userpwd->isNew(); + $is_known_pwd = $userpwd->isUserKnownOrVerified(60); + } + else { + $pwd = null; + $is_new_pwd = true; + $is_known_pwd = false; + } + + if (!$is_staff) { + $ignore_reason = 0; + + $_threat_score = spam_filter_get_threat_score(null, true, false); + + if (!$is_known_pwd) { + $ignore_reason = 1; + } + else if ($_threat_score >= 0.4) { + $ignore_reason = 2; + } + else if ($rep_cat['filtered']) { + if (is_report_filtered($rep_cat['filtered'], $_SERVER['REMOTE_ADDR'], $long_ip, $passid, $is_new_pwd ? null : $pwd)) { + $ignore_reason = 3; + } + } + } + + if ($ignore_reason > 0) { + $weight = 0.5; + if ($ignore_reason == 2) { + $_bot_headers = spam_filter_format_http_headers($log[$no]['com'], '', '', $_threat_score, $req_sig); + log_spam_filter_trigger('ignore_report_score', BOARD_DIR, $no, $_SERVER['REMOTE_ADDR'], $ignore_reason, $_bot_headers); + } + } + + // Check if the post was already reported and cleared + $is_cleared = 0; + $cleared_by = ''; + + $query = "SELECT cleared_by FROM reports WHERE board = '$board' AND no = $no AND cleared = 1 LIMIT 1"; + + $res = mysql_global_call($query); + + if ($res) { + $row = mysql_fetch_row($res); + + if ($row) { + $is_cleared = 1; + $cleared_by = $row[0]; + log_cleared_reporter($long_ip, $pwd, $passid, $rep_cat['id'], $weight); + } + } + + $is_ws = DEFAULT_BURICHAN == 1 ? 1 : 0; + + $query = <<= num_rule, 2, 1) +SQL; + + $res = mysql_global_call($query); + + report_log_action($board, $no); + + if ($userpwd) { + $userpwd->updateReportActivity(); + $userpwd->setCookie('.' . MAIN_DOMAIN); + } + + fancydie('Report submitted! This window will close in 3 seconds...', 1); +} diff --git a/plugins/broomcloset.php b/plugins/broomcloset.php new file mode 100644 index 0000000..596039c --- /dev/null +++ b/plugins/broomcloset.php @@ -0,0 +1,161 @@ + 0 ) die( 'Config PAGE_MAX should be 0!' ); + + +/* register_callback('mode_default_case', 'broomcloset_mode'); + register_callback('regist_before', 'broomcloset_regist'); + register_callback('trim_db_before', 'broomcloset_trim'); + register_callback('head_before', 'broomcloset_head'); + register_callback('form_after', 'broomcloset_form'); + register_callback('post_before', 'broomcloset_post'); + register_callback('capcode', 'broomcloset_capcode'); +*/ + +// add the 'latest' mode +function broomcloset_latest() +{ + //if (!valid('janitor_board')) die(''); + $query = mysql_board_call( "SELECT * FROM `" . SQLLOG . "` ORDER BY no DESC LIMIT 1" ); + if( $row = mysql_fetch_assoc( $query ) ) { + foreach( $row as &$val ) $val = addslashes( $val ); + echo <<Name', '', $dat ); + $newform = str_replace( 'E-mail', '', $newform ); + $newform = str_replace( '[]', '', $newform ); + $newform = str_replace( 'name=sub size="35">', 'name=sub size="35">', $newform ); // move admin ext. placeholders next to subject + return $newform; +} + +// this function is last because it screws up syntax coloring in my editor :( +function broomcloset_head( $dat ) +{ + $dat .= <<<'BUTTCODE' + +BUTTCODE; + + return $dat; +} diff --git a/plugins/enhance_q.php b/plugins/enhance_q.php new file mode 100644 index 0000000..d264329 --- /dev/null +++ b/plugins/enhance_q.php @@ -0,0 +1,42 @@ + '', 'developer' => '', 'mod' => '', 'manager' => ''); + + while( list( $resrow ) = each( $resline ) ) { + + if( !$log[ $resrow ][ 'no' ] ) { + break; + } + + if( $log[ $resrow ][ 'capcode' ] === 'none' ) { + continue; + } + + $capcode = ( $log[ $resrow ][ 'capcode' ] == 'admin_highlight' ) ? 'admin' : $log[$resrow]['capcode']; + $no = $log[$resrow]['no']; + + $posts[$capcode] .= "$no,"; + + } + + unset( $posts['none'] ); + + foreach( $posts as $key => $value ) { + if( $posts[$key] != '' ) { + $posts[$key] = substr($posts[$key], 0, -1); + } + } + + + return array( $posts['admin'], $posts['developer'], $posts['mod'], $posts['manager'] ); +} +?> \ No newline at end of file diff --git a/plugins/robot9000.php b/plugins/robot9000.php new file mode 100644 index 0000000..379726f --- /dev/null +++ b/plugins/robot9000.php @@ -0,0 +1,250 @@ +>num) and duplicate(md5) detection. +# It doesn't know about bans, so those need to be done seperately, but it +# doesn't care if that is before or after. +# It really should run after valid file checks (jpg/png/gif, >0x0, etc) but that's not critical. +# +# Synchronization: There is (in theory) a minor race condition because the tables are not locked. +# It's not exploitable for any useful purpose, and it's blocked by the floodcheck +# +# Changelog: +# 2008/02/20 04:20: Added changelog, fixed $txt error that killed all posts +# 2008/02/20 04:56: Fixed the signal-ratio filter to handle the stupid HTML +# 2008/02/20 05:21: Added a check for repeated characters +# 2008/02/20 07:05: Added a check for long spams +# 2008/02/20 13:02: Rearranged the filters for better results. +# 2008/02/20 23:58: Fixed a bug that broke posts with two quotes far apart +# 2008/02/21 01:57: Fixed a dumb bug with the number filter.. +# 2008/02/21 02:06: Adding content-percentage info to the content filter. +# 2008/02/21 02:15: Adjusted long-text filter. +# 2008/02/21 02:18: Removed long-text filter. +# 2008/02/22 16:43: Added mute-expiring. +# 2008/02/22 17:54: Fixed mute-expiring. +# 2008/02/22 18:13: Added #nextnow and #muteinfo secret mod capcodes +# 2008/02/22 18:21: Fixed #muteinfo for mods. +# 2015/10/24 16:36: Cleanup the code and put the robot back. +# $email, $sub, $name fields aren't used anymore. +# removed $mod parameter. +# 2020/11/16 08:09: Update text hashes for every post to prune stale entries + +define('R9K_SIGNAL_RATIO', 0.1); +define('R9K_MAX_DURATION', 31536000); // one year +define('R9K_DATE_FORMAT', '%m/%d/%y %H:%M:%S'); +define('R9K_DEMUTE_PERIOD', 86400); // one day +define('R9K_SNR_MIN_LEN', 10); // minimum txt length for signal ratio check + +define('R9K_OK', 'OK'); +define('R9K_DB_ERROR', 'Database error.'); +define('R9K_EMPTY_COM', 'Textless posts are not allowed.'); +define('R9K_ASCII_ONLY', 'Non-ASCII text is not allowed.'); +define('R9K_MUTED', "You're muted! You cannot post until %s, %s from now"); +define('R9K_MUTE_ERROR', "You have been muted for %s, because %s"); +define('R9K_LOW_SNR', 'your comment was too low in content (%0.2f%% content).'); +define('R9K_DUP_TXT', 'your comment was not original.'); +define('R9K_DUP_IMG', 'your image was not original.'); + +function r9k_process($com, $md5, $ip) { + // Blank file + if ($md5 == 'd41d8cd98f00b204e9800998ecf8427e') { + $md5 = null; + } + + if ($com === ''){ + return R9K_EMPTY_COM; + } + + if (preg_match('/[\\x80-\\xFF]/', $com)) { + return R9K_ASCII_ONLY; + } + + $table_mutes = ROBOT9000_MUTES; + $table_posts = ROBOT9000_POSTS; + + $ip = (int)$ip; + + $mute = false; + $demute = false; + $timeout_power = 0; + + $query = << $now) { + $duration = r9k_pretty_duration($row['mute_until'] - $now); + $when = strftime(R9K_DATE_FORMAT, $row['mute_until']); + return sprintf(R9K_MUTED, $when, $duration); + } + + if ($row['next_expire'] < $now){ + $demute = true; + } + } + + $txt = strtolower($com); + + // Strip HTML + $stxt=preg_replace('/<.*?>/s','', $txt); + + // Original byte length + $olength = strlen($stxt); + + // Strip >>123 quotelinks + $stxt = preg_replace('/>>\d+/', '', $stxt); + + // Strip html entities + $stxt = preg_replace('/&#?\w+;/', '', $stxt); + + // Strip non-alnum chars + $stxt = preg_replace('/[^a-z\d-]+/', '', $stxt); + + // Trim leading and trailing numeric characters + $stxt = preg_replace('/^\d*(.*)\d*$/', '\1', $stxt); + + // Compress repeated characters: aaa -> a + $stxt = preg_replace('/(.)\\1{2,}/', '\\1', $stxt); + + // Check signal ratio + if (strlen($txt) > R9K_SNR_MIN_LEN) { + $ratio = strlen($stxt) / $olength; + + if ($ratio < R9K_SIGNAL_RATIO) { + $mute = sprintf(R9K_LOW_SNR, $ratio * 100.0); + } + } + + if ($mute === false) { + $txt_hash = md5($stxt); + + // Check if hashes match + $query = "SELECT text, image FROM `$table_posts` WHERE text = '%s'"; + /* + if ($md5) { + $query .= " OR image = '%s'"; + $res = mysql_board_call($query, $txt_hash, $md5); + } + else {*/ + $res = mysql_board_call($query, $txt_hash); + //} + + if (!$res) { + //return R9K_OK; + return R9K_DB_ERROR; + } + + // Post is good. Insert hashes. + if (mysql_num_rows($res) < 1) { + $query = "INSERT INTO `$table_posts` (text) VALUES('%s')"; + mysql_board_call($query, $txt_hash); + } + // Duplicates found. + else { + //$row = mysql_fetch_assoc($res); + + //if ($row['text'] === $txt_hash) { + $mute = R9K_DUP_TXT; + //} + //else if ($md5 && $row['image'] === $md5) { + // $mute = R9K_DUP_IMG; + //} + + // Update the hash with a new timestamp + $query = "UPDATE `$table_posts` SET created_on = NOW() WHERE text = '%s' LIMIT 1"; + mysql_board_call($query, $txt_hash); + } + } + + // Muted + if ($mute !== false) { + ++$timeout_power; + + $mute_duration = pow(2, $timeout_power); + + if ($mute_duration > R9K_MAX_DURATION) { + $timeout_power--; + $mute_duration = R9K_MAX_DURATION; + } + + $next_expire = R9K_DEMUTE_PERIOD; + + $query = << 0, timeout_power - 1, 0), +next_expire = DATE_ADD(NOW(), INTERVAL $next_expire SECOND) +WHERE ip = $ip +SQL; + + $res = mysql_board_call($query); + } + + return R9K_OK; + } +} + +function r9k_pretty_duration($secs){ + $w = (int)($secs / 604800); + $d = (int)($secs / 86400) % 7; + $h = (int)($secs / 3600) % 24; + $m = ((int)($secs / 60)) % 60; + $s = ((int)$secs) % 60; + $out = array(); + $pairs = array( + array($w, 'week'), + array($d, 'day'), + array($h, 'hour'), + array($m, 'minute'), + array($s, 'second') + ); + + foreach($pairs as $v){ + if ($v[0] !== 0) { + $out[] = $v[0] . ' ' . $v[1] . ($v[0] === 1 ? '' : 's'); + } + } + + return implode(' ', $out); +} diff --git a/plugins/yotsuba_plugins.php b/plugins/yotsuba_plugins.php new file mode 100644 index 0000000..b14be94 --- /dev/null +++ b/plugins/yotsuba_plugins.php @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/rebuildd-test.php b/rebuildd-test.php new file mode 100644 index 0000000..fb9fdb7 --- /dev/null +++ b/rebuildd-test.php @@ -0,0 +1,130 @@ + $lastno) { + //may not be indexed... + list($newthreadno) = mysql_fetch_row(mysql_board_call("select max(no) from `".SQLLOG."` where resto=0 and archived=0")); + $do_trim_db = $newthreadno > $lastthreadno; + + $lastthreadno = $newthreadno; + $lastno = $newlastno; + $ret = 1; + } + + echo "new thread $lastthreadno, new last $lastno, shouldRebuild $ret\n"; + return $ret; +} + +function rebuildd_update_index() +{ + global $do_trim_db, $count; + log_cache(1); + if ($do_trim_db) { + trim_db(); + trim_archive(); + } + //updatelog(); + rebuild_indexes_daemon(); + rpc_task(); + if( ENABLE_CATALOG && $count == CATALOG_DAEMON_REBUILD_INTERVAL ) { + generate_catalog(); + $count = 0; + } + + if( ENABLE_CATALOG ) $count++; +} + +function rebuildd_occasional_cleanup() +{ + global $rpc_mh; global $rpc_chs; + + $nrpcs = count($rpc_chs); + echo "finishing $nrpcs RPCs\n"; + + rpc_finish_all(); +} + +while (1) { + $update_start_time = microtime(true); + if (rebuildd_gate()) { + rebuildd_update_index(); + $update_end_time = microtime(true); + $update_counter++; + $work_time = $update_this_secs = $update_end_time - $update_start_time; + $update_total_secs += $update_this_secs; + $update_avg_secs = $update_total_secs / $update_counter; + $next_start_time = $update_start_time + $time_to_sleep_min; + + $to_sleep = $next_start_time - $update_end_time; + if ($to_sleep <= 0) { // if we're late, skip to the next time + rebuildd_occasional_cleanup(); + $to_sleep = $time_to_sleep_min; + $work_time = microtime(true) - $update_start_time; + } + + $sleepusec = $to_sleep * $sec_in_us; + usleep($sleepusec); + + // reset long-sleep to shortest time + $time_to_sleep = $time_to_sleep_min; + + // avoid overflow + if ($update_counter == 65536) { + $update_counter = 0; + $update_total_secs = 0.0; + } + echo "$update_end_time update counter $update_counter slept $to_sleep update took $work_time\n"; + } else { + // todo push-style rebuilds instead of polling for slower boards + rebuildd_occasional_cleanup(); + $update_end_time = microtime(true); + + $sleepusec = $time_to_sleep * $sec_in_us; + usleep($sleepusec); + $time_to_sleep = min($time_to_sleep*1.25, $time_to_sleep_max); + + $update_this_secs = $update_end_time - $update_start_time; + echo "$update_end_time update counter $update_counter slept $to_sleep cleanup took $update_this_secs\n"; + } +} +?> diff --git a/rebuildd.php b/rebuildd.php new file mode 100644 index 0000000..fb9fdb7 --- /dev/null +++ b/rebuildd.php @@ -0,0 +1,130 @@ + $lastno) { + //may not be indexed... + list($newthreadno) = mysql_fetch_row(mysql_board_call("select max(no) from `".SQLLOG."` where resto=0 and archived=0")); + $do_trim_db = $newthreadno > $lastthreadno; + + $lastthreadno = $newthreadno; + $lastno = $newlastno; + $ret = 1; + } + + echo "new thread $lastthreadno, new last $lastno, shouldRebuild $ret\n"; + return $ret; +} + +function rebuildd_update_index() +{ + global $do_trim_db, $count; + log_cache(1); + if ($do_trim_db) { + trim_db(); + trim_archive(); + } + //updatelog(); + rebuild_indexes_daemon(); + rpc_task(); + if( ENABLE_CATALOG && $count == CATALOG_DAEMON_REBUILD_INTERVAL ) { + generate_catalog(); + $count = 0; + } + + if( ENABLE_CATALOG ) $count++; +} + +function rebuildd_occasional_cleanup() +{ + global $rpc_mh; global $rpc_chs; + + $nrpcs = count($rpc_chs); + echo "finishing $nrpcs RPCs\n"; + + rpc_finish_all(); +} + +while (1) { + $update_start_time = microtime(true); + if (rebuildd_gate()) { + rebuildd_update_index(); + $update_end_time = microtime(true); + $update_counter++; + $work_time = $update_this_secs = $update_end_time - $update_start_time; + $update_total_secs += $update_this_secs; + $update_avg_secs = $update_total_secs / $update_counter; + $next_start_time = $update_start_time + $time_to_sleep_min; + + $to_sleep = $next_start_time - $update_end_time; + if ($to_sleep <= 0) { // if we're late, skip to the next time + rebuildd_occasional_cleanup(); + $to_sleep = $time_to_sleep_min; + $work_time = microtime(true) - $update_start_time; + } + + $sleepusec = $to_sleep * $sec_in_us; + usleep($sleepusec); + + // reset long-sleep to shortest time + $time_to_sleep = $time_to_sleep_min; + + // avoid overflow + if ($update_counter == 65536) { + $update_counter = 0; + $update_total_secs = 0.0; + } + echo "$update_end_time update counter $update_counter slept $to_sleep update took $work_time\n"; + } else { + // todo push-style rebuilds instead of polling for slower boards + rebuildd_occasional_cleanup(); + $update_end_time = microtime(true); + + $sleepusec = $time_to_sleep * $sec_in_us; + usleep($sleepusec); + $time_to_sleep = min($time_to_sleep*1.25, $time_to_sleep_max); + + $update_this_secs = $update_end_time - $update_start_time; + echo "$update_end_time update counter $update_counter slept $to_sleep cleanup took $update_this_secs\n"; + } +} +?> diff --git a/rid.php b/rid.php new file mode 100644 index 0000000..763f990 --- /dev/null +++ b/rid.php @@ -0,0 +1,25 @@ + diff --git a/signin-test.php b/signin-test.php new file mode 100644 index 0000000..f6b16eb --- /dev/null +++ b/signin-test.php @@ -0,0 +1,1027 @@ +mode = 'error'; + $this->msg = $msg; + $this->renderHTML('signin-test'); + die(); + } + + final protected function error_generic($code) { + $this->error(sprintf(self::ERR_GENERIC, $code)); + } + + final protected function error_cooldown($units_left, $units = 'minute') { + if ($units_left > 1) { + $units .= 's'; + } + + $this->error(sprintf(self::ERR_CD, $units_left, $units)); + } + + private function log_event($event_id, $token) { + $sql = <<error(self::ERR_COOKIES); + } + + if ($_COOKIE[$arg] !== $_POST[$arg]) { + $this->error(self::ERR_COOKIES); + } + } + + private function is_email_blacklisted($email) { + $tbl = self::TBL_BLACKLIST; + + $sql = "SELECT 1 FROM `$tbl` WHERE email = '%s' LIMIT 1"; + + $res = mysql_global_call($sql, $email); + + if (!$res) { + return false; + } + + return mysql_num_rows($res) === 1; + } + + private function get_bot_score() { + if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { + return 100; + } + + return (int)$_SERVER['HTTP_X_BOT_SCORE']; + } + + private function get_token_bot_score($token) { + $tbl = self::TBL; + + $sql =<<error_generic('vf'); + } + + $tbl = self::TBL; + $tbl_queue = self::TBL_QUEUE; + + // --- + // Base cooldown for IP + // --- + $query =<<error(self::ERR_DB); + } + + $last_ts = (int)mysql_fetch_row($res)[0]; + + $delta = $now - $last_ts; + + if ($delta < self::REQ_CD) { + $cd = ceil($delta / 60.0); + $this->error_cooldown($cd); + } + + // --- + // Check if the email is already in the sending queue + // --- + $query = "SELECT 1 FROM `$tbl_queue` WHERE email = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $email); + + if (!$res) { + $this->error(self::ERR_DB); + } + + if (mysql_num_rows($res) > 0) { + $this->error(self::ERR_EMAIL_QUEUED); + } + + // --- + // Check repeated requests for IP + // --- + $_ip_per_hour = (int)self::REQ_CD_IP_PER_HOUR; + $cd = 3600; + + $query =<< DATE_SUB(NOW(), INTERVAL 1 HOUR) +ORDER BY created_on ASC +LIMIT $_ip_per_hour +SQL; + + $res = mysql_global_call($query, $ip); + + if (!$res) { + $this->error(self::ERR_DB); + } + + if (mysql_num_rows($res) == $_ip_per_hour) { + $last_ts = (int)mysql_fetch_row($res)[0]; + $cd = ceil(($last_ts - $now + $cd) / 60.0); + $this->error_cooldown($cd); + } + + // --- + // Check repeated requests for email + // --- + $_email_per_day = (int)self::REQ_CD_MAIL_PER_DAY; + $cd = 86400; + + $query =<< DATE_SUB(NOW(), INTERVAL 1 DAY) +ORDER BY created_on ASC +LIMIT $_email_per_day +SQL; + + $res = mysql_global_call($query, $hashed_email); + + if (!$res) { + $this->error(self::ERR_DB); + } + + if (mysql_num_rows($res) == $_email_per_day) { + $last_ts = (int)mysql_fetch_row($res)[0]; + $cd = ($last_ts - $now + $cd) / 60.0; + + if ($cd > 60) { + $cd = $cd / 60.0; + $units = 'hour'; + } + else { + $units = 'minute'; + } + + $cd = ceil($cd); + + $this->error_cooldown($cd, $units); + } + } + + private function is_valid_captcha_t() { + require_once 'lib/captcha.php'; + + $m = new Memcached(); + //$m->setOption(Memcached::OPT_TCP_NODELAY, true); + $m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); + $m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms + $m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms + $m->addServer('localhost', 11211); + + return is_twister_captcha_valid($m, $_SERVER['REMOTE_ADDR'], null, '!signin', 1, $_uc); + } + + private function validate_captcha($force_recaptcha = false) { + if (!defined('RECAPTCHA_API_KEY_PRIVATE')) { + $this->error_generic('nck'); + } + + if (!isset($_POST["g-recaptcha-response"])) { + $this->error(self::ERR_BAD_CAPTCHA); + } + + $response = $_POST["g-recaptcha-response"]; + + if (!$response || strlen($response) > 4096) { + $this->error(self::ERR_BAD_CAPTCHA); + } + + if (self::CAPTCHA_MODE === 2 && !$force_recaptcha) { + $url = 'https://hcaptcha.com/siteverify'; + $captcha_private_key = HCAPTCHA_API_KEY_PRIVATE; + $captcha_public_key = HCAPTCHA_API_KEY_PUBLIC; + } + else { + $url = 'https://www.google.com/recaptcha/api/siteverify'; + $captcha_private_key = RECAPTCHA_API_KEY_PRIVATE; + $captcha_public_key = null; + } + + $post = array( + 'secret' => $captcha_private_key, + 'response' => $response, + 'remoteip' => $_SERVER['REMOTE_ADDR'] + ); + + if ($captcha_public_key) { + $post['sitekey'] = $captcha_public_key; + } + + $curl = curl_init(); + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2); + curl_setopt($curl, CURLOPT_TIMEOUT, 4); + curl_setopt($curl, CURLOPT_USERAGENT, '4chan'); + curl_setopt($curl, CURLOPT_POSTFIELDS, $post); + + $resp = curl_exec($curl); + + if ($resp === false) { + curl_close($curl); + $this->error_generic('cne0'); + } + + $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($resp_status >= 300) { + curl_close($curl); + $this->error_generic('cne1'); + } + + curl_close($curl); + + $json = json_decode($resp, true); + + // BAD + if (json_last_error() !== JSON_ERROR_NONE) { + $this->error(self::ERR_BAD_CAPTCHA); + } + + // GOOD + if ($json && isset($json['success']) && $json['success']) { + return true; + } + + // BAD + $this->error(self::ERR_BAD_CAPTCHA); + } + + private function get_random_hex_bytes($length = 64) { + $data = openssl_random_pseudo_bytes($length); + + if (!$data) { + return false; + } + + return bin2hex($data); + } + + private function generate_token() { + return $this->get_random_hex_bytes(self::TOKEN_BYTES); + } + + private function set_ev1_cookie($flag = true) { + $cookie_name = '_ev1'; + + if ($flag) { + setcookie($cookie_name, '1', $_SERVER['REQUEST_TIME'] + 60, '/', '.' . self::PWD_DOMAIN, true, false); + } + else { + setcookie($cookie_name, '', -1, '/', '.' . self::PWD_DOMAIN, true, false); + } + } + + private function prune_old_requests() { + $tbl = self::TBL; + $ttl = (int)self::PRUNE_DAYS; + $sql = "DELETE FROM `$tbl` WHERE created_on <= DATE_SUB(NOW(), INTERVAL $ttl DAY)"; + return mysql_global_call($sql); + } + + private function validate_email($email) { + if (!preg_match('/^[^@]+@[^@]+\.[a-z]+$/', $email)) { + $this->error(self::ERR_BAD_EMAIL); + } + + if (strpos($email, '+') !== false) { + $this->error(self::ERR_BAD_EMAIL_PLUS); + } + + if (self::VERIFY_EMAIL_DOMAIN) { + $flag = false; + + $domain = explode('@', $email)[1]; + + if (!$domain) { + $this->error(self::ERR_BAD_EMAIL); + } + + foreach (self::$allowed_domains as $allowed_domain) { + if ($allowed_domain === $domain) { + $flag = true; + break; + } + } + + if (!$flag) { + $this->error(self::ERR_BAD_EMAIL_DOMAIN); + } + } + + return true; + } + + private function is_pwd_banned($pwd) { + if (!$pwd) { + return false; + } + + $sql =<< NOW() +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $pwd); + + if (!$res) { + return false; + } + + return (int)mysql_num_rows($res) > 0; + } + + private function validate_rangeban() { + $long_ip = ip2long($_SERVER['REMOTE_ADDR']); + + if (!$long_ip) { + $this->error_generic('vri'); + } + + $asn = 0; + + if (isset($_SERVER['HTTP_X_GEO_ASN'])) { + $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; + } + else { + $_asninfo = GeoIP2::get_asn($ip); + + if ($_asninfo) { + $asn = (int)$_asninfo['asn']; + } + } + + $now = (int)$_SERVER['REQUEST_TIME']; + + $perma_clause =<<= $long_ip AND active = 1 +AND $perma_clause) +SQL; + + if ($asn > 0) { + $query .= <<error(self::ERR_DB); + } + + if ((int)mysql_num_rows($res) > 0) { + $this->error(self::ERR_RANGEBAN); + } + } + + private function get_country() { + static $country = null; + + if ($country !== null) { + return $country; + } + + if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { + $country = $_SERVER['HTTP_X_GEO_COUNTRY']; + } + else { + $geo_data = GeoIP2::get_country($_SERVER['REMOTE_ADDR']); + + if ($geo_data && isset($geo_data['country_code'])) { + $country = $geo_data['country_code']; + } + else { + $country = 'XX'; + } + } + + return $country; + } + + private function get_user_agent() { + $ua = $_SERVER['HTTP_USER_AGENT']; + + if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && $_SERVER['HTTP_SEC_CH_UA_MODEL'] && $_SERVER['HTTP_SEC_CH_UA_MODEL'] != '""') { + $model = $_SERVER['HTTP_SEC_CH_UA_MODEL']; + $ua .= " ~[$model]"; + } + + return $ua; + } + + private function normalize_gmail_address($email) { + list($user, $domain) = explode('@', $email); + $user = preg_replace('/\./', '', $user); + return "$user@$domain"; + } + + private function hash_email($email) { + $hmac_key = file_get_contents(self::HMAC_KEY_PATH); + + if (!$hmac_key) { + return false; + } + + $hashed_email = substr(hash_hmac('sha256', $email, $hmac_key, true), 0, self::PWD_BYTES); + + return bin2hex($hashed_email); + } + + private function get_domain($email) { + $parts = explode('@', $email); + + if (count($parts) !== 2) { + return ''; + } + + return $parts[1]; + } + + private function create_request($email, $hashed_email) { + $tbl = self::TBL; + $tbl_queue = self::TBL_QUEUE; + + $token = $this->generate_token(); + + if (!$token) { + $this->error_generic('gt'); + } + + if (!$email || !$hashed_email) { + $this->error_generic('crne'); + } + + $ip = $_SERVER['REMOTE_ADDR']; + + $ua = $this->get_user_agent(); + + $country = $this->get_country(); + + if ($country === 'T1') { + $this->error(self::ERR_RANGEBAN); + } + + $domain = $this->get_domain($email); + + if (isset($_SERVER['HTTP_X_BOT_SCORE'])) { + $bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + } + else { + $bot_score = 0; + } + + // Insert the request + $sql =<<error_generic('cr2'); + } + + // Add the email to the mailer job queue + $sql =<<error_generic('crq'); + } + + return $token; + } + + private function register_captcha_failure($token) { + $tbl = self::TBL; + + if (!$token) { + return false; + } + + $sql = "UPDATE `$tbl` SET failed_challenges = failed_challenges + 1 WHERE token = '%s' LIMIT 1"; + + mysql_global_call($sql, $token); + } + + private function get_captcha_failures($token) { + $tbl = self::TBL; + + if (!$token) { + return 0; + } + + $sql = "SELECT failed_challenges FROM `$tbl` WHERE token = '%s' LIMIT 1"; + + $res = mysql_global_call($sql, $token); + + if (!$res) { + return 0; + } + + return (int)mysql_fetch_row($res)[0]; + } + + public function request() { + $this->mode = 'request'; + + $this->validate_csrf(); + + if (!isset($_POST['email']) || !$_POST['email']) { + $this->error(self::ERR_BAD_EMAIL); + } + + $email = strtolower(trim($_POST['email'])); + + $this->validate_email($email); + + if ($this->get_domain($email) == 'gmail.com') { + $clean_email = $this->normalize_gmail_address($email); + } + else { + $clean_email = $email; + } + + $hashed_email = $this->hash_email($clean_email); + + if (!$hashed_email) { + $this->error_generic('nhe'); + } + + $this->validate_cooldowns($email, $hashed_email); + + $this->validate_captcha(); + + $this->validate_rangeban(); + + if (isset($_COOKIE['4chan_pass'])) { + $pwd = UserPwd::decodePwd($_COOKIE['4chan_pass']); + + // Password has a ban, show fake success screen + if ($pwd && $this->is_pwd_banned($pwd)) { + return $this->renderHTML('signin-test'); + } + } + + // Email is blacklisted, show fake success screen + if ($this->is_email_blacklisted($email)) { + $this->log_event(self::EVT_BLACKLISTED, $hashed_email); + return $this->renderHTML('signin-test'); + } + + $token = $this->create_request($email, $hashed_email); + + $this->renderHTML('signin-test'); + } + + private function pre_verify() { + if (!isset($_GET['tkn'])) { + $this->error(self::ERR_BAD_REQUEST); + } + + $this->token = trim($_GET['tkn']); + + if (!$this->token) { + $this->error(self::ERR_BAD_REQUEST); + } + + $_token_bot_score = $this->get_token_bot_score($this->token); + + if (false && $_token_bot_score < 90) { + $this->use_recaptcha = true; + } + else { + $this->use_recaptcha = false; + } + + $this->csrf_token = $this->get_csrf_token(); + + setcookie(self::CSRF_ARG, $this->csrf_token, 0, '/', $_SERVER['HTTP_HOST'], true, true); + + $this->renderHTML('signin-test'); + } + + public function verify() { + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + $this->mode = 'verify'; + return $this->pre_verify(); + } + else { + $this->mode = 'verify-done'; + } + + $this->validate_csrf(); + + if (!isset($_POST['tkn'])) { + $this->error(self::ERR_BAD_REQUEST); + } + + $token = trim($_POST['tkn']); + + if (!$token) { + $this->error(self::ERR_BAD_REQUEST); + } + + if (isset($_COOKIE['pass_enabled']) && $_COOKIE['pass_enabled']) { + $this->error(self::ERR_PASS_USER); + } + + $_bot_score = $this->get_bot_score(); + $_token_bot_score = $this->get_token_bot_score($token); + + if ($_bot_score < self::BOT_SCORE_SUSP || $_token_bot_score < self::BOT_SCORE_SUSP) { + $_max_fails = self::MAX_CAPTCHA_FAILURES_SUSP; + } + else { + $_max_fails = self::MAX_CAPTCHA_FAILURES; + } + + if ($_token_bot_score < self::BOT_SCORE_SUSP) { + $this->use_recaptcha = true; + } + else { + $this->use_recaptcha = false; + } + + if ($this->get_captcha_failures($token) >= $_max_fails) { + $this->error(self::ERR_BAD_LINK); + } + + if ($this->use_recaptcha == false) { + if ($this->is_valid_captcha_t() !== true) { + $this->register_captcha_failure($token); + $this->mode = 'verify-captcha-failed'; + $this->token = $token; + $this->renderHTML('signin-test'); + die(); + } + } + else { + $this->validate_captcha(true); + } + + $this->prune_old_requests(); + + $this->update_usage_count($token); + + $tbl = self::TBL; + + $ttl = (int)self::VERIFY_TOKEN_TTL; + + $sql =<< DATE_SUB(NOW(), INTERVAL $ttl SECOND) +SQL; + + $res = mysql_global_call($sql, $token); + + if (!$res) { + $this->error(self::ERR_DB); + } + + $request = mysql_fetch_assoc($res); + + if (!$request) { + $this->error(self::ERR_BAD_LINK); + } + + if (!$request['hashed_email']) { + $this->error_generic('hee'); + } + + // Validate usage count + if ((int)$request['used'] > self::TOKEN_MAX_USAGES) { + $this->log_event(self::EVT_MAX_USED, $request['token']); + $this->error(self::ERR_BAD_LINK); + } + + // Country must match the requester's country + $country = $this->get_country(); + + if ($country !== $request['country']) { + $this->log_event(self::EVT_BAD_COUNTRY, $request['token']); + $this->error(self::ERR_BAD_LINK); + } + + $ip = $_SERVER['REMOTE_ADDR']; + + $userpwd = null; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($ip, self::PWD_DOMAIN, $_COOKIE['4chan_pass']); + + if (!$userpwd) { + $this->error_generic('nup'); + } + + // Password has a ban, show fake success screen + if ($this->is_pwd_banned($userpwd->getPwd())) { + return $this->renderHTML('signin-test'); + } + + // If already verified, make a brand new pwd + if ($userpwd->verifiedLevel() > 0) { + $userpwd = null; + } + } + + if (!$userpwd) { + $userpwd = new UserPwd($ip, self::PWD_DOMAIN); + + if (!$userpwd) { + $this->error_generic('nupn'); + } + } + + $userpwd->setPwd($request['hashed_email']); + $userpwd->setVerifiedLevel(self::VERIFIED_LEVEL); + + $userpwd->setCookie('.' . self::PWD_DOMAIN); + + // This is to let currently running cooldowns that the email was verified + $this->set_ev1_cookie(true); + + $this->renderHTML('signin-test'); + } + + /** + * Signout - deletes the password cookie + */ + public function signout() { + $this->mode = 'signout'; + + $this->validate_csrf(); + + $userpwd = null; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], self::PWD_DOMAIN, $_COOKIE['4chan_pass']); + } + + // If already verified, make a brand new pwd + if ($userpwd && $userpwd->verifiedLevel() > 0) { + setcookie(UserPwd::COOKIE_NAME, '', -1, '/', '.' . self::PWD_DOMAIN, true, true); + } + + $this->set_ev1_cookie(false); + + $this->renderHTML('signin-test'); + } + + /** + * Index + */ + public function index() { + $this->mode = 'index'; + + if (isset($_COOKIE['pass_enabled']) && $_COOKIE['pass_enabled']) { + $this->pass_user = true; + return $this->renderHTML('signin-test'); + } + + $this->pass_user = false; + + $this->csrf_token = $this->get_csrf_token(); + + $domain = $_SERVER['HTTP_HOST']; + + $userpwd = null; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], self::PWD_DOMAIN, $_COOKIE['4chan_pass']); + } + + $this->authed = $userpwd && $userpwd->verifiedLevel() > 0; + + setcookie(self::CSRF_ARG, $this->csrf_token, 0, '/', $domain, true, true); + + $this->renderHTML('signin-test'); + } + + /** + * Main + */ + public function run() { + $method = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET; + + if (isset($method['action'])) { + $action = $method['action']; + } + else { + $action = 'index'; + } + + if (in_array($action, $this->actions)) { + $this->$action(); + } + else { + $this->error('Bad request'); + } + } +} + +$ctrl = new App(); +$ctrl->run(); diff --git a/signin.php b/signin.php new file mode 100644 index 0000000..aea58a6 --- /dev/null +++ b/signin.php @@ -0,0 +1,1041 @@ +mode = 'error'; + $this->msg = $msg; + $this->renderHTML('signin'); + die(); + } + + final protected function error_generic($code) { + $this->error(sprintf(self::ERR_GENERIC, $code)); + } + + final protected function error_cooldown($units_left, $units = 'minute') { + if ($units_left > 1) { + $units .= 's'; + } + + $this->error(sprintf(self::ERR_CD, $units_left, $units)); + } + + private function log_event($event_id, $token) { + $sql = <<error(self::ERR_COOKIES); + } + + if ($_COOKIE[$arg] !== $_POST[$arg]) { + $this->error(self::ERR_COOKIES); + } + } + + private function is_email_blacklisted($email) { + $tbl = self::TBL_BLACKLIST; + + $sql = "SELECT 1 FROM `$tbl` WHERE email = '%s' LIMIT 1"; + + $res = mysql_global_call($sql, $email); + + if (!$res) { + return false; + } + + return mysql_num_rows($res) === 1; + } + + private function should_ignore_request($email) { + if (!preg_match('/@hotmail\.com/', $email)) { + return false; + } + + if (!isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) || $_SERVER['HTTP_SEC_CH_UA_MODEL'] != 'SM-S928U') { + return false; + } + + if ($this->get_bot_score() <= 70) { + return true; + } + + return false; + } + + private function get_bot_score() { + if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { + return 100; + } + + return (int)$_SERVER['HTTP_X_BOT_SCORE']; + } + + private function get_token_bot_score($token) { + $tbl = self::TBL; + + $sql =<<error_generic('vf'); + } + + $tbl = self::TBL; + $tbl_queue = self::TBL_QUEUE; + + // --- + // Base cooldown for IP + // --- + $query =<<error(self::ERR_DB); + } + + $last_ts = (int)mysql_fetch_row($res)[0]; + + $delta = $now - $last_ts; + + if ($delta < self::REQ_CD) { + $cd = ceil($delta / 60.0); + $this->error_cooldown($cd); + } + + // --- + // Check if the email is already in the sending queue + // --- + $query = "SELECT 1 FROM `$tbl_queue` WHERE email = '%s' LIMIT 1"; + + $res = mysql_global_call($query, $email); + + if (!$res) { + $this->error(self::ERR_DB); + } + + if (mysql_num_rows($res) > 0) { + $this->error(self::ERR_EMAIL_QUEUED); + } + + // --- + // Check repeated requests for IP + // --- + $_ip_per_hour = (int)self::REQ_CD_IP_PER_HOUR; + $cd = 3600; + + $query =<< DATE_SUB(NOW(), INTERVAL 1 HOUR) +ORDER BY created_on ASC +LIMIT $_ip_per_hour +SQL; + + $res = mysql_global_call($query, $ip); + + if (!$res) { + $this->error(self::ERR_DB); + } + + if (mysql_num_rows($res) == $_ip_per_hour) { + $last_ts = (int)mysql_fetch_row($res)[0]; + $cd = ceil(($last_ts - $now + $cd) / 60.0); + $this->error_cooldown($cd); + } + + // --- + // Check repeated requests for email + // --- + $_email_per_day = (int)self::REQ_CD_MAIL_PER_DAY; + $cd = 86400; + + $query =<< DATE_SUB(NOW(), INTERVAL 1 DAY) +ORDER BY created_on ASC +LIMIT $_email_per_day +SQL; + + $res = mysql_global_call($query, $hashed_email); + + if (!$res) { + $this->error(self::ERR_DB); + } + + if (mysql_num_rows($res) == $_email_per_day) { + $last_ts = (int)mysql_fetch_row($res)[0]; + $cd = ($last_ts - $now + $cd) / 60.0; + + if ($cd > 60) { + $cd = $cd / 60.0; + $units = 'hour'; + } + else { + $units = 'minute'; + } + + $cd = ceil($cd); + + $this->error_cooldown($cd, $units); + } + } + + private function is_valid_captcha_t() { + require_once 'lib/captcha.php'; + + $m = new Memcached(); + //$m->setOption(Memcached::OPT_TCP_NODELAY, true); + $m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); + $m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms + $m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms + $m->addServer('localhost', 11211); + + return is_twister_captcha_valid($m, $_SERVER['REMOTE_ADDR'], null, '!signin', 1, $_uc); + } + + private function validate_captcha($force_recaptcha = false) { + if (!defined('RECAPTCHA_API_KEY_PRIVATE')) { + $this->error_generic('nck'); + } + + if (!isset($_POST["g-recaptcha-response"])) { + $this->error(self::ERR_BAD_CAPTCHA); + } + + $response = $_POST["g-recaptcha-response"]; + + if (!$response || strlen($response) > 4096) { + $this->error(self::ERR_BAD_CAPTCHA); + } + + if (self::CAPTCHA_MODE === 2 && !$force_recaptcha) { + $url = 'https://hcaptcha.com/siteverify'; + $captcha_private_key = HCAPTCHA_API_KEY_PRIVATE; + $captcha_public_key = HCAPTCHA_API_KEY_PUBLIC; + } + else { + $url = 'https://www.google.com/recaptcha/api/siteverify'; + $captcha_private_key = RECAPTCHA_API_KEY_PRIVATE; + $captcha_public_key = null; + } + + $post = array( + 'secret' => $captcha_private_key, + 'response' => $response, + 'remoteip' => $_SERVER['REMOTE_ADDR'] + ); + + if ($captcha_public_key) { + $post['sitekey'] = $captcha_public_key; + } + + $curl = curl_init(); + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2); + curl_setopt($curl, CURLOPT_TIMEOUT, 4); + curl_setopt($curl, CURLOPT_USERAGENT, '4chan'); + curl_setopt($curl, CURLOPT_POSTFIELDS, $post); + + $resp = curl_exec($curl); + + if ($resp === false) { + curl_close($curl); + $this->error_generic('cne0'); + } + + $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($resp_status >= 300) { + curl_close($curl); + $this->error_generic('cne1'); + } + + curl_close($curl); + + $json = json_decode($resp, true); + + // BAD + if (json_last_error() !== JSON_ERROR_NONE) { + $this->error(self::ERR_BAD_CAPTCHA); + } + + // GOOD + if ($json && isset($json['success']) && $json['success']) { + return true; + } + + // BAD + $this->error(self::ERR_BAD_CAPTCHA); + } + + private function get_random_hex_bytes($length = 64) { + $data = openssl_random_pseudo_bytes($length); + + if (!$data) { + return false; + } + + return bin2hex($data); + } + + private function generate_token() { + return $this->get_random_hex_bytes(self::TOKEN_BYTES); + } + + private function set_ev1_cookie($flag = true) { + $cookie_name = '_ev1'; + + if ($flag) { + setcookie($cookie_name, '1', $_SERVER['REQUEST_TIME'] + 60, '/', '.' . self::PWD_DOMAIN, true, false); + } + else { + setcookie($cookie_name, '', -1, '/', '.' . self::PWD_DOMAIN, true, false); + } + } + + private function prune_old_requests() { + $tbl = self::TBL; + $ttl = (int)self::PRUNE_DAYS; + $sql = "DELETE FROM `$tbl` WHERE created_on <= DATE_SUB(NOW(), INTERVAL $ttl DAY)"; + return mysql_global_call($sql); + } + + private function validate_email($email) { + if (!preg_match('/^[^@]+@[^@]+\.[a-z]+$/', $email)) { + $this->error(self::ERR_BAD_EMAIL); + } + + if (strpos($email, '+') !== false) { + $this->error(self::ERR_BAD_EMAIL_PLUS); + } + + if (self::VERIFY_EMAIL_DOMAIN) { + $flag = false; + + $domain = explode('@', $email)[1]; + + if (!$domain) { + $this->error(self::ERR_BAD_EMAIL); + } + + foreach (self::$allowed_domains as $allowed_domain) { + if ($allowed_domain === $domain) { + $flag = true; + break; + } + } + + if (!$flag) { + $this->error(self::ERR_BAD_EMAIL_DOMAIN); + } + } + + return true; + } + + private function is_pwd_banned($pwd) { + if (!$pwd) { + return false; + } + + $sql =<< NOW() +LIMIT 1 +SQL; + + $res = mysql_global_call($sql, $pwd); + + if (!$res) { + return false; + } + + return (int)mysql_num_rows($res) > 0; + } + + private function validate_rangeban() { + $long_ip = ip2long($_SERVER['REMOTE_ADDR']); + + if (!$long_ip) { + $this->error_generic('vri'); + } + + $asn = 0; + + if (isset($_SERVER['HTTP_X_GEO_ASN'])) { + $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; + } + else { + $_asninfo = GeoIP2::get_asn($ip); + + if ($_asninfo) { + $asn = (int)$_asninfo['asn']; + } + } + + $now = (int)$_SERVER['REQUEST_TIME']; + + $perma_clause =<<= $long_ip AND active = 1 +AND $perma_clause) +SQL; + + if ($asn > 0) { + $query .= <<error(self::ERR_DB); + } + + if ((int)mysql_num_rows($res) > 0) { + $this->error(self::ERR_RANGEBAN); + } + } + + private function get_country() { + static $country = null; + + if ($country !== null) { + return $country; + } + + if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { + $country = $_SERVER['HTTP_X_GEO_COUNTRY']; + } + else { + $geo_data = GeoIP2::get_country($_SERVER['REMOTE_ADDR']); + + if ($geo_data && isset($geo_data['country_code'])) { + $country = $geo_data['country_code']; + } + else { + $country = 'XX'; + } + } + + return $country; + } + + private function get_user_agent() { + $ua = $_SERVER['HTTP_USER_AGENT']; + + if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && $_SERVER['HTTP_SEC_CH_UA_MODEL'] && $_SERVER['HTTP_SEC_CH_UA_MODEL'] != '""') { + $model = $_SERVER['HTTP_SEC_CH_UA_MODEL']; + $ua .= " ~[$model]"; + } + + return $ua; + } + + private function normalize_gmail_address($email) { + list($user, $domain) = explode('@', $email); + $user = preg_replace('/\./', '', $user); + return "$user@$domain"; + } + + private function hash_email($email) { + $hmac_key = file_get_contents(self::HMAC_KEY_PATH); + + if (!$hmac_key) { + return false; + } + + $hashed_email = substr(hash_hmac('sha256', $email, $hmac_key, true), 0, self::PWD_BYTES); + + return bin2hex($hashed_email); + } + + private function get_domain($email) { + $parts = explode('@', $email); + + if (count($parts) !== 2) { + return ''; + } + + return $parts[1]; + } + + private function create_request($email, $hashed_email) { + $tbl = self::TBL; + $tbl_queue = self::TBL_QUEUE; + + $token = $this->generate_token(); + + if (!$token) { + $this->error_generic('gt'); + } + + if (!$email || !$hashed_email) { + $this->error_generic('crne'); + } + + $ip = $_SERVER['REMOTE_ADDR']; + + $ua = $this->get_user_agent(); + + $country = $this->get_country(); + + if ($country === 'T1') { + $this->error(self::ERR_RANGEBAN); + } + + $domain = $this->get_domain($email); + + if (isset($_SERVER['HTTP_X_BOT_SCORE'])) { + $bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE']; + } + else { + $bot_score = 0; + } + + // Insert the request + $sql =<<error_generic('cr2'); + } + + // Add the email to the mailer job queue + $sql =<<error_generic('crq'); + } + + return $token; + } + + private function register_captcha_failure($token) { + $tbl = self::TBL; + + if (!$token) { + return false; + } + + $sql = "UPDATE `$tbl` SET failed_challenges = failed_challenges + 1 WHERE token = '%s' LIMIT 1"; + + mysql_global_call($sql, $token); + } + + private function get_captcha_failures($token) { + $tbl = self::TBL; + + if (!$token) { + return 0; + } + + $sql = "SELECT failed_challenges FROM `$tbl` WHERE token = '%s' LIMIT 1"; + + $res = mysql_global_call($sql, $token); + + if (!$res) { + return 0; + } + + return (int)mysql_fetch_row($res)[0]; + } + + public function request() { + $this->mode = 'request'; + + $this->validate_csrf(); + + if (!isset($_POST['email']) || !$_POST['email']) { + $this->error(self::ERR_BAD_EMAIL); + } + + $email = strtolower(trim($_POST['email'])); + + $this->validate_email($email); + + if ($this->get_domain($email) == 'gmail.com') { + $clean_email = $this->normalize_gmail_address($email); + } + else { + $clean_email = $email; + } + + $hashed_email = $this->hash_email($clean_email); + + if (!$hashed_email) { + $this->error_generic('nhe'); + } + + $this->validate_cooldowns($email, $hashed_email); + + $this->validate_captcha(); + + $this->validate_rangeban(); + + if (isset($_COOKIE['4chan_pass'])) { + $pwd = UserPwd::decodePwd($_COOKIE['4chan_pass']); + + // Password has a ban, show fake success screen + if ($pwd && $this->is_pwd_banned($pwd)) { + return $this->renderHTML('signin'); + } + } + + // Email is blacklisted, show fake success screen + if ($this->is_email_blacklisted($email)) { + $this->log_event(self::EVT_BLACKLISTED, $email); + return $this->renderHTML('signin'); + } + + // Try to ignore bot requests + if ($this->should_ignore_request($email)) { + $this->log_event(self::EVT_IGNORED, $email); + return $this->renderHTML('signin'); + } + + $token = $this->create_request($email, $hashed_email); + + $this->renderHTML('signin'); + } + + private function pre_verify() { + if (!isset($_GET['tkn'])) { + $this->error(self::ERR_BAD_REQUEST); + } + + $this->token = trim($_GET['tkn']); + + if (!$this->token) { + $this->error(self::ERR_BAD_REQUEST); + } + + $_token_bot_score = $this->get_token_bot_score($this->token); + + if (true && $_token_bot_score < 90) { + $this->use_recaptcha = true; + } + else { + $this->use_recaptcha = false; + } + + $this->csrf_token = $this->get_csrf_token(); + + setcookie(self::CSRF_ARG, $this->csrf_token, 0, '/', $_SERVER['HTTP_HOST'], true, true); + + $this->renderHTML('signin'); + } + + public function verify() { + if ($_SERVER['REQUEST_METHOD'] == 'GET') { + $this->mode = 'verify'; + return $this->pre_verify(); + } + else { + $this->mode = 'verify-done'; + } + + $this->validate_csrf(); + + if (!isset($_POST['tkn'])) { + $this->error(self::ERR_BAD_REQUEST); + } + + $token = trim($_POST['tkn']); + + if (!$token) { + $this->error(self::ERR_BAD_REQUEST); + } + + if (isset($_COOKIE['pass_enabled']) && $_COOKIE['pass_enabled']) { + $this->error(self::ERR_PASS_USER); + } + + $_bot_score = $this->get_bot_score(); + $_token_bot_score = $this->get_token_bot_score($token); + + if ($_bot_score < self::BOT_SCORE_SUSP || $_token_bot_score < self::BOT_SCORE_SUSP) { + $_max_fails = self::MAX_CAPTCHA_FAILURES_SUSP; + } + else { + $_max_fails = self::MAX_CAPTCHA_FAILURES; + } + + if (true && $_token_bot_score < 90) { + $this->use_recaptcha = true; + } + else { + $this->use_recaptcha = false; + } + + if ($this->get_captcha_failures($token) >= $_max_fails) { + $this->error(self::ERR_BAD_LINK); + } + + if ($this->use_recaptcha == false) { + if ($this->is_valid_captcha_t() !== true) { + $this->register_captcha_failure($token); + $this->mode = 'verify-captcha-failed'; + $this->token = $token; + $this->renderHTML('signin'); + die(); + } + } + else { + $this->validate_captcha(true); + } + + $this->prune_old_requests(); + + $this->update_usage_count($token); + + $tbl = self::TBL; + + $ttl = (int)self::VERIFY_TOKEN_TTL; + + $sql =<< DATE_SUB(NOW(), INTERVAL $ttl SECOND) +SQL; + + $res = mysql_global_call($sql, $token); + + if (!$res) { + $this->error(self::ERR_DB); + } + + $request = mysql_fetch_assoc($res); + + if (!$request) { + $this->error(self::ERR_BAD_LINK); + } + + if (!$request['hashed_email']) { + $this->error_generic('hee'); + } + + // Validate usage count + if ((int)$request['used'] > self::TOKEN_MAX_USAGES) { + $this->log_event(self::EVT_MAX_USED, $request['token']); + $this->error(self::ERR_BAD_LINK); + } + + // Country must match the requester's country + $country = $this->get_country(); + + if ($country !== $request['country']) { + $this->log_event(self::EVT_BAD_COUNTRY, $request['token']); + $this->error(self::ERR_BAD_LINK); + } + + $ip = $_SERVER['REMOTE_ADDR']; + + $userpwd = null; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($ip, self::PWD_DOMAIN, $_COOKIE['4chan_pass']); + + if (!$userpwd) { + $this->error_generic('nup'); + } + + // Password has a ban, show fake success screen + if ($this->is_pwd_banned($userpwd->getPwd())) { + return $this->renderHTML('signin'); + } + + // If already verified, make a brand new pwd + if ($userpwd->verifiedLevel() > 0) { + $userpwd = null; + } + } + + if (!$userpwd) { + $userpwd = new UserPwd($ip, self::PWD_DOMAIN); + + if (!$userpwd) { + $this->error_generic('nupn'); + } + } + + $userpwd->setPwd($request['hashed_email']); + $userpwd->setVerifiedLevel(self::VERIFIED_LEVEL); + + $userpwd->setCookie('.' . self::PWD_DOMAIN); + + // This is to let currently running cooldowns that the email was verified + $this->set_ev1_cookie(true); + + $this->renderHTML('signin'); + } + + /** + * Signout - deletes the password cookie + */ + public function signout() { + $this->mode = 'signout'; + + $this->validate_csrf(); + + $userpwd = null; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], self::PWD_DOMAIN, $_COOKIE['4chan_pass']); + } + + // If already verified, make a brand new pwd + if ($userpwd && $userpwd->verifiedLevel() > 0) { + setcookie(UserPwd::COOKIE_NAME, '', -1, '/', '.' . self::PWD_DOMAIN, true, true); + } + + $this->set_ev1_cookie(false); + + $this->renderHTML('signin'); + } + + /** + * Index + */ + public function index() { + $this->mode = 'index'; + + if (isset($_COOKIE['pass_enabled']) && $_COOKIE['pass_enabled']) { + $this->pass_user = true; + return $this->renderHTML('signin'); + } + + $this->pass_user = false; + + $this->csrf_token = $this->get_csrf_token(); + + $domain = $_SERVER['HTTP_HOST']; + + $userpwd = null; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], self::PWD_DOMAIN, $_COOKIE['4chan_pass']); + } + + $this->authed = $userpwd && $userpwd->verifiedLevel() > 0; + + setcookie(self::CSRF_ARG, $this->csrf_token, 0, '/', $domain, true, true); + + $this->renderHTML('signin'); + } + + /** + * Main + */ + public function run() { + $method = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET; + + if (isset($method['action'])) { + $action = $method['action']; + } + else { + $action = 'index'; + } + + if (in_array($action, $this->actions)) { + $this->$action(); + } + else { + $this->error('Bad request'); + } + } +} + +$ctrl = new App(); +$ctrl->run(); diff --git a/tasks/pass_mailer.php b/tasks/pass_mailer.php new file mode 100644 index 0000000..c9a72f8 --- /dev/null +++ b/tasks/pass_mailer.php @@ -0,0 +1,138 @@ +\r\n"; + $headers .= "MIME-Version: 1.0\r\n"; + $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; + + // Envelope + $opts = '-f 4chanpass@4chan.org'; + + return mail($email, $subject, $message, $headers, $opts); +} + +// Remindre interval in days +$reminder_interval = 7; + +echo '### MAILER BEGIN RUN AT ' . date('r') . " ###\n"; + +$query =<<' . S_RETURN . ' ' . S_CATALOG . ' ' . S_BOTTOM . ' ' . S_REFRESH . ' +
    '; + } + + if( !$stripm ) $msg .= ''; + } + else { + if( !$stripm ) $msg .= ' + +'; + } + + if( $admin ) { + $hidden = ''; + $msg = '

    ' . S_NOTAGS . '

    '; + } + + if ($closed != 1 || (BOARD_DIR === 'qa' && $log[$resno]['capcode'] !== 'none')) { + $dat .= $msg; + form_ads( $dat ); + + $hidden = STATS_USER_JS ? '' : ''; + + $dat .= ' +
    +' . $hidden . ( !TEXT_ONLY ? (' +') : '') . ' + +'; + + if( $no ) { + $dat .= ' + +'; + } + + if (FORCED_ANON) { + $dat .= ' + +'; + } + + $dat .= ' + +'; + + $spoilers = ''; + if( SPOILERS == 1 ) { + $spoilers = '[]'; + } + + if (!FORCED_ANON) { + $dat .= ' + + + + '; + } + + if ($spoilers && !$stripm && !TEXT_ONLY) { + $dat .= ' + + + + + '; + } + + if (!$resno && !FORCED_ANON) { + $attr = TEXT_ONLY ? ' required' : ''; + + $dat .= ' + + + + + + + + '; + } + else { + $dat .= ' + + + + '; + } + + $dat .= ' + + + + + '; + + if (CAPTCHA == 1) { + $dat .= ' + + + + + '; + } + + if (ENABLE_BOARD_FLAGS) { + $board_flags_selector = get_board_flags_selector(); + + if (SHOW_COUNTRY_FLAGS) { + $opts_html = ''; + } + else { + $opts_html = ''; + } + + foreach ($board_flags_selector as $flag_code => $flag_name) { + $opts_html .= ''; + } + + $dat .= ' + + + + + '; + } + + $need_file_form = false; + + if ($_GET['mode'] != 'oe_finish') { + if ((!TEXT_ONLY || (TEXT_ONLY_ALLOW_OP && !$resno)) && MAX_IMGRES != 0) { + $need_file_form = true; + } + } + + if ($need_file_form) { + $dat .= ' + + '; + + if (ENABLE_PAINTERJS) { + $dat .= ' + + + '; + } + } + + if ($resno && SHOW_THREAD_UNIQUES) { + $unique = $thread_unique_ips; + if ($unique) { + $unique_plural = $unique > 1 ? 'are' : 'is'; + $unique = sprintf("
  • There $unique_plural " . S_UNIQUE_POSTS_TH . '
  • ', $unique, $unique > 1 ? 's' : ''); + } + else { + $unique = ''; + } + } + elseif (SHOW_UNIQUES) { + $unique = sprintf('
  • ' . S_UNIQUE_POSTS . '
  • ', $GLOBALS['ipcount']); + } + else { + $unique = ''; + } + + $blotter = get_blotter(); + + // XXX: mode=regist moved to the top + $dat .= ' + + + + '; + $dat .= ' + + +
    ' . S_NAME . '
    ' . S_SPOILERS . '[]
    ' . S_EMAIL . '
    ' . S_SUBJECT . '
    ' . S_EMAIL . '
    ' . S_COMMENT . '
    ' . S_CAPTCHA . '' . (CAPTCHA_TWISTER ? twister_captcha_form() : captcha_form()) . '
    ' . S_PASS_NOTICE . '
    ' . S_FLAG . '
    ' . S_UPLOADFILE . ' + ' . $spoilers; + + if( !$resno && NO_TEXTONLY != 1 ) { + $dat .= '[]'; + } + + $dat .= '
    DrawSize × '; + + if (ENABLE_OEKAKI_REPLAYS) { + $dat .= ' '; + } + + $dat .= '
    +
      + ' . S_RULES . ' + ' . $unique . ' +
    +
    ' . $blotter . ' +
    +' . (!$resno ? EMBED_INDEX : ''); + } else { // Closed thread + form_ads( $dat ); + if( !$stripm ) $dat .= ' + +
    '; + + if ($log[$resno]['archived']) { + $dat .= '
    ' . S_THREAD_ARCHIVED . '
    '; + } + else { + $dat .= '
    ' . S_THREAD_CLOSED . '
    '; + } + } + + if( AD_MIDDLE_ENABLE == 1 ) { + $middlead = ""; + + if( defined( "AD_MIDDLE_TEXT" ) && AD_MIDDLE_TEXT ) { + $middlead .= '
    ' . ad_text_for( AD_MIDDLE_TEXT ) . '
    ' . (defined('AD_MIDDLE_PLEA') ? AD_MIDDLE_PLEA : ''); + } else if( defined( "AD_MIDDLE_TABLE" ) && AD_MIDDLE_TABLE ) { + list( $middleimg, $middlelink ) = rid( AD_MIDDLE_TABLE, 1 ); + $middlead .= "
    \"\"
    "; + } + + if( $middlead ) { + $dat .= "$middlead"; + } + } + else if (!$stripm) { // not for catalog + // Contest banners + $dat .= '
    ' . get_contest_banner() . '
    '; + } + + if (!$resno || !$closed) { + list($globalmsgtxt,$globalmsgdate) = global_msg_txt(); + + if( $globalmsgtxt ) { + $dat .= "\n
    " . S_VIEW_GMSG . "
    " . $globalmsgtxt . "
    \n"; + } + } + + // Catalog + if ($stripm) { + if (defined('AD_CUSTOM_TOP') && AD_CUSTOM_TOP) { + $dat .= '

    ' . AD_CUSTOM_TOP . '
    '; + } + /*else if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + $dat .= '

    '; + } + else if (defined('AD_BIDGEAR_TOP') && AD_BIDGEAR_TOP) { + $dat .= '

    '; + }*/ + else if (defined('ADS_DANBO') && ADS_DANBO) { + $dat .= '
    '; + } + } + // Not catalog + else if (defined('AD_CUSTOM_TOP') && AD_CUSTOM_TOP) { + $dat .= '

    ' . AD_CUSTOM_TOP . '
    '; + } + /*else if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + $dat .= '

    '; + } + else if (defined('AD_BIDGEAR_TOP') && AD_BIDGEAR_TOP) { + $dat .= '

    '; + }*/ + else if (defined('ADS_DANBO') && ADS_DANBO) { + $dat .= '
    '; + } + + if ($resno) { + $dat .= '
    + + '; + } + + if( JANITOR_BOARD == 1 ) { + $dat = broomcloset_form( $dat ); + } + +} + +function updatelog_real( $resno = 0, $noidx = 0, $lazy_rebuild = false ) +{ + global $log, $mode, $index_rbl, $board_flags_array; + + if (ENABLE_BOARD_FLAGS) { + $board_flags_array = get_board_flags_array(); + } + + if( !IS_REBUILDD ) set_time_limit( 60 ); + + // DDOS Protection + if( $_SERVER['REQUEST_METHOD'] == 'GET' && !has_level() ) { + die(); + } + + if( STATIC_REBUILD && $mode != 'nothing' ) { + $noidx = 1; + } + + if( !$resno && $noidx ) { + return; + } + + log_cache( 0, $noidx ? $resno : 0 ); + + // Image directories + /* + $imgdir = ( ( USE_SRC_CGI == 1 ) ? str_replace( 'src', 'src.cgi', IMG_DIR2 ) : IMG_DIR2 ); + if( defined( 'INTERSTITIAL_LINK' ) ) { + $imgdir .= INTERSTITIAL_LINK; + } + */ + + $resno = (int)$resno; + + $inter_ad_html = null; + + if( $resno ) { + if( !isset( $log[$resno] ) ) { + updatelog_real( 0, $noidx ); + + return; + } elseif( $log[$resno]['resto'] ) { + updatelog_real( $log[$resno]['resto'], $noidx ); + + return; + } + + // Inter-reply ads + $inter_ad_html = ''; + + if (defined('AD_ABC_TOP_MOBILE') && AD_ABC_TOP_MOBILE) { + $inter_ad_html .= '


    '; + } + + if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + $inter_ad_html .= '


    '; + } + + if ($inter_ad_html === '') { + $inter_ad_html = null; + } + } + + if( $resno ) { + logtime( "Generating thread JSON" ); + + if (ENABLE_JSON) { + $tailSize = get_json_tail_size($resno); + + if ($tailSize) { + generate_thread_json($resno, false, false, false, $tailSize); + } + + generate_thread_json($resno); + } + + $treeline = array($resno); + logtime( "Formatting thread page" ); + } else { + logtime( "Generating index JSON" ); + if( ENABLE_JSON_INDEXES ) generate_index_json(); + if( ENABLE_JSON_CATALOG ) generate_board_catalogue(); + if( ENABLE_JSON_THREADS ) { + generate_board_threads_json(); + } + + $treeline = $log['THREADS']; + logtime( "Formatting index page" ); + } + + $counttree = count( $treeline ); + if( !$counttree ) { + $logfilename = SELF_PATH2_FILE; + $dat = ''; + head( $dat, $resno ); + form( $dat, $resno ); + print_page( $logfilename, $dat ); + $dat = ''; + } + + $st = 0; + $p = 0; + + if ($lazy_rebuild) { + $start_page = $index_rbl * DEF_PAGES; + } + else { + $start_page = 0; + } + + $index_page_ad_pos = ceil(DEF_PAGES / 2); + + if (defined('REPLIES_SHOWN')) { + $shown_replies_default = REPLIES_SHOWN; + } + else { + $shown_replies_default = 5; + } + + for( $page = $start_page; $page < $counttree; $page += DEF_PAGES ) { + $file_page_num = $page / DEF_PAGES + 1; + + if (PAGE_MAX && $file_page_num > PAGE_MAX) { + break; + } + + $dat = ''; + head( $dat, $resno, 0, $page, $counttree ); + form( $dat, $resno ); + if( !$resno ) { + $st = $page; + + $dat .= '

    [' . S_CATALOG . ']'; + + if (ENABLE_ARCHIVE) { + $dat .= ' [' . S_ARCHIVE . ']'; + } + + $dat .= '
    '; + + if (floor( $page / DEF_PAGES ) > $index_rbl) { + return; + } + } + + // Post form / board container container start. + $dat .= '
    +
    +
    +'; + + $index_page_th_id = 0; + + for( $i = $st; $i < $st + DEF_PAGES; $i++ ) { + $no = $treeline[$i]; + + if (!$no) { + break; + } + /* + if (!isset($log[$no]['children'])) { + log_bad_cache_entry($no); + } + */ + $sorted_replies = $log[$no]['children']; + ksort($sorted_replies); + + // Party hats + $party = PARTY ? '' : ''; + + // Omitted replies + $reply_count = $log[$no]['replycount']; + + if ($log[$no]['sticky'] == 1) { + $shown_replies = min(1, $shown_replies_default); + } + else { + $shown_replies = $shown_replies_default; + } + + // Open thread tag and render OP + $dat .= '
    ' + . $party + . renderPostHtml($no, $resno, $sorted_replies, $reply_count, $shown_replies); + + // Render replies + if ($resno) { + $s = 0; + } + else { + $s = $reply_count - $shown_replies; + } + + $repCount = 0; + + if ($inter_ad_html && $reply_count >= 100) { + $middle_reply_idx = floor($reply_count / 2.0); + } + else { + $middle_reply_idx = 0; + } + + while (list($resrow) = each($sorted_replies)) { + if( $s > 0 ) { + $s--; + continue; + } + + if (!$log[$resrow]['no']) { + break; + } + + $dat .= renderPostHtml($resrow, $resno); + + $repCount++; + + if ($repCount == $middle_reply_idx) { + $dat .= $inter_ad_html; + } + } + + // Close thread tag + $dat .= ' +
    +
    +'; + ++$index_page_th_id; + + if (!$resno && AD_INTERTHREAD_ENABLED && $index_page_ad_pos == $index_page_th_id) { + $dat .= AD_INTERTHREAD_TAG . '
    '; + } + + $p++; + + if ($resno) { + break; + } + } + + if ($resno) { + $dat .= '
    '; + } + + // Close board tag + $lang = $resno ? S_FORM_REPLY : S_FORM_THREAD; + + $dat .= ' + +
    '; + + if (!$resno) { + $dat .= '
    '; + } + else { + $dat .= '
    '; + } + + /** + * ADS + */ + + if (defined('AD_CUSTOM_BOTTOM') && AD_CUSTOM_BOTTOM) { + $dat .= '
    ' . AD_CUSTOM_BOTTOM . '
    '; + } + /* + else if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) { + $dat .= '

    '; + } + else if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM) { + $dat .= '
    '; + } + */ + else if (defined('ADS_DANBO') && ADS_DANBO) { + $dat .= '

    '; + } + + if( $resno ) { + $resredir = ''; + } else { + $resredir = ''; + } + + $dat .= '
    ' . S_REPDEL . $resredir . ' [' . S_DELPICONLY . '] '; + + if( !defined( 'CSS_FORCE' ) ) { + $dat .= 'Style: + + '; + } + + $dat .= '
    '; + + if( !$resno ) { + $prev = $st - DEF_PAGES; + $next = $st + DEF_PAGES; + + $dat .= '
    '; + $dat .= $mpl . '
    '; + } + + if (true && (!$resno || (!$log[$resno]['closed'] && !$log[$resno]['archived']))) { + $dat .= ''; + } + + foot( $dat ); + + if( $resno ) { + logtime( 'Printing thread ' . $resno . ' page' ); + $logfilename = RES_DIR . $resno . PHP_EXT; + print_page( $logfilename, $dat ); + $dat = ''; + + if( !$noidx ) { + updatelog_real( 0 ); + } + + break; + } + + logtime( "Printing index page" ); + if ($page == 0) { + $logfilename = SELF_PATH2_FILE; + print_page($logfilename, $dat); + } + else { + $logfilename = INDEX_DIR . ($file_page_num) . PHP_EXT; + print_page($logfilename, $dat); + } + + $dat = ''; + + if( !$resno && $page == 0 && USE_RSS == 1 ) { + include_once 'lib/rss.php'; + rss_dump(); + } + } // for +} + +//wrapper function for forwarding updatelog calls +//resno - thread page to update (no of thread OP) +//noidx - don't rebuild page indexes +function updatelog( $resno = 0, $noidx = 0, $lazy_rebuild = false ) +{ + updatelog_real( $resno, $noidx, $lazy_rebuild ); + + if( !STATIC_REBUILD && ENABLE_CATALOG && !$noidx ) generate_catalog( true ); +} + +?> \ No newline at end of file diff --git a/views/imgboard.php b/views/imgboard.php new file mode 100644 index 0000000..b1e6952 --- /dev/null +++ b/views/imgboard.php @@ -0,0 +1,739 @@ + + ' . S_RETURN . ' ' . S_CATALOG . ' ' . S_BOTTOM . ' ' . S_REFRESH . ' +
    '; + } + + if( !$stripm ) $msg .= ''; + } + else { + if( !$stripm ) $msg .= ' + +'; + } + + if( $admin ) { + $hidden = ''; + $msg = '

    ' . S_NOTAGS . '

    '; + } + + if ($closed != 1 || (BOARD_DIR === 'qa' && $log[$resno]['capcode'] !== 'none')) { + $dat .= $msg; + form_ads( $dat ); + + $hidden = STATS_USER_JS ? '' : ''; + + $dat .= ' +
    +' . $hidden . ( !TEXT_ONLY ? (' +') : '') . ' + +'; + + if( $no ) { + $dat .= ' + +'; + } + + if (FORCED_ANON) { + $dat .= ' + +'; + } + + $dat .= ' + +'; + + $spoilers = ''; + if( SPOILERS == 1 ) { + $spoilers = '[]'; + } + + if (!FORCED_ANON) { + $dat .= ' + + + + '; + } + + if ($spoilers && !$stripm && !TEXT_ONLY) { + $dat .= ' + + + + + '; + } + + if (!$resno && !FORCED_ANON) { + $attr = TEXT_ONLY ? ' required' : ''; + + $dat .= ' + + + + + + + + '; + } + else { + $dat .= ' + + + + '; + } + + $dat .= ' + + + + + '; + + if (CAPTCHA == 1) { + $dat .= ' + + + + + '; + } + + if (ENABLE_BOARD_FLAGS) { + $board_flags_selector = get_board_flags_selector(); + + if (SHOW_COUNTRY_FLAGS) { + $opts_html = ''; + } + else { + $opts_html = ''; + } + + foreach ($board_flags_selector as $flag_code => $flag_name) { + $opts_html .= ''; + } + + $dat .= ' + + + + + '; + } + + $need_file_form = false; + + if ($_GET['mode'] != 'oe_finish') { + if ($resno) { + if (!TEXT_ONLY && MAX_IMGRES != 0) { + $need_file_form = true; + } + } + else { + $need_file_form = true; + } + } + + if ($need_file_form) { + $dat .= ' + + '; + + if (ENABLE_PAINTERJS) { + $dat .= ' + + + '; + } + } + + if ($resno && SHOW_THREAD_UNIQUES) { + $unique = $thread_unique_ips; + if ($unique) { + $unique_plural = $unique > 1 ? 'are' : 'is'; + $unique = sprintf("
  • There $unique_plural " . S_UNIQUE_POSTS_TH . '
  • ', $unique, $unique > 1 ? 's' : ''); + } + else { + $unique = ''; + } + } + elseif (SHOW_UNIQUES) { + $unique = sprintf('
  • ' . S_UNIQUE_POSTS . '
  • ', $GLOBALS['ipcount']); + } + else { + $unique = ''; + } + + $blotter = get_blotter(); + + // XXX: mode=regist moved to the top + $dat .= ' + + + + '; + $dat .= ' + + +
    ' . S_NAME . '
    ' . S_SPOILERS . '[]
    ' . S_EMAIL . '
    ' . S_SUBJECT . '
    ' . S_EMAIL . '
    ' . S_COMMENT . '
    ' . S_CAPTCHA . '' . (CAPTCHA_TWISTER ? twister_captcha_form() : captcha_form()) . '
    ' . S_PASS_NOTICE . '
    ' . S_FLAG . '
    ' . S_UPLOADFILE . ' + ' . $spoilers; + + if( !$resno && NO_TEXTONLY != 1 ) { + $dat .= '[]'; + } + + $dat .= '
    DrawSize × '; + + if (ENABLE_OEKAKI_REPLAYS) { + $dat .= ' '; + } + + $dat .= '
    +
      + ' . S_RULES . ' + ' . $unique . ' +
    +
    ' . $blotter . ' +
    +' . (!$resno ? EMBED_INDEX : ''); + } else { // Closed thread + form_ads( $dat ); + if( !$stripm ) $dat .= ' + +
    '; + + if ($log[$resno]['archived']) { + $dat .= '
    ' . S_THREAD_ARCHIVED . '
    '; + } + else { + $dat .= '
    ' . S_THREAD_CLOSED . '
    '; + } + } + + if( AD_MIDDLE_ENABLE == 1 ) { + $middlead = ""; + + if( defined( "AD_MIDDLE_TEXT" ) && AD_MIDDLE_TEXT ) { + $middlead .= '
    ' . ad_text_for( AD_MIDDLE_TEXT ) . '
    ' . (defined('AD_MIDDLE_PLEA') ? AD_MIDDLE_PLEA : ''); + } else if( defined( "AD_MIDDLE_TABLE" ) && AD_MIDDLE_TABLE ) { + list( $middleimg, $middlelink ) = rid( AD_MIDDLE_TABLE, 1 ); + $middlead .= "
    \"\"
    "; + } + + if( $middlead ) { + $dat .= "$middlead"; + } + } + else if (!$stripm) { // not for catalog + // Contest banners + $dat .= '
    ' . get_contest_banner() . '
    '; + } + + if (!$resno || !$closed) { + list($globalmsgtxt,$globalmsgdate) = global_msg_txt(); + + if( $globalmsgtxt ) { + $dat .= "\n
    " . S_VIEW_GMSG . "
    " . $globalmsgtxt . "
    \n"; + } + } + + // Catalog + if ($stripm) { + if (defined('AD_CUSTOM_TOP') && AD_CUSTOM_TOP) { + $dat .= '

    ' . AD_CUSTOM_TOP . '
    '; + } + /*else if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + $dat .= '

    '; + } + else if (defined('AD_BIDGEAR_TOP') && AD_BIDGEAR_TOP) { + $dat .= '

    '; + }*/ + else if (defined('ADS_DANBO') && ADS_DANBO) { + $dat .= '
    '; + } + } + // Not catalog + else if (defined('AD_CUSTOM_TOP') && AD_CUSTOM_TOP) { + $dat .= '

    ' . AD_CUSTOM_TOP . '
    '; + } + /*else if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + $dat .= '

    '; + } + else if (defined('AD_BIDGEAR_TOP') && AD_BIDGEAR_TOP) { + $dat .= '

    '; + }*/ + else if (defined('ADS_DANBO') && ADS_DANBO) { + $dat .= '
    '; + } + + if ($resno) { + $dat .= '
    + + '; + } + + if( JANITOR_BOARD == 1 ) { + $dat = broomcloset_form( $dat ); + } + +} + +function updatelog_real( $resno = 0, $noidx = 0, $lazy_rebuild = false ) +{ + global $log, $mode, $index_rbl, $board_flags_array; + + if (ENABLE_BOARD_FLAGS) { + $board_flags_array = get_board_flags_array(); + } + + if( !IS_REBUILDD ) set_time_limit( 60 ); + + // DDOS Protection + if( $_SERVER['REQUEST_METHOD'] == 'GET' && !has_level() ) { + die(); + } + + if( STATIC_REBUILD && $mode != 'nothing' ) { + $noidx = 1; + } + + if( !$resno && $noidx ) { + return; + } + + log_cache( 0, $noidx ? $resno : 0 ); + + // Image directories + /* + $imgdir = ( ( USE_SRC_CGI == 1 ) ? str_replace( 'src', 'src.cgi', IMG_DIR2 ) : IMG_DIR2 ); + if( defined( 'INTERSTITIAL_LINK' ) ) { + $imgdir .= INTERSTITIAL_LINK; + } + */ + + $resno = (int)$resno; + + $inter_ad_html = null; + + if( $resno ) { + if( !isset( $log[$resno] ) ) { + updatelog_real( 0, $noidx ); + + return; + } elseif( $log[$resno]['resto'] ) { + updatelog_real( $log[$resno]['resto'], $noidx ); + + return; + } + + // Inter-reply ads + $inter_ad_html = ''; + + if (defined('AD_ABC_TOP_MOBILE') && AD_ABC_TOP_MOBILE) { + $inter_ad_html .= '


    '; + } + + if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + $inter_ad_html .= '


    '; + } + + if ($inter_ad_html === '') { + $inter_ad_html = null; + } + } + + if( $resno ) { + logtime( "Generating thread JSON" ); + + if (ENABLE_JSON) { + $tailSize = get_json_tail_size($resno); + + if ($tailSize) { + generate_thread_json($resno, false, false, false, $tailSize); + } + + generate_thread_json($resno); + } + + $treeline = array($resno); + logtime( "Formatting thread page" ); + } else { + logtime( "Generating index JSON" ); + if( ENABLE_JSON_INDEXES ) generate_index_json(); + if( ENABLE_JSON_CATALOG ) generate_board_catalogue(); + if( ENABLE_JSON_THREADS ) { + generate_board_threads_json(); + } + + $treeline = $log['THREADS']; + logtime( "Formatting index page" ); + } + + $counttree = count( $treeline ); + if( !$counttree ) { + $logfilename = SELF_PATH2_FILE; + $dat = ''; + head( $dat, $resno ); + form( $dat, $resno ); + print_page( $logfilename, $dat ); + $dat = ''; + } + + $st = 0; + $p = 0; + + if ($lazy_rebuild) { + $start_page = $index_rbl * DEF_PAGES; + } + else { + $start_page = 0; + } + + $index_page_ad_pos = ceil(DEF_PAGES / 2); + + if (defined('REPLIES_SHOWN')) { + $shown_replies_default = REPLIES_SHOWN; + } + else { + $shown_replies_default = 5; + } + + for( $page = $start_page; $page < $counttree; $page += DEF_PAGES ) { + $file_page_num = $page / DEF_PAGES + 1; + + if (PAGE_MAX && $file_page_num > PAGE_MAX) { + break; + } + + $dat = ''; + head( $dat, $resno, 0, $page, $counttree ); + form( $dat, $resno ); + if( !$resno ) { + $st = $page; + + $dat .= '

    [' . S_CATALOG . ']'; + + if (ENABLE_ARCHIVE) { + $dat .= ' [' . S_ARCHIVE . ']'; + } + + $dat .= '
    '; + + if (floor( $page / DEF_PAGES ) > $index_rbl) { + return; + } + } + + // Post form / board container container start. + $dat .= '
    +
    +
    +'; + + $index_page_th_id = 0; + + for( $i = $st; $i < $st + DEF_PAGES; $i++ ) { + $no = $treeline[$i]; + + if (!$no) { + break; + } + /* + if (!isset($log[$no]['children'])) { + log_bad_cache_entry($no); + } + */ + $sorted_replies = $log[$no]['children']; + ksort($sorted_replies); + + // Party hats + $party = PARTY ? '' : ''; + + // Omitted replies + $reply_count = $log[$no]['replycount']; + + if ($log[$no]['sticky'] == 1) { + $shown_replies = min(1, $shown_replies_default); + } + else { + $shown_replies = $shown_replies_default; + } + + // Open thread tag and render OP + $dat .= '
    ' + . $party + . renderPostHtml($no, $resno, $sorted_replies, $reply_count, $shown_replies); + + // Render replies + if ($resno) { + $s = 0; + } + else { + $s = $reply_count - $shown_replies; + } + + $repCount = 0; + + if ($inter_ad_html && $reply_count >= 100) { + $middle_reply_idx = floor($reply_count / 2.0); + } + else { + $middle_reply_idx = 0; + } + + while (list($resrow) = each($sorted_replies)) { + if( $s > 0 ) { + $s--; + continue; + } + + if (!$log[$resrow]['no']) { + break; + } + + $dat .= renderPostHtml($resrow, $resno); + + $repCount++; + + if ($repCount == $middle_reply_idx) { + $dat .= $inter_ad_html; + } + } + + // Close thread tag + $dat .= ' +
    +
    +'; + ++$index_page_th_id; + + if (!$resno && AD_INTERTHREAD_ENABLED && $index_page_ad_pos == $index_page_th_id) { + $dat .= AD_INTERTHREAD_TAG . '
    '; + } + + $p++; + + if ($resno) { + break; + } + } + + if ($resno) { + $dat .= '
    '; + } + + // Close board tag + $lang = $resno ? S_FORM_REPLY : S_FORM_THREAD; + + $dat .= ' + +
    '; + + if (!$resno) { + $dat .= '
    '; + } + else { + $dat .= '
    '; + } + + /** + * ADS + */ + + if (defined('AD_CUSTOM_BOTTOM') && AD_CUSTOM_BOTTOM) { + $dat .= '
    ' . AD_CUSTOM_BOTTOM . '
    '; + } + /* + else if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) { + $dat .= '

    '; + } + else if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM) { + $dat .= '
    '; + } + */ + else if (defined('ADS_DANBO') && ADS_DANBO) { + $dat .= '

    '; + } + + if( $resno ) { + $resredir = ''; + } else { + $resredir = ''; + } + + $dat .= '
    ' . S_REPDEL . $resredir . ' [' . S_DELPICONLY . '] '; + + if( !defined( 'CSS_FORCE' ) ) { + $dat .= 'Style: + + '; + } + + $dat .= '
    '; + + if( !$resno ) { + $prev = $st - DEF_PAGES; + $next = $st + DEF_PAGES; + + $dat .= '
    '; + $dat .= $mpl . '
    '; + } + + foot( $dat ); + + if( $resno ) { + logtime( 'Printing thread ' . $resno . ' page' ); + $logfilename = RES_DIR . $resno . PHP_EXT; + print_page( $logfilename, $dat ); + $dat = ''; + + if( !$noidx ) { + updatelog_real( 0 ); + } + + break; + } + + logtime( "Printing index page" ); + if ($page == 0) { + $logfilename = SELF_PATH2_FILE; + print_page($logfilename, $dat); + } + else { + $logfilename = INDEX_DIR . ($file_page_num) . PHP_EXT; + print_page($logfilename, $dat); + } + + $dat = ''; + + if( !$resno && $page == 0 && USE_RSS == 1 ) { + include_once 'lib/rss.php'; + rss_dump(); + } + } // for +} + +//wrapper function for forwarding updatelog calls +//resno - thread page to update (no of thread OP) +//noidx - don't rebuild page indexes +function updatelog( $resno = 0, $noidx = 0, $lazy_rebuild = false ) +{ + updatelog_real( $resno, $noidx, $lazy_rebuild ); + + if( !STATIC_REBUILD && ENABLE_CATALOG && !$noidx ) generate_catalog( true ); +} + +?> \ No newline at end of file diff --git a/views/pass_auth.tpl.php b/views/pass_auth.tpl.php new file mode 100644 index 0000000..b8a363f --- /dev/null +++ b/views/pass_auth.tpl.php @@ -0,0 +1,67 @@ + +> + + + 4chan Pass - Authenticate + + + + +
    +

    4chan Pass

    +
    +
    +auth_status === self::AUTH_NO): ?> +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    Token
    PIN

    Forgot your 4chan Pass login details?
    Go here to reset your PIN.

    Don't have a 4chan Pass?
    Click here to learn more.

    +
    +
    +auth_status === self::AUTH_YES): ?> +
    + +

    You are authenticated.

    +
    +
    +auth_status === self::AUTH_SUCCESS): ?> +
    +

    Success! Your device is now authorized.

    +
    +auth_status === self::AUTH_ERROR): ?> +
    +

    message ? $this->message : 'Something went wrong.' ?>

    +

    [Return]

    +
    +auth_status === self::AUTH_OUT): ?> +
    +

    You are now logged out.

    +
    + +
    +
    + + diff --git a/views/signin-test.tpl.php b/views/signin-test.tpl.php new file mode 100644 index 0000000..6f9c872 --- /dev/null +++ b/views/signin-test.tpl.php @@ -0,0 +1,99 @@ + + + + + 4chan - Email Verification + + + +mode === 'index'): ?> + + + + + +mode === 'verify'): ?> + use_recaptcha): ?> + + + + + + + + + + +
    + +
    +
    +mode === 'index'): ?> +authed): ?> +

    This session is already verified.

    You can clear your verified status by clicking the Logout button below.

    +
    + + + +
    +pass_user): ?> +

    + +

    Email Verification

    +

    A verified email address may be used to bypass
    anti-spam filters on some boards. If you are having trouble posting, try verifying your email.

    Enter your email below and click Send to receive a verification link.

    Your email address will be stored on our servers only briefly (usually for just a couple of minutes while the verification link is awaiting delivery).

    Email verification is not required for 4chan Pass users.

    +
    +
    +
    name="email" type="email" required>
    +
    Allowed domains are:
    + + +
    + +mode === 'verify-captcha-failed'): ?> +

    You seem to have mistyped the CAPTCHA.

    +

    Click here to try again.

    +mode === 'verify'): ?> +

    Please solve the CAPTCHA to finish the verification.

    Make sure cookies are not blocked before continuing.

    +
    + use_recaptcha): ?> +
    + + +
    + + + + + +
    +mode === 'verify-done'): ?> +

    This session is now verified.

    +
    +mode === 'request'): ?> +

    An email containing the verification link will be sent out shortly.

    +mode === 'signout'): ?> +

    Session cleared.

    +mode === 'error'): ?> +

    msg ?>

    + +
    +
    + + diff --git a/views/signin.tpl.php b/views/signin.tpl.php new file mode 100644 index 0000000..6f9c872 --- /dev/null +++ b/views/signin.tpl.php @@ -0,0 +1,99 @@ + + + + + 4chan - Email Verification + + + +mode === 'index'): ?> + + + + + +mode === 'verify'): ?> + use_recaptcha): ?> + + + + + + + + + + +
    + +
    +
    +mode === 'index'): ?> +authed): ?> +

    This session is already verified.

    You can clear your verified status by clicking the Logout button below.

    +
    + + + +
    +pass_user): ?> +

    + +

    Email Verification

    +

    A verified email address may be used to bypass
    anti-spam filters on some boards. If you are having trouble posting, try verifying your email.

    Enter your email below and click Send to receive a verification link.

    Your email address will be stored on our servers only briefly (usually for just a couple of minutes while the verification link is awaiting delivery).

    Email verification is not required for 4chan Pass users.

    +
    +
    +
    name="email" type="email" required>
    +
    Allowed domains are:
    + + +
    + +mode === 'verify-captcha-failed'): ?> +

    You seem to have mistyped the CAPTCHA.

    +

    Click here to try again.

    +mode === 'verify'): ?> +

    Please solve the CAPTCHA to finish the verification.

    Make sure cookies are not blocked before continuing.

    +
    + use_recaptcha): ?> +
    + + +
    + + + + + +
    +mode === 'verify-done'): ?> +

    This session is now verified.

    +
    +mode === 'request'): ?> +

    An email containing the verification link will be sent out shortly.

    +mode === 'signout'): ?> +

    Session cleared.

    +mode === 'error'): ?> +

    msg ?>

    + +
    +
    + + diff --git a/views/syncframe.html b/views/syncframe.html new file mode 100644 index 0000000..d78b5e1 --- /dev/null +++ b/views/syncframe.html @@ -0,0 +1,88 @@ + + + + 4chan + + + + + diff --git a/views/upboard.php b/views/upboard.php new file mode 100644 index 0000000..1014525 --- /dev/null +++ b/views/upboard.php @@ -0,0 +1,993 @@ + array("long" => "Hentai", "short" => "[H]"), + 6 => array("long" => "Porn", "short" => '[P]'), + 1 => array("long" => "Japanese", "short" => "[J]"), + 2 => array("long" => "Anime", "short" => "[A]"), + 3 => array("long" => "Game", "short" => "[G]"), + 5 => array("long" => "Loop", "short" => "[L]"), + 4 => array("long" => "Other", "short" => "[?]") + ); + + return $tags; +} + +function updatelog( $resno = 0, $rebuild = 0 ) +{ + $tags = upboard_tags(); + + $imgdir = IMG_DIR2; + $thumbdir = THUMB_DIR2; + $imgurl = STATIC_IMG_DIR2; + $sqlog = SQLLOG; // to make this a shite load easier + + $find = false; + $resno = (int)$resno; + + log_cache(); + + if( $resno ) { + $result = mysql_board_call( "SELECT `no` FROM `$sqlog` WHERE `root` > 0 AND `no` = '$resno'" ); + if( $result ) { + $find = mysql_fetch_row( $result ); + mysql_free_result( $result ); + } + + if( !$find ) { + $result2 = mysql_board_call( "SELECT `no`, `resto` FROM `$sqlog` WHERE `no` = '$resno'" ); + + list( $chkno, $resto ) = mysql_fetch_row( $result2 ); + if( !$resto ) { + error( S_REPORTERR ); + } + + mysql_free_result( $result2 ); + + $result3 = mysql_board_call( "SELECT `no` FROM `$sqlog` WHERE `no` = '$resto'" ); + if( $result3 ) { + $chkfind = mysql_fetch_row( $result3 ); + mysql_free_result( $result3 ); + } + + if( !$chkfind ) { + error( S_REPORTERR ); + } + } + } + + if( $resno ) { + if( !$treeline = mysql_board_call( "SELECT * FROM `$sqlog` WHERE `root` > 0 AND `no` = '$resno' ORDER BY `root` DESC" ) ) { + echo S_SQLFAIL; + } + + + } else { + if( !$treeline = mysql_board_call( "SELECT * FROM `$sqlog` WHERE `root` > 0 ORDER BY `root` DESC" ) ) { + echo S_SQLFAIL; + } + } + + if( $resno ) { + //logtime("Generating thread JSON"); + if( ENABLE_JSON ) generate_thread_json( $resno ); + + //$treeline = array( $resno ); + //logtime("Formatting thread page"); + } else { + //logtime("Generating index JSON"); + if( ENABLE_JSON_INDEXES ) generate_index_json(); + if( ENABLE_JSON_CATALOG ) generate_board_catalogue(); + if( ENABLE_JSON_THREADS ) generate_board_threads_json(); + + //$treeline = $log['THREADS']; + //logtime("Formatting index page"); + } + + if( !$result = mysql_board_call( "SELECT MAX(`no`) FROM `$sqlog`" ) ) { + echo S_SQLFAIL; + } + + $row = mysql_fetch_array( $result ); + $lastno = (int)$row[0]; + mysql_free_result( $result ); + + $counttree = mysql_num_rows( $treeline ); + + if( !$counttree ) { + $logfilename = SELF_PATH2_FILE; + $dat = ''; + + head( $dat, $resno ); + form( $dat, $resno ); + print_page( $logfilename, $dat ); + } + + $page = 0; + $dat = ''; + + head( $dat, $resno ); + form( $dat, $resno ); + + if( !$resno ) { + $st = $page; + } + + $dat .= '
    '; + + // here we go + if( !$resno ) { + $dat .= << + + No. + Name + File + Tag + Subject + Size + Date + Replies + + +HTML; + } + + $delarr = array(); + $limit = (int)round( DEF_PAGES * 0.83 ); + $lim = DEF_PAGES - $limit; + + if( !$result = mysql_board_call( "SELECT COUNT(*) FROM `$sqlog` WHERE `resto` = '0'" ) ) { + echo S_SQLFAIL; + } + + $row = mysql_fetch_array( $result ); + $countth = (int)$row[0]; + + if( $limit < $countth ) { + if( !$result = mysql_board_call( "SELECT `no` FROM `$sqlog` WHERE `resto` = '0' ORDER BY `no` ASC LIMIT $lim" ) ) { + echo S_SQLFAIL; + } + + while( $row = mysql_fetch_array( $result ) ) { + $delarr[] = (int)$row[0]; + } + } + + mysql_free_result( $result ); + + for( $i = $st; $i < $st + DEF_PAGES; $i++ ) { // NO PAGES FOR /f/ (apparently!) + //if( !mysql_fetch_assoc($treeline) ) continue; + + $thistree = mysql_fetch_assoc( $treeline ); + if( !$thistree ) break; + + extract( $thistree ); + //list($no,$sticky,$permasage,$closed,$now,$name,$email,$sub,$com,$host,$pwd,$filename,$ext,$w,$h,$tn_w,$tn_h,$tim,$time,$md5,$fsize,$root,$resto,$filedeleted,$tmd5,$id,$capcode)=mysql_fetch_row($treeline); + + $tag_matches = array(); + $tag = 4; + + if( preg_match( '/^(\d+)\|/', $sub, $tag_matches ) ) { + $tag = (int)( $tag_matches[1] ); + $sub = preg_replace( '/^(\d+)\|/', '', $sub ); + } + + if( $resno ) { + if( !$no ) { + break; + } + + + $sub = str_replace( ',', ',', $sub ); + + $emailstart = $emailend = ''; + //if( $email != '' ) { + // $email = emailencode($email); + // $emailstart = ''; + // $emailend = ''; + //} + + // NEW CAPCODE STUFF + + switch( $capcode ) { + case 'admin': + $capcodeStart = ' ## Admin'; + $capcode_class = ' capcodeAdmin'; + + $capcode = ' This user is the 4chan Administrator.'; + $highlight = ''; + break; + + case 'admin_highlight': + $capcodeStart = ' ## Admin'; + $capcode_class = ' capcodeAdmin'; + + $capcode = ' This user is the 4chan Administrator.'; + $highlight = ' highlightPost'; + break; + + case 'mod': + $capcodeStart = ' ## Mod'; + $capcode_class = ' capcodeMod'; + + $capcode = ' This user is a 4chan Moderator.'; + $highlight = ''; + break; + + case 'developer': + $capcodeStart = ' ## Developer'; + $capcode_class = ' capcodeDeveloper'; + + $capcode = ' This user is a 4chan Developer.'; + $highlight = ''; + break; + + case 'manager': + $capcodeStart = ' ## Manager'; + $capcode_class = ' capcodeManager'; + + $capcode = ' Manager Icon'; + $highlight = ''; + break; + + case 'verified': + $capcodeStart = ' ## Verified'; + $capcode_class = ' capcodeVerified'; + + $capcode = ''; + $highlight = ''; + break; + + default: + $capcode = $capcodeStart = $highlight = $capcode_class = ''; + break; + } + + $com = auto_link( $com, $resno ); + + $subshort = $sub; + if( mb_strlen( $sub ) > 28 ) { + $subshort = '' . mb_substr( $sub, 0, 25, 'UTF-8' ) . '(...)'; + } + + $file = ''; + if( $ext ) { + $img = IMG_DIR . $tim . $ext; + + $displaysrc = $imgdir . rawurlencode( $filename ) . $ext; + + $src = IMG_DIR . $filename . $ext; + + $longname = $filename . $ext; + + if( $fsize >= 1048576 ) { + $size = round( $fsize / 1048576, 2 ) . ' M'; + } elseif( $fsize >= 1024 ) { + $size = round( $fsize / 1048 ) . ' K'; + } else { + $size = $fsize . ' '; + } + + if( $filedeleted ) { + $imgsrc = '[File Deleted]'; + $class = ''; + } else { + $imgsrc = '' . S_PICNAME . ': ' . $longname . '-(' . $size . 'B, ' . $w . 'x' . $h . ', ' . $tags[$tag]['long'] . ')'; + $class = ' class="fileInfo"'; + } + + $imgClassStart = ( $imgsrc == '' ) ? '' : '
    '; + $imgClassEnd = ( $imgsrc == '' ) ? '' : '
    '; + + $file = << + $imgClassStart + $imgsrc + $imgClassEnd +
    +HTML; + } + + // Main creatio + if( $sticky == 1 ) { + $threadmodes .= ' Sticky'; + } + + if( $closed == 1 ) { + $threadmodes .= ' Closed'; + } + + + $href = ( $resno ) ? $no . PHP_EXT2 : RES_DIR2 . $no . PHP_EXT2; + $quote = ( $resno ) ? 'javascript:quote(\'' . $no . '\');' : $href . '#q' . $no; + + $extra = ''; + + $stickies = array(); + if( !$result = mysql_board_call( "SELECT `no` FROM `$sqlog` WHERE `sticky` = '1'" ) ) { + echo S_SQLFAIL; + } + + while( $stickrow = mysql_fetch_row( $result ) ) { + list( $stickno ) = $stickrow; + $stickies[] = $stickno; + } + + if( in_array( $no, $delarr ) ) { + if( in_array( $no, $stickies ) ) { + $stuck = 1; + } + + if( $stuck != 1 ) { + $extra = '' . S_OLD . ''; + } + } + + + $dat .= << +
    +
    + + + $file + + + +
    $com
    +
    + + $postInfo +
    + + $extra + +HTML; + + if( !$resline = mysql_board_call( "SELECT * FROM `$sqlog` WHERE `resto` = '$no' ORDER BY `no`" ) ) { + echo S_SQLFAIL; + } + + $countres = mysql_num_rows( $resline ); + $s = 0; + + + while( $resrow = mysql_fetch_assoc( $resline ) ) { + extract( $resrow ); + //list($no,$sticky,$permasage,$closed,$now,$name,$email,$sub,$com,$host,$pwd,$filename,$ext,$w,$h,$tn_w,$tn_h,$tim,$time,$md5,$fsize,$root,$resto)=$resrow; + + + if( !$no ) { + break; + } + + $emailstart = $emailend = ''; + //if( $email != '' ) { + // $email = emailencode($email); + // $emailstart = ''; + // $emailend = ''; + //} + + // NEW CAPCODE STUFF + + switch( $capcode ) { + case 'admin': + $capcodeStart = ' ## Admin'; + $capcode_class = ' capcodeAdmin'; + + $capcode = ' This user is the 4chan Administrator.'; + $highlight = ''; + break; + + case 'admin_highlight': + $capcodeStart = ' ## Admin'; + $capcode_class = ' capcodeAdmin'; + + $capcode = ' This user is the 4chan Administrator.'; + $highlight = ' highlightPost'; + break; + + case 'mod': + $capcodeStart = ' ## Mod'; + $capcode_class = ' capcodeMod'; + + $capcode = ' This user is a 4chan Moderator.'; + $highlight = ''; + break; + + case 'developer': + $capcodeStart = ' ## Developer'; + $capcode_class = ' capcodeDeveloper'; + + $capcode = ' This user is a 4chan Developer.'; + $highlight = ''; + break; + + case 'manager': + $capcodeStart = ' ## Manager'; + $capcode_class = ' capcodeManager'; + + $capcode = ' Manager Icon'; + $highlight = ''; + break; + + default: + $capcode = $capcodeStart = $highlight = $capcode_class = ''; + break; + } + + $com = auto_link( $com, $no ); + + $subshort = $sub; + + if( mb_strlen( $sub ) > 28 ) { + + $subshort = '' . mb_substr( $sub, 0, 25, 'UTF-8' ) . '(...)'; + + } + + $href = ( $resno ) ? $resto . PHP_EXT2 : RES_DIR2 . $resto . PHP_EXT2; + $quote = ( $resno ) ? 'javascript:quote(\'' . $no . '\');' : $href . '#q' . $no; + + + $dat .= << +
    >>
    +
    + + + +
    $com
    +
    +
    +HTML; + } + + // end thread + $dat .= '
    '; + + //clearstatcache(); + mysql_free_result( $resline ); + $p++; + break; + } else { + + /** BUILD /f/ INDEX **/ + + if( !$resline = mysql_board_call( "SELECT * FROM `$sqlog` WHERE `resto` = '$no' ORDER BY `no`" ) ) { + echo S_SQLFAIL; + } + + $countres = mysql_num_rows( $resline ); + + if( $fsize >= 1048576 ) { + $kbsize = round( ( $fsize / 1048576 ), 2 ) . ' M'; + } elseif( $fsize >= 1024 ) { + $kbsize = round( $fsize / 1024 ) . ' K'; + } else { + $kbsize = $fsize . ' '; + } + $kbsize .= 'B'; + + $emailstart = $emailend = ''; + + + //if( $email != '' ) { + // $email = emailencode($email); + // $emailstart = ''; + // $emailend = ''; + //} + + if( mb_strlen( $filename ) > 25 ) { + $shortname = mb_substr( $filename, 0, 25, 'UTF-8' ) . "(...)"; + } else { + $shortname = $filename; + } + + if( $kbsize != '0 B' ) { + $class = ''; + if( in_array( $no, $delarr ) ) { + $class = 'class="oldpost"'; + } + + + $texttag = $tags[$tag]['short']; + + if ($sub != '') { + $sub = str_replace(',', ',', $sub); + + if (mb_strlen($sub) > 30) { + $shortsub = mb_substr($sub, 0, 30, 'UTF-8') . '(...)'; + } + else { + $shortsub = $sub; + } + } + else { + $com = str_replace(',', ',', $com); + $com = explode('<', $com)[0]; + if (mb_strlen($com) > 30) { + $shortsub = mb_substr($com, 0, 30, 'UTF-8') . '(...)'; + } + else { + $shortsub = $com; + } + } + + $replink = RES_DIR2 . $no . PHP_EXT2; + + $semantic_url = generate_href_context($sub, $com); + + if ($semantic_url != '') { + $semantic_url = "/$semantic_url"; + } + + $filelink = IMG_DIR2 . rawurlencode( $filename ) . '.swf'; + + + // NEW CAPCODE STUFF + + if( $capcode === 'admin' ) { + + $capcodeStart = ' ## Admin'; + + $capcode_class = ' capcodeAdmin'; + + + $capcode = ' This user is the 4chan Administrator.'; + + $highlight = ''; + + } elseif( $capcode === 'mod' ) { + + $capcodeStart = ' ## Mod'; + + $capcode_class = ' capcodeMod'; + + + $capcode = ' This user is a 4chan Moderator.'; + + $highlight = ''; + + } elseif( $capcode === 'admin_highlight' ) { + + $capcodeStart = ' ## Admin'; + + $capcode_class = ' capcodeAdmin'; + + + $capcode = ' This user is the 4chan Administrator.'; + + $highlight = ' highlightPost'; + + } else { + + $capcode = $capcodeStart = $highlight = $capcode_class = ''; + + } + + $oldclass = in_array( $no, $delarr ) ? ' class="highlightPost"' : ''; + + + $dat .= << + + $no + + $emailStart$name$emailEnd$capcodeStart$capcode + [$shortname] + $texttag + $shortsub + $kbsize + $now + $countres + [Reply] + + + +HTML; + } + + //clearstatcache(); + mysql_free_result( $resline ); + } // end /f/ + } // no pages for /f/ + + $lang = $resno ? S_FORM_REPLY : S_FORM_THREAD; + + if( !$resno ) { + $dat .= '
    '; + } else { + $dat .= '
    '; + $dat .= ' + + '; + } + + + if (!$resno) { + $dat .= '
    '; + } + else { + $dat .= '
    '; + } + /* + if( AD_BOTTOM_ENABLE == 1 ) { + $bottomad = ""; + + if( defined( "AD_BOTTOM_TEXT" ) && AD_BOTTOM_TEXT ) { + $bottomad .= '
    ' . ad_text_for( AD_BOTTOM_TEXT ) . '
    ' . (defined('AD_BOTTOM_PLEA') ? AD_BOTTOM_PLEA : ''); + } else if( defined( "AD_BOTTOM_TABLE" ) && AD_BOTTOM_TABLE ) { + list( $bottomimg, $bottomlink ) = rid( AD_BOTTOM_TABLE, 1 ); + $bottomad .= "
    \"\"
    "; + } + + if( $bottomad ) { + $dat .= "$bottomad
    "; + } + } + */ + + /** + * ADS + */ + + if (defined('AD_ADGLARE_BOTTOM') && AD_ADGLARE_BOTTOM) { + $dat .= '

    '; + } + + if (defined('AD_ADGLARE_BOTTOM_MOBILE') && AD_ADGLARE_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + if (defined('AD_RC_BOTTOM') && AD_RC_BOTTOM) { + $dat .= '

    '; + } + + if (defined('AD_RC_BOTTOM_MOBILE') && AD_RC_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + if (defined('AD_ADNIUM_BOTTOM_MOBILE') && AD_ADNIUM_BOTTOM_MOBILE) { + $dat .= '

    '; + } + + $dat .= '
    ' . S_REPDEL . $resredir . ' [' . S_DELPICONLY . '] '; + + if( !defined( 'CSS_FORCE' ) ) { + $dat .= 'Style: + + '; + } + + $dat .= '
    '; + + foot( $dat ); + + if( $page == 0 ) { + $logfilename = SELF_PATH2_FILE; + } + + if( $resno ) { + $logfilename = RES_DIR . $resno . PHP_EXT; + print_page( $logfilename, $dat ); + + if( !$rebuild ) { + updatelog(); + } + } else { + print_page( $logfilename, $dat ); + } + + mysql_free_result( $treeline ); +} + +function form( &$dat, $resno, $admin = '' ) +{ + global $thread_unique_ips; + + $maxbyte = MAX_KB * 1024; + $no = $resno; + $closed = 0; + $msg = $hidden = ''; + $tags = upboard_tags(); + + if( $resno ) { + if( !$cchk = mysql_board_call( "select closed from `" . SQLLOG . "` where no=" . $resno ) ) { + echo S_SQLFAIL; + } + list( $closed ) = mysql_fetch_row( $cchk ); + + $msg .= ' +'; + } + else { + $msg .= ' + +'; + } + + if( $admin ) { + $hidden = ''; + $msg = '

    ' . S_NOTAGS . '

    '; + } + + if( $closed != 1 ) { + $dat .= $msg; + form_ads( $dat ); + + $dat .= ' +
    + + +'; + + if( $no ) { + $dat .= ' + +'; + } + + $dat .= ' + +'; + + // TODO: ADS + + $spoilers = ''; + if( SPOILERS == 1 ) { + $spoilers = '[]'; + } + + if( FORCED_ANON == 1 ) { + $dat .= ' + + + + + '; + + if( $spoilers ) { + + if( !$stripm ) $dat .= ' + + + + + + '; + } + } + else { + $dat .= ' + + + + '; + if ($resno) { + $dat .= ' + + + + '; + } + else { + $dat .= ' + + + + + + + + '; + } + + if( $spoilers ) { + + if( !$stripm ) $dat .= ' + + + + + + '; + } + } + + if( $admin ) { + $dat .= ' + + + + + '; + } + + //if( EXPANDING_POST_ ) + + $dat .= ' + + + + + '; + + if( CAPTCHA ) { + $dat .= ' + + + + + '; + } + + if( !$resno ) { + $dat .= ' + + + + + + + + '; + } + + if ($resno && SHOW_THREAD_UNIQUES) { + $unique = $thread_unique_ips; + if ($unique) { + $unique_plural = $unique > 1 ? 'are' : 'is'; + $unique = sprintf("
  • There $unique_plural " . S_UNIQUE_POSTS_TH . '
  • ', $unique, $unique > 1 ? 's' : ''); + } + else { + $unique = ''; + } + } + elseif (SHOW_UNIQUES) { + $unique = sprintf('
  • ' . S_UNIQUE_POSTS . '
  • ', $GLOBALS['ipcount']); + } + else { + $unique = ''; + } + + // XXX: mode=regist moved to the top + $dat .= ' + + + + '; + + $blotter = $resno ? '' : get_blotter(); + + $dat .= ' + +
    ' . S_EMAIL . '
    Spoilers[]
    ' . S_NAME . '
    ' . S_EMAIL . '
    ' . S_EMAIL . '
    ' . S_SUBJECT . '
    ' . S_SPOILERS . '[]
    Reply ID [
    ' . S_COMMENT . '
    ' . S_CAPTCHA . '' . captcha_form() . '
    ' . S_PASS_NOTICE . '
    ' . S_UPLOADFILE . '
    Tag
    +
      + ' . S_RULES . ' + ' . $unique . ' +
    +
    ' . $blotter . ' +
    +' . DONATE . ' + +'; + } else { // Closed thread + form_ads( $dat ); + $dat .= ' +
    Thread closed.
    You may not reply at this time.
    '; + } + + if( AD_MIDDLE_ENABLE == 1 ) { + $middlead = ""; + + if( defined( "AD_MIDDLE_TEXT" ) && AD_MIDDLE_TEXT ) { + $middlead .= '
    ' . ad_text_for( AD_MIDDLE_TEXT ) . '
    ' . (defined('AD_MIDDLE_PLEA') ? AD_MIDDLE_PLEA : ''); + } else if( defined( "AD_MIDDLE_TABLE" ) && AD_MIDDLE_TABLE ) { + list( $middleimg, $middlelink ) = rid( AD_MIDDLE_TABLE, 1 ); + $middlead .= "
    \"\"
    "; + } + + if( $middlead ) { + $dat .= "$middlead"; + } + } + + list($globalmsgtxt,$globalmsgdate) = global_msg_txt(); + + if( $globalmsgtxt ) { + $dat .= "\n
    View Important Announcement
    " . $globalmsgtxt . "
    \n"; + } + + if (defined('AD_ADGLARE_TOP') && AD_ADGLARE_TOP) { + $dat .= '

    '; + } + else if (defined('AD_RC_TOP') && AD_RC_TOP) { + $dat .= '

    '; + } + else if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP) { + list($_abc_left, $_abc_right) = explode(',', AD_ABC_TOP_DESKTOP); + $dat .= '

    '; + } + + if ($resno) { + $dat .= '
    + + '; + } +} + +?> diff --git a/wordfilters/asp.php b/wordfilters/asp.php new file mode 100644 index 0000000..e9a0d2d --- /dev/null +++ b/wordfilters/asp.php @@ -0,0 +1,103 @@ + 'baka', + 'SMH' => 'BAKA', + 'tbh' => 'desu', + 'TBH' => 'DESU', + 'fam' => 'senpai', + 'FAM' => 'SENPAI', + 'Fam' => 'Senpai', + 'fams' => 'senpaitachi', + 'FAMS' => 'SENPAITACHI', + 'FAMs' => 'SENPAITACHI', + 'Fams' => 'Senpaitachi' + ); + + if (!isset($vals[$m[2]])) { + return $m[0]; + } + + return "{$m[1]}{$vals[$m[2]]}{$m[3]}"; +} + +function word_filter_callback_soy($m) { + $is_uc = $m[2] === strtoupper($m[2]); + + $lc = strtolower($m[4]); + + if ($lc === 'uz') { + return $m[0]; + } + + if ($lc === 'im' || $lc === 'lent') { + $m[4] = ''; + } + + if (!isset($m[4][1])) { + if ($is_uc) { + $onions = 'ONIONS'; + } + else { + if ($m[2][0] === 's') { + $onions = 'onions'; + } + else { + $onions = 'Onions'; + } + } + + return "{$m[1]}{$onions}{$m[4]}"; + } + + if ($m[2][0] === 's') { + $b = 'b'; + } + else { + $b = 'B'; + } + + $ac = mb_strlen($m[3]); + + if ($ac == 1) { + $a = 'a'; + } + else { + if ($ac < 35) { + $a = str_repeat('a', $ac); + } + else { + $a = 'a'; + } + } + + $based = "{$b}{$a}sed"; + + if ($is_uc) { + $based = strtoupper($based); + } + + return $m[1] . $based . $m[4]; +} + +// $text contents of a field +// $type name of the field +function word_filter($text, $type) { + if ($type !== 'com') { + return $text; + } + +// $text = str_replace('Cuck', 'Kek', $text); +// $text = str_replace('cuck', 'kek', $text); + $text = str_replace('CUCK', 'KEK', $text); + + $text = str_replace('finna', 'ding-dong diddly', $text); + $text = str_replace('Finna ', 'Ding-Dong Diddly', $text); + $text = str_replace('FINNA', 'DING-DONG DIDDLY', $text); + + $text = preg_replace_callback('/(\b)(sjw|sjws|smh|tbh|fams|fam)(\b)/i', 'word_filter_callback', $text); + + $text = preg_replace_callback('/(\b)(s([o0οоօჿ]+)[yуΥ])(\b|[[:alpha:]]{2,4})/iu', 'word_filter_callback_soy', $text); + + return $text; +} diff --git a/wordfilters/ck.php b/wordfilters/ck.php new file mode 100644 index 0000000..df0fd7d --- /dev/null +++ b/wordfilters/ck.php @@ -0,0 +1,38 @@ + 'baka', + 'SMH' => 'BAKA', + 'tbh' => 'desu', + 'TBH' => 'DESU', + 'fam' => 'senpai', + 'FAM' => 'SENPAI', + 'Fam' => 'Senpai', + 'fams' => 'senpaitachi', + 'FAMS' => 'SENPAITACHI', + 'FAMs' => 'SENPAITACHI', + 'Fams' => 'Senpaitachi' + ); + + if (!isset($vals[$m[2]])) { + return $m[0]; + } + + return "{$m[1]}{$vals[$m[2]]}{$m[3]}"; +} + +// $text contents of a field +// $type name of the field +function word_filter($text, $type) { + if ($type !== 'com') { + return $text; + } + +// $text = str_replace('Cuck', 'Kek', $text); +// $text = str_replace('cuck', 'kek', $text); + $text = str_replace('CUCK', 'KEK', $text); + + $text = preg_replace_callback('/(\b)(sjw|sjws|smh|tbh|fams|fam)(\b)/i', 'word_filter_callback', $text); + + return $text; +} diff --git a/wordfilters/global.php b/wordfilters/global.php new file mode 100644 index 0000000..22187da --- /dev/null +++ b/wordfilters/global.php @@ -0,0 +1,99 @@ + 'baka', + 'SMH' => 'BAKA', + 'tbh' => 'desu', + 'TBH' => 'DESU', + 'fam' => 'senpai', + 'FAM' => 'SENPAI', + 'Fam' => 'Senpai', + 'fams' => 'senpaitachi', + 'FAMS' => 'SENPAITACHI', + 'FAMs' => 'SENPAITACHI', + 'Fams' => 'Senpaitachi' + ); + + if (!isset($vals[$m[2]])) { + return $m[0]; + } + + return "{$m[1]}{$vals[$m[2]]}{$m[3]}"; +} + +function word_filter_callback_soy($m) { + $is_uc = $m[2] === strtoupper($m[2]); + + $lc = strtolower($m[4]); + + if ($lc === 'uz') { + return $m[0]; + } + + if ($lc === 'im' || $lc === 'lent') { + $m[4] = ''; + } + + if (!isset($m[4][1])) { + if ($is_uc) { + $onions = 'ONIONS'; + } + else { + if ($m[2][0] === 's') { + $onions = 'onions'; + } + else { + $onions = 'Onions'; + } + } + + return "{$m[1]}{$onions}{$m[4]}"; + } + + if ($m[2][0] === 's') { + $b = 'b'; + } + else { + $b = 'B'; + } + + $ac = mb_strlen($m[3]); + + if ($ac == 1) { + $a = 'a'; + } + else { + if ($ac < 35) { + $a = str_repeat('a', $ac); + } + else { + $a = 'a'; + } + } + + $based = "{$b}{$a}sed"; + + if ($is_uc) { + $based = strtoupper($based); + } + + return $m[1] . $based . $m[4]; +} + +// $text contents of a field +// $type name of the field +function word_filter($text, $type) { + if ($type !== 'com') { + return $text; + } + +// $text = str_replace('Cuck', 'Kek', $text); +// $text = str_replace('cuck', 'kek', $text); + $text = str_replace('CUCK', 'KEK', $text); + + $text = preg_replace_callback('/(\b)(sjw|sjws|smh|tbh|fams|fam)(\b)/i', 'word_filter_callback', $text); + + $text = preg_replace_callback('/(\b)(s([o0οоօჿ]+)[yуΥ])(\b|[[:alpha:]]{2,4})/iu', 'word_filter_callback_soy', $text); + + return $text; +} diff --git a/wordfilters/int.php b/wordfilters/int.php new file mode 100644 index 0000000..df0fd7d --- /dev/null +++ b/wordfilters/int.php @@ -0,0 +1,38 @@ + 'baka', + 'SMH' => 'BAKA', + 'tbh' => 'desu', + 'TBH' => 'DESU', + 'fam' => 'senpai', + 'FAM' => 'SENPAI', + 'Fam' => 'Senpai', + 'fams' => 'senpaitachi', + 'FAMS' => 'SENPAITACHI', + 'FAMs' => 'SENPAITACHI', + 'Fams' => 'Senpaitachi' + ); + + if (!isset($vals[$m[2]])) { + return $m[0]; + } + + return "{$m[1]}{$vals[$m[2]]}{$m[3]}"; +} + +// $text contents of a field +// $type name of the field +function word_filter($text, $type) { + if ($type !== 'com') { + return $text; + } + +// $text = str_replace('Cuck', 'Kek', $text); +// $text = str_replace('cuck', 'kek', $text); + $text = str_replace('CUCK', 'KEK', $text); + + $text = preg_replace_callback('/(\b)(sjw|sjws|smh|tbh|fams|fam)(\b)/i', 'word_filter_callback', $text); + + return $text; +} diff --git a/wordfilters/test.php b/wordfilters/test.php new file mode 100644 index 0000000..ad8a213 --- /dev/null +++ b/wordfilters/test.php @@ -0,0 +1,154 @@ + 'baka', + 'SMH' => 'BAKA', + 'tbh' => 'desu', + 'TBH' => 'DESU', + 'fam' => 'senpai', + 'FAM' => 'SENPAI', + 'Fam' => 'Senpai', + 'fams' => 'senpaitachi', + 'FAMS' => 'SENPAITACHI', + 'FAMs' => 'SENPAITACHI', + 'Fams' => 'Senpaitachi' + ); + + if (!isset($vals[$m[2]])) { + return $m[0]; + } + + return "{$m[1]}{$vals[$m[2]]}{$m[3]}"; +} + +function word_filter_callback_soy($m) { + $is_uc = $m[2] === strtoupper($m[2]); + + $lc = strtolower($m[4]); + + if ($lc === 'uz') { + return $m[0]; + } + + if ($lc === 'im' || $lc === 'lent') { + $m[4] = ''; + } + + if (!isset($m[4][1])) { + if ($is_uc) { + $onions = 'ONIONS'; + } + else { + if ($m[2][0] === 's') { + $onions = 'onions'; + } + else { + $onions = 'Onions'; + } + } + + return "{$m[1]}{$onions}{$m[4]}"; + } + + if ($m[2][0] === 's') { + $b = 'b'; + } + else { + $b = 'B'; + } + + $ac = mb_strlen($m[3]); + + if ($ac == 1) { + $a = 'a'; + } + else { + if ($ac < 35) { + $a = str_repeat('a', $ac); + } + else { + $a = 'a'; + } + } + + $based = "{$b}{$a}sed"; + + if ($is_uc) { + $based = strtoupper($based); + } + + return $m[1] . $based . $m[4]; +} + +function april_leet_filter($text) { + $pairs = [ + [ + 'a' => '4', + 'A' => '4' + ], + [ + 'e' => '3', + 'E' => '3' + ], + [ + 'i' => '1', + 'I' => '1' + ], + [ + 'o' => '0', + 'O' => '0' + ], + [ + 's' => '5', + 'S' => '5' + ], + [ + 'T' => '7', + 't' => '7' + ] + ]; + + $roll1 = mt_rand(0, 5); + $roll2 = mt_rand(0, 5); + + + if ($roll1 === 5) { + $text = preg_replace('/([^gl])[tT]/', '${1}7', $text); + } + else { + $repl = $pairs[$roll1]; + $text = strtr($text, $repl); + } + + if ($roll2 != $roll1) { + if ($roll2 === 5) { + $text = preg_replace('/([^gl])[tT]/', '${1}7', $text); + } + else { + $repl = $pairs[$roll2]; + $text = strtr($text, $repl); + } + } + + return $text; +} + +// $text contents of a field +// $type name of the field +function word_filter($text, $type) { + if ($type !== 'com') { + return $text; + } + +// $text = str_replace('Cuck', 'Kek', $text); +// $text = str_replace('cuck', 'kek', $text); + $text = str_replace('CUCK', 'KEK', $text); + + $text = preg_replace_callback('/(\b)(sjw|sjws|smh|tbh|fams|fam)(\b)/i', 'word_filter_callback', $text); + + $text = preg_replace_callback('/(\b)(s([o0οоօჿ]+)[yуΥ])(\b|[[:alpha:]]{2,4})/iu', 'word_filter_callback_soy', $text); + + $text = april_leet_filter($text); + + return $text; +} diff --git a/wordfilters/v.php b/wordfilters/v.php new file mode 100644 index 0000000..2d033d3 --- /dev/null +++ b/wordfilters/v.php @@ -0,0 +1,201 @@ + 'baka', + 'SMH' => 'BAKA', + 'tbh' => 'desu', + 'TBH' => 'DESU', + 'fam' => 'senpai', + 'FAM' => 'SENPAI', + 'Fam' => 'Senpai', + 'fams' => 'senpaitachi', + 'FAMS' => 'SENPAITACHI', + 'FAMs' => 'SENPAITACHI', + 'Fams' => 'Senpaitachi' + ); + + if (!isset($vals[$m[2]])) { + return $m[0]; + } + + return "{$m[1]}{$vals[$m[2]]}{$m[3]}"; +} + +function word_filter_consoles($text) { + static $from = array( + 'pcfat', + 'pcuck', + 'pccuck', + 'valvedrone', + 'sonynigger', + 'sonygger', + 'sonydrone', + 'sonycuck', + 'sonypony', + 'nintencuck', + 'nintoddler', + 'nintendotoddler', + 'nintendrone', + 'nintenyearold', + 'nintendroid', + 'nintenshit', + 'Pcfat', + 'Pcuck', + 'PCuck', + 'Pccuck', + 'Valvedrone', + 'Sonynigger', + 'Sonygger', + 'Sonydrone', + 'Sonycuck', + 'Sonypony', + 'Nintencuck', + 'Nintoddler', + 'Nintendotoddler', + 'Nintendrone', + 'Nintenyearold', + 'Nintendroid', + 'Nintenshit', + 'nintendr0ne', + 'Nintendr0ne', + 'sonybrony', + 'Sonybrony', + 'sonybronies', + 'Sonybronies', + 'sonypony', + 'Sonypony', + 'sonyponies', + 'Sonyponies', + 'sonigger', + 'Sonigger' + ); + + static $to = array( + 'pcbro', + 'pcbro', + 'pcbro', + 'pcbro', + 'sonybro', + 'sonybro', + 'sonybro', + 'sonybro', + 'sonybro', + 'nintenbro', + 'nintenbro', + 'nintenbro', + 'nintenbro', + 'nintenbro', + 'nintenbro', + 'nintenbro', + 'Pcbro', + 'Pcbro', + 'PCbro', + 'Pcbro', + 'Pcbro', + 'Sonybro', + 'Sonybro', + 'Sonybro', + 'Sonybro', + 'Sonybro', + 'Nintenbro', + 'Nintenbro', + 'Nintenbro', + 'Nintenbro', + 'Nintenbro', + 'Nintenbro', + 'Nintenbro', + 'nintenbro', + 'Nintenbro', + 'sonybro', + 'Sonybro', + 'sonybros', + 'Sonybros', + 'sonybro', + 'Sonybro', + 'sonybros', + 'Sonybros', + 'sonybro', + 'Sonybro' + ); + + return str_replace($from, $to, $text); +} + +function word_filter_callback_soy($m) { + $is_uc = $m[2] === strtoupper($m[2]); + + $lc = strtolower($m[4]); + + if ($lc === 'uz') { + return $m[0]; + } + + if ($lc === 'im' || $lc === 'lent') { + $m[4] = ''; + } + + if (!isset($m[4][1])) { + if ($is_uc) { + $onions = 'ONIONS'; + } + else { + if ($m[2][0] === 's') { + $onions = 'onions'; + } + else { + $onions = 'Onions'; + } + } + + return "{$m[1]}{$onions}{$m[4]}"; + } + + if ($m[2][0] === 's') { + $b = 'b'; + } + else { + $b = 'B'; + } + + $ac = mb_strlen($m[3]); + + if ($ac == 1) { + $a = 'a'; + } + else { + if ($ac < 35) { + $a = str_repeat('a', $ac); + } + else { + $a = 'a'; + } + } + + $based = "{$b}{$a}sed"; + + if ($is_uc) { + $based = strtoupper($based); + } + + return $m[1] . $based . $m[4]; +} + +// $text contents of a field +// $type name of the field +function word_filter($text, $type) { + if ($type !== 'com') { + return $text; + } + +// $text = str_replace('Cuck', 'Kek', $text); +// $text = str_replace('cuck', 'kek', $text); + $text = str_replace('CUCK', 'KEK', $text); + + $text = preg_replace_callback('/(\b)(sjw|sjws|smh|tbh|fams|fam)(\b)/i', 'word_filter_callback', $text); + + $text = preg_replace_callback('/(\b)(s([o0οоօჿ]+)[yуΥ])(\b|[[:alpha:]]{2,4})/iu', 'word_filter_callback_soy', $text); + + $text = word_filter_consoles($text); + + return $text; +} diff --git a/wordfilters/vg.php b/wordfilters/vg.php new file mode 100644 index 0000000..22187da --- /dev/null +++ b/wordfilters/vg.php @@ -0,0 +1,99 @@ + 'baka', + 'SMH' => 'BAKA', + 'tbh' => 'desu', + 'TBH' => 'DESU', + 'fam' => 'senpai', + 'FAM' => 'SENPAI', + 'Fam' => 'Senpai', + 'fams' => 'senpaitachi', + 'FAMS' => 'SENPAITACHI', + 'FAMs' => 'SENPAITACHI', + 'Fams' => 'Senpaitachi' + ); + + if (!isset($vals[$m[2]])) { + return $m[0]; + } + + return "{$m[1]}{$vals[$m[2]]}{$m[3]}"; +} + +function word_filter_callback_soy($m) { + $is_uc = $m[2] === strtoupper($m[2]); + + $lc = strtolower($m[4]); + + if ($lc === 'uz') { + return $m[0]; + } + + if ($lc === 'im' || $lc === 'lent') { + $m[4] = ''; + } + + if (!isset($m[4][1])) { + if ($is_uc) { + $onions = 'ONIONS'; + } + else { + if ($m[2][0] === 's') { + $onions = 'onions'; + } + else { + $onions = 'Onions'; + } + } + + return "{$m[1]}{$onions}{$m[4]}"; + } + + if ($m[2][0] === 's') { + $b = 'b'; + } + else { + $b = 'B'; + } + + $ac = mb_strlen($m[3]); + + if ($ac == 1) { + $a = 'a'; + } + else { + if ($ac < 35) { + $a = str_repeat('a', $ac); + } + else { + $a = 'a'; + } + } + + $based = "{$b}{$a}sed"; + + if ($is_uc) { + $based = strtoupper($based); + } + + return $m[1] . $based . $m[4]; +} + +// $text contents of a field +// $type name of the field +function word_filter($text, $type) { + if ($type !== 'com') { + return $text; + } + +// $text = str_replace('Cuck', 'Kek', $text); +// $text = str_replace('cuck', 'kek', $text); + $text = str_replace('CUCK', 'KEK', $text); + + $text = preg_replace_callback('/(\b)(sjw|sjws|smh|tbh|fams|fam)(\b)/i', 'word_filter_callback', $text); + + $text = preg_replace_callback('/(\b)(s([o0οоօჿ]+)[yуΥ])(\b|[[:alpha:]]{2,4})/iu', 'word_filter_callback_soy', $text); + + return $text; +} diff --git a/wordfilters/vp.php b/wordfilters/vp.php new file mode 100644 index 0000000..22187da --- /dev/null +++ b/wordfilters/vp.php @@ -0,0 +1,99 @@ + 'baka', + 'SMH' => 'BAKA', + 'tbh' => 'desu', + 'TBH' => 'DESU', + 'fam' => 'senpai', + 'FAM' => 'SENPAI', + 'Fam' => 'Senpai', + 'fams' => 'senpaitachi', + 'FAMS' => 'SENPAITACHI', + 'FAMs' => 'SENPAITACHI', + 'Fams' => 'Senpaitachi' + ); + + if (!isset($vals[$m[2]])) { + return $m[0]; + } + + return "{$m[1]}{$vals[$m[2]]}{$m[3]}"; +} + +function word_filter_callback_soy($m) { + $is_uc = $m[2] === strtoupper($m[2]); + + $lc = strtolower($m[4]); + + if ($lc === 'uz') { + return $m[0]; + } + + if ($lc === 'im' || $lc === 'lent') { + $m[4] = ''; + } + + if (!isset($m[4][1])) { + if ($is_uc) { + $onions = 'ONIONS'; + } + else { + if ($m[2][0] === 's') { + $onions = 'onions'; + } + else { + $onions = 'Onions'; + } + } + + return "{$m[1]}{$onions}{$m[4]}"; + } + + if ($m[2][0] === 's') { + $b = 'b'; + } + else { + $b = 'B'; + } + + $ac = mb_strlen($m[3]); + + if ($ac == 1) { + $a = 'a'; + } + else { + if ($ac < 35) { + $a = str_repeat('a', $ac); + } + else { + $a = 'a'; + } + } + + $based = "{$b}{$a}sed"; + + if ($is_uc) { + $based = strtoupper($based); + } + + return $m[1] . $based . $m[4]; +} + +// $text contents of a field +// $type name of the field +function word_filter($text, $type) { + if ($type !== 'com') { + return $text; + } + +// $text = str_replace('Cuck', 'Kek', $text); +// $text = str_replace('cuck', 'kek', $text); + $text = str_replace('CUCK', 'KEK', $text); + + $text = preg_replace_callback('/(\b)(sjw|sjws|smh|tbh|fams|fam)(\b)/i', 'word_filter_callback', $text); + + $text = preg_replace_callback('/(\b)(s([o0οоօჿ]+)[yуΥ])(\b|[[:alpha:]]{2,4})/iu', 'word_filter_callback_soy', $text); + + return $text; +} diff --git a/xa24tb.php b/xa24tb.php new file mode 100644 index 0000000..5fb233c --- /dev/null +++ b/xa24tb.php @@ -0,0 +1,379 @@ + $msg]); + die(); +} + +function create_account() { + $user_ip = $_SERVER['REMOTE_ADDR']; + + if (isset($_COOKIE['4chan_pass'])) { + $userpwd = new UserPwd($user_ip, '4chan.org', $_COOKIE['4chan_pass']); + } + else { + $userpwd = new UserPwd($user_ip, '4chan.org'); + } + + $user_id = $userpwd->getPwd(); + + $sql = "SELECT id FROM april_stock_users WHERE user_id = '%s' LIMIT 1"; + + $res = mysql_global_call($sql, $user_id); + + if (!$res) { + output_error('Internal Server Error (frac1)'); + } + + if (mysql_num_rows($res)) { + $account = get_account_balance($user_id); + output_json($account); + die(); + } + + $cur_code = CURRENCY_CODE; + $cur_amount = (int)STARTING_AMOUNT; + + $sql =<<setCookie('.4chan.org'); + + output_json([ + 'balance' => $cur_amount + ]); +} + +function get_account_balance($user_id) { + $sql = "SELECT stock, SUM(amount) as amount FROM april_stock_users WHERE user_id = '%s' GROUP BY stock"; + + $res = mysql_global_call($sql, $user_id); + + $data = []; + + while ($row = mysql_fetch_assoc($res)) { + if ($row['stock'] == CURRENCY_CODE) { + $amount = (int)$row['amount']; + + if ($amount < 0) { + $amount = 0; + } + + $data['balance'] = $amount; + } + else if (in_array($row['stock'], STOCK_LIST)) { + $amount = (int)$row['amount']; + + if ($amount <= 0) { + continue; + } + + $data[$row['stock']] = $amount; + } + } + + return $data; +} + +function get_account() { + $user_ip = $_SERVER['REMOTE_ADDR']; + + if (!isset($_COOKIE['4chan_pass'])) { + output_error('Account not found'); + } + + $userpwd = new UserPwd($user_ip, '4chan.org', $_COOKIE['4chan_pass']); + + if ($userpwd->isNew()) { + output_error('Account not found'); + } + + $user_id = $userpwd->getPwd(); + + $data = get_account_balance($user_id); + + return [$user_id, $data]; +} + +function get_stock_price($stock) { + if ($stock == CURRENCY_CODE) { + output_error('Stock not found'); + } + + $sql = "SELECT price FROM april_stock_prices WHERE stock = '%s' ORDER BY id DESC LIMIT 1"; + + $res = mysql_global_call($sql, $stock); + + if (!$res) { + return false; + } + + $price = (int)mysql_fetch_row($res)[0]; + + if ($price <= 0) { + return false; + } + + return $price; +} + +function get_stock_http_param() { + if (!isset($_POST['stock']) || !$_POST['stock'] || $_POST['stock'] == CURRENCY_CODE) { + return false; + } + + return $_POST['stock']; +} + +function get_amount_http_param() { + if (!isset($_POST['amount']) || !$_POST['amount']) { + return false; + } + + $amount = (int)$_POST['amount']; + + if ($amount < 1 || $amount > MAX_BUY_SELL_SIZE) { + return false; + } + + return $amount; +} + +function get_price_http_param() { + if (!isset($_POST['price']) || !$_POST['price']) { + return false; + } + + $price = (int)$_POST['price']; + + if ($price < 1) { + return false; + } + + return $price; +} + +function enforce_cooldown($user_id) { + $sql =<< DATE_SUB(NOW(), INTERVAL 10 SECOND) +SQL; + + $res = mysql_global_call($sql, $user_id); + + if (!$res) { + return true; + } + + if (mysql_num_rows($res)) { + output_error('You can only make an order once every 10 seconds'); + } + + return false; +} + +/** + * BUY + */ + +function buy_stock() { + $stock = get_stock_http_param(); + + if (!$stock) { + output_error('Stock not found'); + } + + $amount = get_amount_http_param(); + + if (!$amount) { + output_error('Invalid amount'); + } + + $user_price = get_price_http_param(); + + if (!$user_price) { + output_error('Invalid price'); + } + + $price = get_stock_price($stock); + + if (!$price) { + output_error('Invalid price'); + } + + if ($user_price != $price) { + output_error('The price has changed.'); + } + + $total_price = $amount * $price; + + list($user_id, $account) = get_account(); + + if ($total_price > $account['balance']) { + output_error('Your account balance is too low'); + } + + enforce_cooldown($user_id); + + // Decrement balance + $cur_code = CURRENCY_CODE; + + $sql =<< $account[$stock]) { + output_error('Your account balance is too low'); + } + + // Cooldown + enforce_cooldown($user_id); + + // Decrement the stock amount + $sql =<<fetch(); +} +else { + $query = mysql_global_call( "SELECT name,db from boardlist WHERE dir = '%s'", $board ); + $row = mysql_fetch_assoc( $query ); +} + +if( !$row ) { + $row = array( 'db' => 1 ); +} +//maybe get our default board title from DB +if( isset( $row[ 'name' ] ) ) { + $title = $row[ 'name' ]; + $constants[ 'TITLE' ] = "/$board/ - $title"; +} +if( !defined( 'SQLHOST' ) ) { + $constants[ 'SQLHOST' ] = "db-ena.int"; +} // was db$row['db'] +if( !defined( 'SQLDB' ) ) { + $constants[ 'SQLDB' ] = "img{$row[ 'db' ]}"; +} + +load_board_config( $board, $subdomain ); + +finalize_constants(); +define("YES", TRUE); +define("NO", FALSE); + +if( basename( $_SERVER[ 'SCRIPT_NAME' ] ) == basename( __FILE__ ) ) { + print ""; + $cxs = get_defined_constants( true ); + print_r( $cxs[ 'user' ] ); +} + +if( !$no_unset ) unset( $title, $constants, $board, $subdomain, $pathcomps, $CONFIG_PATTERN, $query, $row, $fakecwd );