diff --git a/breezewiki.rkt b/breezewiki.rkt index 17b619b..6b3d5eb 100644 --- a/breezewiki.rkt +++ b/breezewiki.rkt @@ -27,9 +27,7 @@ (require-reloadable "src/page-static-archive.rkt" page-static-archive) (require-reloadable "src/page-subdomain.rkt" subdomain-dispatcher) (require-reloadable "src/page-wiki.rkt" page-wiki) -(require-reloadable "src/page-wiki.rkt" page-wiki-with-data) (require-reloadable "src/page-wiki-offline.rkt" page-wiki-offline) -(require-reloadable "src/page-wiki-jsonp.rkt" page-wiki-jsonp) (require-reloadable "src/page-file.rkt" page-file) (reload!) @@ -59,9 +57,7 @@ page-set-user-settings page-static-archive page-wiki - page-wiki-with-data page-wiki-offline - page-wiki-jsonp page-file redirect-wiki-home static-dispatcher diff --git a/dist.rkt b/dist.rkt index 491ca80..1e7096e 100644 --- a/dist.rkt +++ b/dist.rkt @@ -16,9 +16,8 @@ (require (only-in "src/page-static.rkt" static-dispatcher)) (require (only-in "src/page-static-archive.rkt" page-static-archive)) (require (only-in "src/page-subdomain.rkt" subdomain-dispatcher)) -(require (only-in "src/page-wiki.rkt" page-wiki page-wiki-with-data)) +(require (only-in "src/page-wiki.rkt" page-wiki)) (require (only-in "src/page-wiki-offline.rkt" page-wiki-offline)) -(require (only-in "src/page-wiki-jsonp.rkt" page-wiki-jsonp)) (require (only-in "src/page-file.rkt" page-file)) (serve/launch/wait @@ -43,8 +42,6 @@ page-static-archive page-wiki page-wiki-offline - page-wiki-with-data - page-wiki-jsonp page-file redirect-wiki-home static-dispatcher diff --git a/lib/tree-updater.rkt b/lib/tree-updater.rkt index f941285..c1af714 100644 --- a/lib/tree-updater.rkt +++ b/lib/tree-updater.rkt @@ -12,7 +12,7 @@ update-tree-wiki) (define (preprocess-html-wiki html) - (regexp-replace* #rx"(<(?:td|figcaption)[^>]*?>\n?)(?:
  • |[ \t]*?

    (.*?)

    )" + (regexp-replace* #rx"(<(?:td|figcaption)[^>]*?>\n?)(?:[ \t]*)?(?:
  • |[ \t]*?

    (.*?)

    )" html (λ (whole first-tag [contents #f]) (if (eq? (string-ref whole 1) #\f) ;; figcaption (string-append first-tag "" contents "") diff --git a/src/dispatcher-tree.rkt b/src/dispatcher-tree.rkt index 0bbf8c8..fb9fa1f 100644 --- a/src/dispatcher-tree.rkt +++ b/src/dispatcher-tree.rkt @@ -55,6 +55,7 @@ (define tree (sequencer:make subdomain-dispatcher + (pathprocedure:make "/buddyfight/wiki/It_Doesn't_Work!!" (page ds page-it-works)) (pathprocedure:make "/" (page ds page-home)) (filter:make #rx"^/static/" (hash-ref ds 'static-dispatcher)) (filter:make (pregexp "^/captcha/img/[0-9]+/[0-9]+$") (lift:make (page ds page-captcha-image))) @@ -65,16 +66,11 @@ (pathprocedure:make "/proxy" (page ds page-proxy)) (pathprocedure:make "/search" (page ds page-global-search)) (pathprocedure:make "/set-user-settings" (page ds page-set-user-settings)) - (pathprocedure:make "/buddyfight/wiki/It_Doesn't_Work!!" (page ds page-it-works)) - (pathprocedure:make "/api/render/wiki" (page ds page-wiki-with-data)) (filter:make (pregexp (format "^/~a/wiki/Category:.+$" px-wikiname)) (lift:make (page ds page-category))) (filter:make (pregexp (format "^/~a/wiki/File:.+$" px-wikiname)) (lift:make (page ds page-file))) (if (config-true? 'feature_offline::enabled) (filter:make (pregexp (format "^/~a/wiki/.+$" px-wikiname)) (lift:make (page ds page-wiki-offline))) (λ (_conn _req) (next-dispatcher))) - (if (config-true? 'feature_jsonp::enabled) - (filter:make (pregexp (format "^/~a/wiki/.+$" px-wikiname)) (lift:make (page ds page-wiki-jsonp))) - (λ (_conn _req) (next-dispatcher))) (filter:make (pregexp (format "^/~a/wiki/.+$" px-wikiname)) (lift:make (page ds page-wiki))) (filter:make (pregexp (format "^/~a/search$" px-wikiname)) (lift:make (page ds page-search))) (filter:make (pregexp (format "^/~a(/(wiki(/)?)?)?$" px-wikiname)) (lift:make (page ds redirect-wiki-home))) diff --git a/src/endpoints.rkt b/src/endpoints.rkt new file mode 100644 index 0000000..7b2115a --- /dev/null +++ b/src/endpoints.rkt @@ -0,0 +1,248 @@ +#lang racket/base +(require racket/format + racket/match + racket/string + ; libs + (prefix-in easy: net/http-easy) + json + ; html libs + html-writing + ; web server libs + web-server/http + web-server/dispatchers/dispatch + ; my libs + "application-globals.rkt" + "config.rkt" + "data.rkt" + "fandom-request.rkt" + "static-data.rkt" + "../lib/archive-file-mappings.rkt" + "../lib/thread-utils.rkt" + "../lib/url-utils.rkt" + "../lib/xexpr-utils.rkt") +(require (for-syntax racket/base syntax/parse)) + +(provide + define-endpoint + define-standard-handler + define-post-data-handler + define-jsonp-handler + make-switch-handler + (all-from-out racket/format + racket/match + json + net/http-easy + web-server/dispatchers/dispatch + "../lib/thread-utils.rkt" + "../lib/archive-file-mappings.rkt" + "config.rkt" + "data.rkt" + "fandom-request.rkt" + "static-data.rkt")) + + +(define-for-syntax (fix-scopes here orig stx) + (define remover (make-syntax-delta-introducer here #f)) + (define introducer (make-syntax-delta-introducer orig #f)) + (introducer (remover stx 'remove))) + + +(define-syntax (define-endpoint stx) + (syntax-parse stx + [(_ name + (~and (~seq all ...) + (~seq + ((~datum variables) variable ...) + ((~datum endpoints) endpoint ...) + ((~datum render) render ...)))) + #'(define-syntax name #'(all ...))])) + + +(define ((make-switch-handler #:standard standard #:jsonp jsonp #:post post) req) + (cond + [(equal? (request-method req) #"POST") + (post req)] + [(config-true? 'feature_jsonp::enabled) + (jsonp req)] + [else + (standard req)])) + + +(define-syntax (define-standard-handler stx) + (syntax-parse stx + [(_ (~and name-args (name:id arg:id ...)) endpoint-ref:expr fn-body ...) + #:with endpoint-syntax (syntax-local-value (cadr (syntax->list #'endpoint-ref))) + (syntax-parse #'endpoint-syntax + #:context 'endpoint-data + [(((~datum variables) variable ...) + ((~datum endpoints) (endpoint-id:id (endpoint-param:expr ...)) ...) + ((~datum render) render ...)) + #:with endpoint-getters (for/list ([id (syntax-e #'(endpoint-id ...))] + [params (syntax-e #'((endpoint-param ...) ...))]) + (with-syntax ([params (append (syntax-e params) #;'(("callback" . "proxy.wikipage")))]) + (if (eq? (syntax-e id) 'siteinfo) + #'(λ () + (siteinfo-fetch wikiname)) + #'(λ () + (fandom-get-api + wikiname + `params + #:headers `#hasheq((cookie . ,(format "theme=~a" (user-cookies^-theme user-cookies))))))))) + #:with endpoint-intermediates->values (for/list ([id (syntax-e #'(endpoint-id ...))]) + (with-syntax ([id id]) + (if (eq? (syntax-e #'id) 'siteinfo) + #'id + #'(easy:response-json id)))) + + (fix-scopes + #'here stx + #'(define name-args + (response-handler + (let/cc k + variable ... + (define user-cookies (user-cookies-getter req)) + (define-values (endpoint-id ...) + (let-values ([(endpoint-id ...) (thread-values (~@ . endpoint-getters))]) + (for ([response (list endpoint-id ...)] + #:when (easy:response? response)) + (match (easy:response-status-code response) + [404 + (next-dispatcher)] + [(or 403 406) + (define body + (generate-wiki-page + `(div + (p "Sorry! Fandom isn't allowing BreezeWiki to show pages right now.") + (p "We'll automatically try again in 30 seconds, so please stay on this page and be patient.") + (p (small "In a hurry? " (a (@ (href ,source-url)) "Click here to read the page on Fandom.")))) + #:req req + #:source-url source-url + #:wikiname wikiname + #:title title + #:siteinfo siteinfo)) + (k (response/output + #:code 503 + #:headers (build-headers + always-headers + (header #"Retry-After" #"30") + (header #"Cache-Control" #"max-age=30, public") + (header #"Refresh" #"35")) + (λ (out) + (write-html body out))))] + [(not 200) + (k (error 'page-wiki "Tried to load page ~a/~a~nSadly, the page didn't load because Fandom returned status code ~a with response:~n~a" + wikiname + path + (easy:response-status-code response) + (easy:response-body response)))] + [_ (void)])) + (values (~@ . endpoint-intermediates->values)))) + fn-body ... + render ...))))])])) + + +(define-syntax (define-post-data-handler stx) + (syntax-parse stx + [(_ (~and name-args (name:id arg:id ...)) endpoint-ref:expr fn-body ...) + #:with endpoint-syntax (syntax-local-value (cadr (syntax->list #'endpoint-ref))) + (syntax-parse #'endpoint-syntax + #:context 'endpoint-data + [(((~datum variables) variable ...) + ((~datum endpoints) (endpoint-id:id (endpoint-param:expr ...)) ...) + ((~datum render) render ...)) + #:with endpoint-getters (for/list ([id (syntax-e #'(endpoint-id ...))] + [params (syntax-e #'((endpoint-param ...) ...))]) + (with-syntax ([id id]) + (if (eq? (syntax-e #'id) 'siteinfo) + #'(data->siteinfo (hash-ref post-data 'id)) + #'(hash-ref post-data 'id)))) + + (fix-scopes + #'here stx + #'(define name-args + (response-handler + (let/cc k + (define (k-validation-error message) + (k (response/jsexpr + #:code 400 + #:headers always-headers + `#hasheq((error . + #hasheq((code . "breezewiki") + (info . ,message))))))) + (define post-data/bytes (request-post-data/raw req)) + (when (not post-data/bytes) + (k-validation-error "POST requests only, please.")) + + (define origin-header + (or (headers-assq* #"origin" (request-headers/raw req)) + (headers-assq* #"referer" (request-headers/raw req)))) + (when (or (not origin-header) (not (string-prefix? (bytes->string/latin-1 (header-value origin-header)) (config-get 'canonical_origin)))) + (k-validation-error "Origin/Referer header failed validation - cross-origin requests are not allowed here")) + + (define post-data/string (bytes->string/utf-8 post-data/bytes)) + (define post-data (string->jsexpr post-data/string)) + (define-values (endpoint-id ...) (values (~@ . endpoint-getters))) + (define wikiname (hash-ref post-data 'wikiname)) + (define path (hash-ref post-data 'path)) + (define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path)) + fn-body ... + render ...))))])])) + + +(define-syntax (define-jsonp-handler stx) + (syntax-parse stx + [(_ (~and name-args (name:id arg:id ...)) endpoint-ref:expr fn-body ...) + #:with endpoint-syntax (syntax-local-value (cadr (syntax->list #'endpoint-ref))) + (syntax-parse #'endpoint-syntax + #:context 'endpoint-data + [(((~datum variables) variable ...) + ((~datum endpoints) (endpoint-id:id (endpoint-param:expr ...)) ...) + ((~datum render) render ...)) + #:with endpoint-scripts (for/list ([id (syntax-e #'(endpoint-id ...))] + [params (syntax-e #'((endpoint-param ...) ...))]) + (with-syntax ([id id] + [params (append (syntax-e params) + `(("callback" . ,(format "proxy.~a" (syntax-e id)))))]) + #'(script (@ (async) + (src ,(format "https://~a.fandom.com/api.php?~a" + wikiname + (params->query `params))) + (data-jsonp-var ,(format "~a" 'id)) + (onerror ,(format "proxy.~a({error: {code: 'disconnected', info: 'Fandom connection failed or was blocked by your browser. Check any browser extensions that may block third-party scripts, then reload the page.'}})" 'id)))))) + + (fix-scopes + #'here stx + #'(define name-args + (response-handler + (let/cc k + variable ... + fn-body ... + + (define body + (generate-wiki-page + `(div + (noscript "You have to enable JavaScript to load wiki pages. Sorry!") + (div (@ (id "loading"))) + (div (@ (id "progress-bar") (style "margin-bottom: 50vh")) + (progress)) + (script #< obj[prop] = value }}) +END + ) + endpoint-scripts + (script (@ (type "module") (src ,(get-static-url "jsonp.js"))))) + #:req req + #:source-url source-url + #:wikiname wikiname + #:title title + #:siteinfo siteinfo-default + #:path path + #:jsonp #t)) + (when (config-true? 'debug) + (xexp->html body)) + (response/output + #:code 200 + #:headers always-headers + (λ (out) + (write-html body out)))))))])])) diff --git a/src/page-category.rkt b/src/page-category.rkt index e1fe659..8913a81 100644 --- a/src/page-category.rkt +++ b/src/page-category.rkt @@ -1,8 +1,5 @@ #lang racket/base -(require racket/dict - racket/list - racket/match - racket/string +(require racket/string (prefix-in easy: net/http-easy) ; html libs html-parsing @@ -11,14 +8,9 @@ net/url web-server/http (only-in web-server/dispatchers/dispatch next-dispatcher) - #;(only-in web-server/http/redirect redirect-to) "application-globals.rkt" - "config.rkt" - "data.rkt" - "fandom-request.rkt" - "page-wiki.rkt" - "../lib/syntax.rkt" - "../lib/thread-utils.rkt" + "endpoints.rkt" + "../lib/tree-updater.rkt" "../lib/url-utils.rkt" "../lib/xexpr-utils.rkt") @@ -63,47 +55,47 @@ ,title))) members)))))) -(define (page-category req) - (response-handler - (define wikiname (path/param-path (first (url-path (request-uri req))))) - (define prefixed-category (string-join (map path/param-path (cddr (url-path (request-uri req)))) "/")) - (define origin (format "https://~a.fandom.com" wikiname)) - (define source-url (format "~a/wiki/~a" origin prefixed-category)) - (define-values (members-data page-data siteinfo) - (thread-values - (λ () - (easy:response-json - (fandom-get-api - wikiname - `(("action" . "query") - ("list" . "categorymembers") - ("cmtitle" . ,prefixed-category) - ("cmlimit" . "max") - ("formatversion" . "2") - ("format" . "json"))))) - (λ () - (easy:response-json - (fandom-get-api - wikiname - `(("action" . "parse") - ("page" . ,prefixed-category) - ("prop" . "text|headhtml|langlinks") - ("formatversion" . "2") - ("format" . "json"))))) - (λ () - (siteinfo-fetch wikiname)))) - - (define title (preprocess-html-wiki (jp "/parse/title" page-data prefixed-category))) - (define page-html (preprocess-html-wiki (jp "/parse/text" page-data ""))) +(define-endpoint + category-endpoint + [variables + (define wikiname (path/param-path (car (url-path (request-uri req))))) + (define prefixed-category (string-join (map path/param-path (cddr (url-path (request-uri req)))) "/")) + (define segments (map path/param-path (cdr (url-path (request-uri req))))) + (define title (url-segments->guess-title segments)) + (define path (string-join (cdr segments) "/")) + (define origin (format "https://~a.fandom.com" wikiname)) + (define source-url (format "~a/wiki/~a" origin prefixed-category))] + [endpoints + (membersdata + (("action" . "query") + ("list" . "categorymembers") + ("cmtitle" . ,prefixed-category) + ("cmlimit" . "max") + ("formatversion" . "2") + ("format" . "json"))) + (pagedata + (("action" . "parse") + ("page" . ,prefixed-category) + ("prop" . "text|headhtml|langlinks") + ("formatversion" . "2") + ("format" . "json"))) + (siteinfo + (("action" . "query") + ("meta" . "siteinfo") + ("siprop" . "general|rightsinfo") + ("format" . "json") + ("formatversion" . "2")))] + [render + (define page-html (preprocess-html-wiki (jp "/parse/text" pagedata ""))) (define page (html->xexp page-html)) - (define head-data ((head-data-getter wikiname) page-data)) + (define head-data ((head-data-getter wikiname) pagedata)) (define body (generate-results-page #:req req #:source-url source-url #:wikiname wikiname - #:title title - #:members-data members-data + #:title (preprocess-html-wiki (jp "/parse/title" pagedata prefixed-category)) + #:members-data membersdata #:page page #:head-data head-data #:siteinfo siteinfo)) @@ -116,7 +108,22 @@ #:code 200 #:headers (build-headers always-headers) (λ (out) - (write-html body out))))) + (write-html body out)))]) + +(define-standard-handler (page-category-standard req) + #'category-endpoint) + +(define-jsonp-handler (page-category-jsonp req) + #'category-endpoint) + +(define-post-data-handler (page-category-with-data req) + #'category-endpoint + (define prefixed-category path)) + +(define page-category (make-switch-handler #:standard page-category-standard + #:jsonp page-category-jsonp + #:post page-category-with-data)) + (module+ test (check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Ankle_Monitor") (generate-results-page diff --git a/src/page-file.rkt b/src/page-file.rkt index 5151f1d..78dee37 100644 --- a/src/page-file.rkt +++ b/src/page-file.rkt @@ -18,6 +18,7 @@ "fandom-request.rkt" "page-wiki.rkt" "../lib/syntax.rkt" + "../lib/tree-updater.rkt" "../lib/thread-utils.rkt" "../lib/url-utils.rkt" "../lib/xexpr-utils.rkt") diff --git a/src/page-search.rkt b/src/page-search.rkt index 39f361a..9eec557 100644 --- a/src/page-search.rkt +++ b/src/page-search.rkt @@ -23,8 +23,7 @@ page-search) (define search-providers - (hash "fandom" search-fandom - "solr" search-solr)) + (hash "solr" search-solr)) ;; this takes the info we gathered from fandom and makes the big fat x-expression page (define (generate-results-page req source-url wikiname query results-content #:siteinfo [siteinfo #f]) @@ -40,7 +39,7 @@ results-content)) ;; will be called when the web browser asks to load the page -(define (page-search req) +(define (page-search-solr req) ;; this just means, catch any errors and display them in the browser. it's a function somewhere else (response-handler ;; the URL will look like "/minecraft/wiki/Special:Search?q=Spawner" @@ -84,3 +83,9 @@ (λ (out) (write-html body out))))) + + +(define (page-search req) + (if (equal? (config-get 'feature_offline::search) "fandom") + (page-search-fandom req) + (page-search-solr req))) \ No newline at end of file diff --git a/src/page-wiki-jsonp.rkt b/src/page-wiki-jsonp.rkt deleted file mode 100644 index 55c98f5..0000000 --- a/src/page-wiki-jsonp.rkt +++ /dev/null @@ -1,72 +0,0 @@ -#lang racket/base -(require racket/list - racket/string - web-server/http - net/url-structs - html-writing - "application-globals.rkt" - "data.rkt" - "config.rkt" - "../lib/url-utils.rkt" - "../lib/xexpr-utils.rkt" - "../lib/archive-file-mappings.rkt" - "static-data.rkt") - -(provide - page-wiki-jsonp) - -(define (page-wiki-jsonp req) - (response-handler - (define wikiname (path/param-path (first (url-path (request-uri req))))) - (define segments (map path/param-path (cdr (url-path (request-uri req))))) - (define path (string-join (cdr segments) "/")) - (define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path)) - - (define wiki-page-script-url - (format "https://~a.fandom.com/api.php?~a" - wikiname - (params->query `(("action" . "parse") - ("page" . ,path) - ("prop" . "text|headhtml|langlinks") - ("formatversion" . "2") - ("format" . "json") - ("callback" . "proxy.wikipage"))))) - (define siteinfo-script-url - (format "https://~a.fandom.com/api.php?~a" - wikiname - (params->query `(("action" . "query") - ("meta" . "siteinfo") - ("siprop" . "general|rightsinfo") - ("format" . "json") - ("formatversion" . "2") - ("callback" . "proxy.siteinfo"))))) - - (define body - (generate-wiki-page - `(div - (noscript "You have to enable JavaScript to load wiki pages. Sorry!") - (div (@ (id "loading"))) - (div (@ (id "progress-bar") (style "margin-bottom: 50vh")) - (progress)) - (script #< obj[prop] = value }}) -END - ) - (script (@ (async) (src ,wiki-page-script-url) (onerror "proxy.wikipage({error: {code: 'script blocked', info: 'Fandom connection failed or was blocked by your browser. Check any browser extensions that may block third-party scripts, then reload the page.'}})"))) - (script (@ (async) (src ,siteinfo-script-url) (onerror "proxy.siteinfo({error: {code: 'script blocked', info: 'Fandom connection failed or was blocked by your browser. Check any browser extensions that may block third-party scripts, then reload the page.'}})"))) - (script (@ (type "module") (src ,(get-static-url "jsonp.js"))))) - #:req req - #:source-url source-url - #:wikiname wikiname - #:title (url-segments->guess-title segments) - #:siteinfo siteinfo-default - #:path path - #:jsonp #t)) - (when (config-true? 'debug) - (xexp->html body)) - (response/output - #:code 200 - #:headers always-headers - (λ (out) - (write-html body out))))) diff --git a/src/page-wiki.rkt b/src/page-wiki.rkt index 1caf41d..d87cb96 100644 --- a/src/page-wiki.rkt +++ b/src/page-wiki.rkt @@ -1,12 +1,6 @@ #lang racket/base (require racket/dict - racket/function - racket/list - racket/match racket/string - ; libs - (prefix-in easy: net/http-easy) - json ; html libs "../lib/html-parsing/main.rkt" html-writing @@ -16,27 +10,121 @@ web-server/dispatchers/dispatch ; my libs "application-globals.rkt" - "config.rkt" - "data.rkt" - "fandom-request.rkt" - "../lib/archive-file-mappings.rkt" - "../lib/thread-utils.rkt" + "endpoints.rkt" "../lib/tree-updater.rkt" "../lib/url-utils.rkt" "../lib/xexpr-utils.rkt") +(require (for-syntax racket/base syntax/parse)) (provide - ; used by the web server - page-wiki - page-wiki-with-data - ; used by page-category, and similar pages that are partially wiki pages - update-tree-wiki - preprocess-html-wiki) + page-wiki) -(module+ test - (require rackunit)) +(define-endpoint + wiki-endpoint + [variables + (define wikiname (path/param-path (car (url-path (request-uri req))))) + (define segments (map path/param-path (cdr (url-path (request-uri req))))) + (define title (url-segments->guess-title segments)) + (define path (string-join (cdr segments) "/")) + (define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path))] + [endpoints + (wikipage (("action" . "parse") + ("page" . ,path) + ("prop" . "text|headhtml|langlinks") + ("formatversion" . "2") + ("format" . "json"))) + (siteinfo (("action" . "query") + ("meta" . "siteinfo") + ("siprop" . "general|rightsinfo") + ("format" . "json") + ("formatversion" . "2")))] + [render + (define page-html (preprocess-html-wiki (jp "/parse/text" wikipage ""))) + (define page (html->xexp page-html)) + (define head-data ((head-data-getter wikiname) wikipage)) + (define body + (generate-wiki-page + (update-tree-wiki page wikiname) + #:req req + #:source-url source-url + #:wikiname wikiname + #:title (jp "/parse/title" wikipage "") + #:head-data head-data + #:siteinfo siteinfo)) + (define redirect-query-parameter (dict-ref (url-query (request-uri req)) 'redirect "yes")) + (define redirect-msg ((query-selector (attribute-selector 'class "redirectMsg") body))) + (define redirect-msg-a (if redirect-msg + ((query-selector (λ (t a c) (eq? t 'a)) redirect-msg)) + #f)) + (define html (xexp->html-bytes body)) + (define headers + (build-headers + always-headers + ; redirect-query-parameter: only the string "no" is significant: + ; https://github.com/Wikia/app/blob/fe60579a53f16816d65dad1644363160a63206a6/includes/Wiki.php#L367 + (when (and redirect-msg-a + (not (equal? redirect-query-parameter "no"))) + (let* ([dest (get-attribute 'href (bits->attributes redirect-msg-a))] + [value (bytes-append #"0;url=" (string->bytes/utf-8 dest))]) + (header #"Refresh" value))))) + (response/full + 200 + #"OK" + (current-seconds) + #"text/html; charset=utf-8" + headers + (list html))]) -(define (page-wiki req) +(define-standard-handler (page-wiki-standard req) + #'wiki-endpoint + (when (equal? "missingtitle" (jp "/error/code" wikipage #f)) + (next-dispatcher))) + +(define-jsonp-handler (page-wiki-jsonp req) + #'wiki-endpoint) + +(define-post-data-handler (page-wiki-with-data req) + #'wiki-endpoint) + +(define page-wiki (make-switch-handler #:standard page-wiki-standard + #:jsonp page-wiki-jsonp + #:post page-wiki-with-data)) + +#;(define (page-wiki-with-data req) + (response-handler + (let/cc return + (define post-data/bytes (request-post-data/raw req)) + (when (not post-data/bytes) + (k (response/jsexpr + #:code 400 + #:headers always-headers + '#hasheq((error . + #hasheq((code . "breezewiki") + (info . "POST requests only, please."))))))) + + (define origin-header + (or (headers-assq* #"origin" (request-headers/raw req)) + (headers-assq* #"referer" (request-headers/raw req)))) + (when (or (not origin-header) (not (string-prefix? (bytes->string/latin-1 (header-value origin-header)) (config-get 'canonical_origin)))) + (k (response/jsexpr + #:code 400 + #:headers always-headers + '#hasheq((error . + #hasheq((code . "breezewiki") + (info . "Origin/Referer header failed validation - cross-origin requests are not allowed here"))))))) + + (define post-data/string (bytes->string/utf-8 post-data/bytes)) + (define post-data (string->jsexpr post-data/string)) + (define wikiname (jp "/wikiname" post-data)) + (define path (jp "/path" post-data)) + (take-json-rewrite-and-return-page + #:req req + #:wikiname wikiname + #:source-url (format "https://~a.fandom.com/wiki/~a" wikiname path) + #:data (jp "/data" post-data) + #:siteinfo (data->siteinfo (jp "/siteinfo" post-data)))))) + +#;(define (page-wiki req) (define wikiname (path/param-path (first (url-path (request-uri req))))) (define segments (map path/param-path (cdr (url-path (request-uri req))))) (define user-cookies (user-cookies-getter req)) @@ -95,75 +183,3 @@ (easy:response-status-code dest-res) (easy:response-body dest-res)))])) -(define (page-wiki-with-data req) - (response-handler - (let/cc return - (define post-data/bytes (request-post-data/raw req)) - (when (not post-data/bytes) - (return (response/jsexpr - #:code 400 - #:headers always-headers - '#hasheq((error . - #hasheq((code . "breezewiki") - (info . "POST requests only, please."))))))) - - (define origin-header - (or (headers-assq* #"origin" (request-headers/raw req)) - (headers-assq* #"referer" (request-headers/raw req)))) - (when (or (not origin-header) (not (string-prefix? (bytes->string/latin-1 (header-value origin-header)) (config-get 'canonical_origin)))) - (return (response/jsexpr - #:code 400 - #:headers always-headers - '#hasheq((error . - #hasheq((code . "breezewiki") - (info . "Origin/Referer header failed validation - cross-origin requests are not allowed here"))))))) - - (define post-data/string (bytes->string/utf-8 post-data/bytes)) - (define post-data (string->jsexpr post-data/string)) - (define wikiname (jp "/wikiname" post-data)) - (define path (jp "/path" post-data)) - (take-json-rewrite-and-return-page - #:req req - #:wikiname wikiname - #:source-url (format "https://~a.fandom.com/wiki/~a" wikiname path) - #:data (jp "/data" post-data) - #:siteinfo (data->siteinfo (jp "/siteinfo" post-data)))))) - -(define (take-json-rewrite-and-return-page #:req req #:wikiname wikiname #:source-url source-url #:data data #:siteinfo siteinfo) - (define title (jp "/parse/title" data "")) - (define page-html (preprocess-html-wiki (jp "/parse/text" data ""))) - (define page (html->xexp page-html)) - (define head-data ((head-data-getter wikiname) data)) - (response-handler - (define body - (generate-wiki-page - (update-tree-wiki page wikiname) - #:req req - #:source-url source-url - #:wikiname wikiname - #:title title - #:head-data head-data - #:siteinfo siteinfo)) - (define redirect-query-parameter (dict-ref (url-query (request-uri req)) 'redirect "yes")) - (define redirect-msg ((query-selector (attribute-selector 'class "redirectMsg") body))) - (define redirect-msg-a (if redirect-msg - ((query-selector (λ (t a c) (eq? t 'a)) redirect-msg)) - #f)) - (define html (xexp->html-bytes body)) - (define headers - (build-headers - always-headers - ; redirect-query-parameter: only the string "no" is significant: - ; https://github.com/Wikia/app/blob/fe60579a53f16816d65dad1644363160a63206a6/includes/Wiki.php#L367 - (when (and redirect-msg-a - (not (equal? redirect-query-parameter "no"))) - (let* ([dest (get-attribute 'href (bits->attributes redirect-msg-a))] - [value (bytes-append #"0;url=" (string->bytes/utf-8 dest))]) - (header #"Refresh" value))))) - (response/full - 200 - #"OK" - (current-seconds) - #"text/html; charset=utf-8" - headers - (list html)))) diff --git a/src/search-provider-fandom.rkt b/src/search-provider-fandom.rkt index b8dd48f..d87c868 100644 --- a/src/search-provider-fandom.rkt +++ b/src/search-provider-fandom.rkt @@ -1,59 +1,95 @@ #lang racket/base -(require racket/string +(require racket/dict + racket/string (prefix-in easy: net/http-easy) + net/url + web-server/http + html-writing "application-globals.rkt" "config.rkt" + "endpoints.rkt" "fandom-request.rkt" "../lib/url-utils.rkt" "../lib/xexpr-utils.rkt") (provide - search-fandom) + page-search-fandom) -(module+ test - (require rackunit - "test-utils.rkt") - (define search-results-data - '(#hasheq((ns . 0) (pageid . 219) (size . 1482) (snippet . "") (timestamp . "2022-08-21T08:54:23Z") (title . "Gacha Capsule") (wordcount . 214)) #hasheq((ns . 0) (pageid . 201) (size . 1198) (snippet . "") (timestamp . "2022-07-11T17:52:47Z") (title . "Badges") (wordcount . 181))))) +(define-endpoint + search-endpoint + [variables + (define wikiname (path/param-path (car (url-path (request-uri req))))) + (define params (url-query (request-uri req))) + (define query (dict-ref params 'q #f)) + (define title "Search") + (define path (format "Special:Search?~a" (params->query `(("query" . ,query) + ("search" . "internal"))))) + (define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path))] + [endpoints + (search + (("action" . "query") + ("list" . "search") + ("srsearch" . ,query) + ("formatversion" . "2") + ("format" . "json"))) + (siteinfo + (("action" . "query") + ("meta" . "siteinfo") + ("siprop" . "general|rightsinfo") + ("format" . "json") + ("formatversion" . "2")))] + [render + (define search-results (jp "/query/search" search)) + (define body + (generate-wiki-page + #:req req + #:source-url source-url + #:wikiname wikiname + #:title query + #:siteinfo siteinfo + `(div (@ (class "mw-parser-output")) + ;; header before the search results showing how many we found + (p ,(format "~a results found for " (length search-results)) + (strong ,query)) + ;; *u*nordered *l*ist of matching search results + (ul ,@(for/list ([result search-results]) + (let* ([title (jp "/title" result)] + [page-path (page-title->path title)] + [timestamp (jp "/timestamp" result)] + [wordcount (jp "/wordcount" result)] + [size (jp "/size" result)]) + ;; and make this x-expression... + `(li (@ (class "my-result")) + (a (@ (class "my-result__link") (href ,(format "/~a/wiki/~a" wikiname page-path))) ; using unquote to insert the result page URL + ,title) ; using unquote to insert the result page title + (div (@ (class "my-result__info")) ; constructing the line under the search result + "last edited " + (time (@ (datetime ,timestamp)) ,(list-ref (string-split timestamp "T") 0)) + ,(format ", ~a words, ~a kb" + wordcount + (exact->inexact (/ (round (/ size 100)) 10))))))))))) + (when (config-true? 'debug) + (xexp->html body)) + (response/output + #:code 200 + #:headers (build-headers always-headers) + (λ (out) + (write-html body out)))]) -(define (search-fandom wikiname query params) - (define res - (fandom-get-api - wikiname - `(("action" . "query") - ("list" . "search") - ("srsearch" . ,query) - ("formatversion" . "2") - ("format" . "json")))) - (define json (easy:response-json res)) - (define search-results (jp "/query/search" json)) - (generate-results-content-fandom wikiname query search-results)) -;;; generate content for display in the wiki page layout -(define (generate-results-content-fandom wikiname query search-results) - `(div (@ (class "mw-parser-output")) - ;; header before the search results showing how many we found - (p ,(format "~a results found for " (length search-results)) - (strong ,query)) - ;; *u*nordered *l*ist of matching search results - (ul ,@(for/list ([result search-results]) - (let* ([title (jp "/title" result)] - [page-path (page-title->path title)] - [timestamp (jp "/timestamp" result)] - [wordcount (jp "/wordcount" result)] - [size (jp "/size" result)]) - ;; and make this x-expression... - `(li (@ (class "my-result")) - (a (@ (class "my-result__link") (href ,(format "/~a/wiki/~a" wikiname page-path))) ; using unquote to insert the result page URL - ,title) ; using unquote to insert the result page title - (div (@ (class "my-result__info")) ; constructing the line under the search result - "last edited " - (time (@ (datetime ,timestamp)) ,(list-ref (string-split timestamp "T") 0)) - ,(format ", ~a words, ~a kb" - wordcount - (exact->inexact (/ (round (/ size 100)) 10)))))))))) +(define-standard-handler (page-search-standard req) + #'search-endpoint) -(module+ test - (parameterize ([(config-parameter 'feature_offline::only) "false"]) - (check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Gacha_Capsule") - (generate-results-content-fandom "test" "Gacha" search-results-data)))))) +(define-jsonp-handler (page-search-jsonp req) + #'search-endpoint) + +(define-post-data-handler (page-search-with-data req) + #'search-endpoint + (define params (url-query (request-uri req))) + (define query (dict-ref params 'q #f)) + (define title "Search")) + +(define page-search-fandom + (make-switch-handler #:standard page-search-standard + #:jsonp page-search-jsonp + #:post page-search-with-data)) diff --git a/static/jsonp.js b/static/jsonp.js index 7997045..efd96a1 100644 --- a/static/jsonp.js +++ b/static/jsonp.js @@ -37,23 +37,29 @@ window.proxy = new Proxy(jsonpData, { // *** Data upload and download async function cont() { - if (jsonpData.wikipage?.error) return error(jsonpData.wikipage) - if (jsonpData.siteinfo?.error) return error(jsonpData.siteinfo) - if (!(jsonpData.wikipage && jsonpData.siteinfo)) return + // Check for errors + for (const v of Object.values(jsonpData)) { + if (v.error) return error(v) + } + + // Check for completion + const dependencies = [...document.querySelectorAll("[data-jsonp-var]")].map(e => e.getAttribute("data-jsonp-var")) + for (const d of dependencies) { + if (!(d in jsonpData)) return + } const xhr = new XMLHttpRequest(); const uploadFraction = 0.7 const pkg = { - url: "/api/render/wiki", + url: location.href, init: { method: "POST", body: JSON.stringify({ - data: jsonpData.wikipage, - siteinfo: jsonpData.siteinfo, wikiname: BWData.wikiname, - path: BWData.path + path: BWData.path, + ...jsonpData }) } } @@ -127,7 +133,7 @@ function error(data) { if (typeof data === "string") { render(html`

    BreezeWiki ran into an error on this page.

    Try reloading the page.

    If this keeps happening, you could send a public bug report. Please include the following information:

    URL: ${window.location.href}${"\n"}${data}
    `, eContent) } else if (data.error.code === "missingtitle") { - render(html`

    This page doesn't exist on Fandom.

    Return to the homepage?

    `, eContent) + render(html`

    This page doesn't exist on Fandom.

    Return to the wiki's main page

    `, eContent) } else { render(html`

    BreezeWiki wasn't able to load this page.

    ${data.error.code}: ${data.error.info}

    `, eContent) }