diff --git a/nyxt.asd b/nyxt.asd index b8e59bf6919..d3d129525cf 100644 --- a/nyxt.asd +++ b/nyxt.asd @@ -363,7 +363,7 @@ :components ((:file "renderer-package") (:file "renderer-offline/set-url") (:file "renderer-offline/execute-command-eval") - (:file "renderer-offline/nyxt-url-security") + (:file "renderer-offline/custom-schemes") (:file "renderer-offline/search-buffer") ;; See https://github.com/atlas-engineer/nyxt/issues/3172 ;; (:file "renderer-online/set-url") diff --git a/source/changelog.lisp b/source/changelog.lisp index 80de7efcfba..80686f70962 100644 --- a/source/changelog.lisp +++ b/source/changelog.lisp @@ -27,7 +27,7 @@ (define-version "4.0.0" (:ul - (:li "Refactor lisp schemes URLs API.") + (:li "Refactor custom schemes URLs API.") (:li "Deprecate slot " (:code "status-buffer-position") "in favour of" (:nxref :slot 'placement :class-name 'status-buffer) ".") (:li "Deprecate slot " (:code "prompt-buffer-open-height") " since " diff --git a/source/foreign-interface.lisp b/source/foreign-interface.lisp index cdb2b1c6767..8897503e6cc 100644 --- a/source/foreign-interface.lisp +++ b/source/foreign-interface.lisp @@ -123,6 +123,10 @@ When URL is non-nil, relative URLs are resolved against it.") Like `ffi-buffer-load-html', except that it doesn't influence the BUFFER history or CSS/HTML cache.") +(define-ffi-generic ffi-register-custom-scheme (scheme) + "Register internal custom SCHEME. +See `scheme'.") + (define-ffi-generic ffi-buffer-evaluate-javascript (buffer javascript &optional world-name) "Evaluate JAVASCRIPT, encoded as a string, in BUFFER.") (define-ffi-generic ffi-buffer-evaluate-javascript-async (buffer javascript &optional world-name) diff --git a/source/manual.lisp b/source/manual.lisp index 0da5c7fd9a3..5ce3978170d 100644 --- a/source/manual.lisp +++ b/source/manual.lisp @@ -382,39 +382,18 @@ Nyxt provides. For example, one can use the " "Bookmark Link"))) (:nsection :title "Custom URL schemes" - (:p "If there's a scheme that Nyxt doesn't support, but you want it to, you can -always define the handler for this scheme so that it's Nyxt-openable.") - (:p "As a totally hypothetical example, you can define a nonsense scheme " - (:code "bleep") " to generate a page with random text:") + (:p "Nyxt can register custom schemes that run a handler on URL load.") + (:p "The example below defines a scheme " (:code "hello") " that replies + accordingly when loading URLs " (:code "hello:world") " and " + (:code "hello:mars") ".") (:ncode - '(define-internal-scheme "bleep" - (lambda (url buffer) - (values - (spinneret:with-html-string - (:h1 "Bleep bloop?") - (:p (loop repeat (parse-integer (quri:uri-path (url url)) :junk-allowed t) - collect (:li (elt '("bleep" "bloop") (random 2)))))) - "text/html;charset=utf8")) - :local-p t)) - (:p "What this piece of code does is") - (:ul - (:li "Define a new scheme.") - (:li "Make a handler for it that takes the URL (as a string) and a buffer it's being -opened in.") - (:li "Read the path (the part after the bleep:) of the URL and interpret it as a number.") - (:ul - (:li "(Note that you need to wrap the URL into a " (:nxref :function 'url) - " call so that it turns into a " (:nxref :class-name 'quri:uri) - " for the convenience of path (and other elements) fetching.)")) - (:li "Generate a random list of \"bleep\" and \"bloop\".") - (:li "Return it as a " (:code "text/html") " content.")) - (:p "The next time you run Nyxt and open " (:code "bleep:20") - ", you'll see a list of twenty bleeps and bloops.") - (:p "Internal schemes can return any type of content (both strings and arrays of -bytes are recognized), and they are capable of being " - (:nxref :class-name 'scheme :slot 'cors-enabled-p "CORS-enabled") - ", " (:nxref :class-name 'scheme :slot 'local-p "protected") - ", and are in general capable of whatever the renderer-provided schemes do.") + '(define-internal-scheme "hello" + (lambda (url) + (if (string= (quri:uri-path (url url)) "world") + (spinneret:with-html-string (:p "Hello, World!")) + (spinneret:with-html-string (:p "Please instruct me on how to greet you!")))))) + (:p "Note that scheme privileges, such as enabling the Fetch API or +enabling CORS requests are renderer-specific.") (:nsection :title "nyxt: URLs and internal pages" (:p "You can create pages out of Lisp commands, and make arbitrary computations for diff --git a/source/mode/document.lisp b/source/mode/document.lisp index b7a04171aca..599c65345a5 100644 --- a/source/mode/document.lisp +++ b/source/mode/document.lisp @@ -267,12 +267,9 @@ Otherwise, create a dummy buffer with URL to get its source." (ffi-buffer-delete buffer))))) (define-internal-scheme "view-source" - (lambda (url buffer) - (declare (ignore buffer)) - (values - (get-url-source (quri:url-decode (quri:uri-path (quri:uri url)))) - "text/plain")) - :no-access-p t) + (lambda (url) + (values (get-url-source (quri:url-decode (quri:uri-path (quri:uri url)))) + "text/plain"))) (define-command-global view-source (&key (url (url (current-buffer)))) "View source of the URL (by default current page) in a separate buffer." diff --git a/source/mode/editor.lisp b/source/mode/editor.lisp index 783f8182a8a..7214d1c39c3 100644 --- a/source/mode/editor.lisp +++ b/source/mode/editor.lisp @@ -92,8 +92,8 @@ See `describe-class editor-mode' for details.")) (uiop:parse-native-namestring (quri:uri-path (url buffer)))) (define-internal-scheme "editor" - (lambda (url buffer) - (markup (find-submode 'editor-mode buffer) + (lambda (url) + (markup (find-submode 'editor-mode) (uiop:read-file-string (quri:uri-path (quri:uri url)))))) (defmethod editor ((editor-buffer editor-buffer)) @@ -157,3 +157,6 @@ BUFFER is of type `editor-buffer'." (prompt1 :prompt "Edit user file" :sources 'nyxt::user-file-source))))) (edit-file file-path)) + +(define-auto-rule '(match-scheme "editor") + :included '(nyxt/mode/editor:editor-mode)) diff --git a/source/mode/plaintext-editor.lisp b/source/mode/plaintext-editor.lisp index 5a276e334fe..6db65197c29 100644 --- a/source/mode/plaintext-editor.lisp +++ b/source/mode/plaintext-editor.lisp @@ -9,7 +9,6 @@ It renders the file in a single textarea HTML element. Enabled by default for `editor-buffer's." ((visible-in-status-p nil) - (rememberable-p nil) (style (theme:themed-css (theme *browser*) `("body" :margin 0) @@ -44,3 +43,6 @@ It renders the file in a single textarea HTML element. Enabled by default for (defmethod get-content ((editor plaintext-editor-mode)) (ps-eval :buffer (buffer editor) (ps:chain (nyxt/ps:qs document "#editor") value))) + +(define-auto-rule '(match-scheme "editor") + :included '(nyxt/mode/editor:plaintext-editor-mode)) diff --git a/source/mode/small-web.lisp b/source/mode/small-web.lisp index c99f8d0febc..a7e573a9289 100644 --- a/source/mode/small-web.lisp +++ b/source/mode/small-web.lisp @@ -35,9 +35,9 @@ Gemini support is a bit more brittle, but you can override `line->html' for (url :documentation "The URL being opened.") (model :documentation "The contents of the current page.") (redirections nil :documentation "The list of redirection Gemini URLs.") - (allowed-redirections-count + (max-redirections 5 - :documentation "The number of redirections that Gemini resources are allowed to make.") + :documentation "The maximum number of times a redirection is attempted.") (style (theme:themed-css (nyxt::theme *browser*) `(body :background-color ,theme:background) @@ -187,22 +187,19 @@ Implies that `small-web-mode' is enabled.")) (defmethod gopher-render ((line cl-gopher:gif)) (render-binary-content line "image/gif")) (defmethod gopher-render ((line cl-gopher:png)) (render-binary-content line "image/png")) -;; TODO: :display-isolated-p? Gopher's behavior implies inability to embed it -;; into pages of the bigger Web, which is exactly what display-isolated means. (define-internal-scheme "gopher" - (lambda (url buffer) + (lambda (url) (handler-case - (let* ((line (if (uiop:emptyp (quri:uri-path (quri:uri url))) - (buffer-load (str:concat url "/") :buffer buffer) - (cl-gopher:parse-gopher-uri url)))) + (let ((line (if (uiop:emptyp (quri:uri-path (quri:uri url))) + (buffer-load (str:concat url "/")) + (cl-gopher:parse-gopher-uri url)))) (if (and (typep line 'cl-gopher:search-line) (uiop:emptyp (cl-gopher:terms line))) (progn (setf (cl-gopher:terms line) (prompt1 :prompt (format nil "Search query for ~a" url) :sources 'prompter:raw-source)) - (buffer-load (cl-gopher:uri-for-gopher-line line) :buffer buffer)) - (with-current-buffer buffer - (gopher-render line)))) + (buffer-load (cl-gopher:uri-for-gopher-line line))) + (with-current-buffer (current-buffer) (gopher-render line)))) (cl-gopher:bad-submenu-error () (error-help (format nil "Malformed line at ~s" url) (format nil "One of the lines on this page has an improper format. @@ -266,7 +263,7 @@ Please, check URL correctness and try again."))) (:br))) (export-always 'gemtext-render) -(defun gemtext-render (gemtext buffer) +(defun gemtext-render (gemtext &optional (buffer (current-buffer))) "Renders the Gemtext (Gemini markup format) to HTML. Implies that `small-web-mode' is enabled." @@ -282,46 +279,46 @@ Implies that `small-web-mode' is enabled." collect (:raw (nyxt/mode/small-web:line->html element)))) "text/html;charset=utf8"))) -;; TODO: :secure-p t? Gemini is encrypted, so it can be considered secure. (define-internal-scheme "gemini" - (lambda (url buffer) + (lambda (url) (handler-case (sera:mvlet* ((status meta body (gemini:request url))) (unless (member status '(:redirect :permanent-redirect)) - (setf (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)) nil)) - (case status - ((:input :sensitive-input) - (let ((text (quri:url-encode - (handler-case - (prompt1 :prompt meta - :sources 'prompter:raw-source - :invisible-input-p (eq status :sensitive-input)) - (nyxt::prompt-buffer-canceled () ""))))) - (buffer-load (str:concat url "?" text) :buffer buffer))) - (:success - (if (str:starts-with-p "text/gemini" meta) - (gemtext-render body buffer) - (values body meta))) - ((:redirect :permanent-redirect) - (push url (nyxt/mode/small-web:redirections (find-submode 'small-web-mode))) - (if (< (length (nyxt/mode/small-web:redirections (find-submode 'small-web-mode))) - (nyxt/mode/small-web:allowed-redirections-count (find-submode 'small-web-mode))) - (buffer-load (quri:merge-uris (quri:uri meta) (quri:uri url)) :buffer buffer) - (error-help - "Error" - (format nil "The server has caused too many (~a+) redirections.~& ~a~{ -> ~a~}" - (nyxt/mode/small-web:allowed-redirections-count (find-submode 'small-web-mode)) - (alex:lastcar (nyxt/mode/small-web:redirections (find-submode 'small-web-mode))) - (butlast (nyxt/mode/small-web:redirections (find-submode 'small-web-mode))))))) - ((:temporary-failure :server-unavailable :cgi-error :proxy-error - :permanent-failure :not-found :gone :proxy-request-refused :bad-request) - (error-help "Error" meta)) - (:slow-down - (error-help - "Slow down error" - (format nil "Try reloading the page in ~a seconds." meta))) - ((:client-certificate-required :certificate-not-authorised :certificate-not-valid) - (error-help "Certificate error" meta)))) + (setf (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)) nil)) + (case status + ((:input :sensitive-input) + (let ((text (quri:url-encode + (handler-case + (prompt1 :prompt meta + :sources 'prompter:raw-source + :invisible-input-p (eq status :sensitive-input)) + (nyxt::prompt-buffer-canceled () ""))))) + (buffer-load (str:concat url "?" text)) + nil)) + (:success + (if (str:starts-with-p "text/gemini" meta) + (gemtext-render body) + (values body meta))) + ((:redirect :permanent-redirect) + (push url (nyxt/mode/small-web:redirections (find-submode 'small-web-mode))) + (if (< (length (nyxt/mode/small-web:redirections (find-submode 'small-web-mode))) + (nyxt/mode/small-web:max-redirections (find-submode 'small-web-mode))) + (progn (buffer-load (quri:merge-uris (quri:uri meta) (quri:uri url))) nil) + (error-help + "Error" + (format nil "The server has caused too many (~a+) redirections.~& ~a~{ -> ~a~}" + (nyxt/mode/small-web:max-redirections (find-submode 'small-web-mode)) + (alex:lastcar (nyxt/mode/small-web:redirections (find-submode 'small-web-mode))) + (butlast (nyxt/mode/small-web:redirections (find-submode 'small-web-mode))))))) + ((:temporary-failure :server-unavailable :cgi-error :proxy-error + :permanent-failure :not-found :gone :proxy-request-refused :bad-request) + (error-help "Error" meta)) + (:slow-down + (error-help + "Slow down error" + (format nil "Try reloading the page in ~a seconds." meta))) + ((:client-certificate-required :certificate-not-authorised :certificate-not-valid) + (error-help "Certificate error" meta)))) (gemini::malformed-response (e) (error-help "Malformed response" diff --git a/source/renderer-script.lisp b/source/renderer-script.lisp index b457be9c272..09c076c3283 100644 --- a/source/renderer-script.lisp +++ b/source/renderer-script.lisp @@ -203,10 +203,7 @@ Overwrites the whole HTML document (head and body elements included)." (export-always 'match-internal-page) (defun match-internal-page (symbol) "Return a predicate for URL designators matching the page of SYMBOL name." - #'(lambda (url) - (and (str:starts-with-p "nyxt:" (render-url url)) - (eq (parse-nyxt-url url) - symbol)))) + #'(lambda (url) (eq (internal-page-name url) symbol))) (define-class internal-page (command) ((dynamic-title ; Not `title' so that it does not clash with other `title' methods. @@ -333,11 +330,6 @@ See `find-internal-page-buffer'.")) (t (format nil "*~a*" (string-downcase (name page))))))) -(defun internal-page-name (url) - (when (string= "nyxt" (quri:uri-scheme url)) - (uiop:safe-read-from-string - (str:upcase (quri:uri-path url)) :package :nyxt))) - ;; (-> find-internal-page-buffer (internal-page-symbol) (maybe buffer)) (defun find-internal-page-buffer (name) ; TODO: Test if CCL can catch bad calls at compile-time. "Return first buffer which URL is a NAME `internal-page'." @@ -345,10 +337,7 @@ See `find-internal-page-buffer'.")) (defun find-url-internal-page (url) "Return the `internal-page' to which URL corresponds." - (and (equal "nyxt" (quri:uri-scheme url)) - (gethash - (internal-page-name url) - *nyxt-url-commands*))) + (gethash (internal-page-name url) *nyxt-url-commands*)) (export-always 'buffer-load-internal-page-focus) (defun buffer-load-internal-page-focus (name &rest args) diff --git a/source/renderer/gtk.lisp b/source/renderer/gtk.lisp index 6d17812ef94..dfe431cb9b7 100644 --- a/source/renderer/gtk.lisp +++ b/source/renderer/gtk.lisp @@ -810,11 +810,109 @@ See `gtk-browser's `modifier-translator' slot." (funcall (input-dispatcher window) event sender window)))) (define-class gtk-scheme () - () + ((context + nil + :writer nil + :reader t + :documentation "See `webkit-web-context'.") + (local-p + nil + :writer nil + :reader t + :documentation "Whether pages of other URI schemes cannot access URIs of +this scheme.") + (no-access-p + nil + :writer nil + :reader t + :documentation "Whether pages of this URI scheme cannot access other URI schemes.") + (secure-p + nil + :writer nil + :reader t + :documentation "Whether mixed content warnings aren't generated for this +scheme when included by an HTTPS page. + +See https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content.") + (cors-enabled-p + nil + :writer nil + :reader t + :documentation "Whether CORS requests are allowed.") + (display-isolated-p + nil + :writer nil + :reader t + :documentation "Whether pages cannot display URIs unless they are from the +same scheme. +For example, pages in another origin cannot create iframes or hyperlinks to URIs +with this scheme.") + (empty-document-p + nil + :writer nil + :reader t + :documentation "Whether pages are allowed to be loaded synchronously.")) (:export-class-name-p t) (:export-accessor-names-p t) (:documentation "Related to WebKit's custom schemes.")) +(defmethod manager ((scheme gtk-scheme)) + (webkit:webkit-web-context-get-security-manager (context scheme))) + +(defmethod (setf local-p) (value (scheme gtk-scheme)) + (when value + (webkit:webkit-security-manager-register-uri-scheme-as-local (manager scheme) + (name scheme))) + (setf (slot-value scheme 'local-p) value)) + +(defmethod (setf no-access-p) (value (scheme gtk-scheme)) + (when value + (webkit:webkit-security-manager-register-uri-scheme-as-no-access (manager scheme) + (name scheme))) + (setf (slot-value scheme 'no-access-p) value)) + +(defmethod (setf secure-p) (value (scheme gtk-scheme)) + (when value + (webkit:webkit-security-manager-register-uri-scheme-as-secure (manager scheme) + (name scheme))) + (setf (slot-value scheme 'secure-p) value)) + +(defmethod (setf cors-enabled-p) (value (scheme gtk-scheme)) + (when value + (webkit:webkit-security-manager-register-uri-scheme-as-cors-enabled (manager scheme) + (name scheme))) + (setf (slot-value scheme 'cors-enabled-p) value)) + +(defmethod (setf display-isolated-p) (value (scheme gtk-scheme)) + (when value + (webkit:webkit-security-manager-register-uri-scheme-as-display-isolated (manager scheme) + (name scheme))) + (setf (slot-value scheme 'display-isolated-p) value)) + +(defmethod (setf empty-document-p) (value (scheme gtk-scheme)) + (when value + (webkit:webkit-security-manager-register-uri-scheme-as-empty-document (manager scheme) + (name scheme))) + (setf (slot-value scheme 'empty-document-p) value)) + +(defmethod initialize-instance :after ((scheme gtk-scheme) &key) + ;; NOTE: No security settings for the nyxt scheme since: + ;; - :local-p makes it inaccessible from other schemes. + ;; - :display-isolated-p does not allow embedding a nyxt scheme page inside a + ;; page of the same scheme. + ;; - :secure-p and :cors-enabled-p are too permissive for a scheme that allows + ;; evaluating Lisp code. + ;; Therefore, no settings provide the best configuration so that: + ;; -