#lang racket/base (require racket/string json (prefix-in easy: net/http-easy) html-writing web-server/http "config.rkt" "data.rkt" "xexpr-utils.rkt" "url-utils.rkt") (provide ; headers to always send on all http responses always-headers ; timeout durations for http-easy requests timeouts ; generates a consistent footer application-footer ; generates a consistent template for wiki page content to sit in generate-wiki-page ; generates a minimal but complete redirect to another page generate-redirect) (module+ test (require rackunit html-writing)) (define always-headers (list (header #"Referrer-Policy" #"same-origin"))) ; header to not send referers to fandom (define timeouts (easy:make-timeout-config #:lease 5 #:connect 5)) (define (application-footer source-url #:license [license-in #f]) (define license (or license-in license-default)) `(footer (@ (class "custom-footer")) (div (@ (class ,(if source-url "custom-footer__cols" "internal-footer"))) (div (p (img (@ (class "my-logo") (src "/static/breezewiki.svg")))) (p (a (@ (href "https://gitdab.com/cadence/breezewiki")) ,(format "~a source code" (config-get 'application_name)))) (p (a (@ (href "https://docs.breezewiki.com")) "Documentation and more information")) (p (a (@ (href "https://lists.sr.ht/~cadence/breezewiki-discuss")) "Discussions / Bug reports / Feature requests")) ,(if (config-true? 'instance_is_official) `(p ,(format "This instance is run by the ~a developer, " (config-get 'application_name)) (a (@ (href "https://cadence.moe/contact")) "Cadence.")) `(p ,(format "This unofficial instance is based off the ~a source code, but is not controlled by the code developer." (config-get 'application_name))))) ,(if source-url `(div (p "This page displays proxied content from " (a (@ (href ,source-url) (rel "noreferrer")) ,source-url) ,(format ". Text content is available under the ~a license, " (license^-text license)) (a (@ (href ,(license^-url license))) "see license info.") " Media files may have different copying restrictions.") (p ,(format "Fandom is a trademark of Fandom, Inc. ~a is not affiliated with Fandom." (config-get 'application_name)))) `(div (p "Text content on wikis run by Fandom is available under the Creative Commons Attribution-Share Alike License 3.0 (Unported), " (a (@ (href "https://www.fandom.com/licensing")) "see license info.") " Media files and official Fandom documents have different copying restrictions.") (p ,(format "Fandom is a trademark of Fandom, Inc. ~a is not affiliated with Fandom." (config-get 'application_name)))))))) (define (generate-wiki-page content #:source-url source-url #:wikiname wikiname #:title title #:body-class [body-class-in #f] #:siteinfo [siteinfo-in #f]) (define siteinfo (or siteinfo-in siteinfo-default)) (define body-class (if (not body-class-in) "skin-fandomdesktop" body-class-in)) (define (required-styles origin) (map (λ (dest-path) (define url (format dest-path origin)) (if (config-true? 'strict_proxy) (u-proxy-url url) url)) '(#;"~a/load.php?lang=en&modules=skin.fandomdesktop.styles&only=styles&skin=fandomdesktop" #;"~a/load.php?lang=en&modules=ext.gadget.dungeonsWiki%2CearthWiki%2Csite-styles%2Csound-styles&only=styles&skin=fandomdesktop" #;"~a/load.php?lang=en&modules=site.styles&only=styles&skin=fandomdesktop" ; combine the above entries into a single request for potentially extra speed - fandom.com doesn't even do this! "~a/wikia.php?controller=ThemeApi&method=themeVariables" "~a/load.php?lang=en&modules=skin.fandomdesktop.styles%7Cext.fandom.PortableInfoboxFandomDesktop.css%7Cext.fandom.GlobalComponents.CommunityHeaderBackground.css%7Cext.gadget.site-styles%2Csound-styles%7Csite.styles&only=styles&skin=fandomdesktop"))) `(html (head (meta (@ (name "viewport") (content "width=device-width, initial-scale=1"))) (title ,(format "~a | ~a+~a" title (regexp-replace #rx" ?Wiki$" (siteinfo^-sitename siteinfo) "") (config-get 'application_name))) ,@(map (λ (url) `(link (@ (rel "stylesheet") (type "text/css") (href ,url)))) (required-styles (format "https://~a.fandom.com" wikiname))) (link (@ (rel "stylesheet") (type "text/css") (href "/static/main.css"))) (script "const BWData = " ,(jsexpr->string (hasheq 'wikiname wikiname 'strict_proxy (config-true? 'strict_proxy)))) ,(if (config-true? 'feature_search_suggestions) '(script (@ (type "module") (src "/static/search-suggestions.js"))) "")) (body (@ (class ,body-class)) (div (@ (class "main-container")) (div (@ (class "fandom-community-header__background tileHorizontally header"))) (div (@ (class "page")) (main (@ (class "page__main")) (div (@ (class "custom-top")) (h1 (@ (class "page-title")) ,title) (nav (@ (class "sitesearch")) (form (@ (action ,(format "/~a/search" wikiname)) (class "bw-search-form") (id "bw-pr-search-form")) (label (@ (for "bw-search-input")) "Search ") (div (@ (id "bw-pr-search-input")) (input (@ (type "text") (name "q") (id "bw-search-input") (autocomplete "off")))) (div (@ (class "bw-ss__container") (id "bw-pr-search-suggestions")))))) (div (@ (id "content") #;(class "page-content")) (div (@ (id "mw-content-text")) ,content)) ,(application-footer source-url #:license (siteinfo^-license siteinfo)))))))) (module+ test (define page (parameterize ([(config-parameter 'strict_proxy) "true"]) (generate-wiki-page '(template) #:source-url "" #:title "test" #:wikiname "test"))) ; check the page is a valid xexp (check-not-false (xexp->html page)) ; check the stylesheet is proxied (check-true (string-prefix? (get-attribute 'href (bits->attributes ((query-selector (λ (t a c) (eq? t 'link)) page)))) "/proxy?dest=https%3A%2F%2Ftest.fandom.com"))) (define (generate-redirect dest) (define dest-bytes (string->bytes/utf-8 dest)) (response/output #:code 302 #:headers (list (header #"Location" dest-bytes)) (λ (out) (write-html `(html (head (title "Redirecting...")) (body "Redirecting to " (a (@ (href ,dest)) ,dest) "...")) out))))