about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-06-08 21:30:32 +0200
committerbptato <nincsnevem662@gmail.com>2024-06-08 21:47:19 +0200
commitdee468d502a2d2e0aa6ae2f8365c098f72acc0c9 (patch)
tree8700f96705e1e9f12122539c81047b43d6992a8b
parentbb0605182a3944418f74e1f5269916d4d0298190 (diff)
downloadchawan-dee468d502a2d2e0aa6ae2f8365c098f72acc0c9.tar.gz
pager, buffer: improve forms, protocol config
* refactor form submission
* add options to specify form handling per protocol
* block cross-protocol POST requests
-rw-r--r--doc/config.md25
-rw-r--r--res/config.toml9
-rw-r--r--src/config/config.nim21
-rw-r--r--src/loader/request.nim16
-rw-r--r--src/local/container.nim8
-rw-r--r--src/local/pager.nim32
-rw-r--r--src/server/buffer.nim254
7 files changed, 192 insertions, 173 deletions
diff --git a/doc/config.md b/doc/config.md
index d7b92723..d3abdb22 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -34,6 +34,7 @@ examples.
 * [External](#external)
 * [Network](#network)
 * [Display](#display)
+* [Protocol](#protocol)
 * [Omnirule](#omnirule)
 * [Siteconf](#siteconf)
 * [Stylesheets](#stylesheets)
@@ -476,6 +477,30 @@ fallback values provided above.</td>
 
 </table>
 
+## Protocol
+
+Protocol-related rules are to be placed in objects keyed as
+`[protocol.{protocol-name}]`. e.g. FTP related rules are placed in in
+`[protocol.ftp]`.
+
+<table>
+
+<tr>
+<th>Name</th>
+<th>Value</th>
+<th>Function</th>
+</tr>
+
+<tr>
+<td>form-request</td>
+<td>http, ftp, data, mailto</td>
+<td>Specify which protocol to imitate when submitting forms to this
+protocol.<br>
+Defaults to HTTP.</td>
+</tr>
+
+</table>
+
 ## Omnirule
 
 The omni-bar (by default opened with C-l) can be used to perform searches using
diff --git a/res/config.toml b/res/config.toml
index 4a295099..48eb0e64 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -215,6 +215,15 @@ escape = '() => line.escape()'
 prevHist = '() => line.prevHist()'
 nextHist = '() => line.nextHist()'
 
+[protocol.ftp]
+form-request = "ftp"
+
+[protocol.mailto]
+form-request = "mailto"
+
+[protocol.data]
+form-request = "data"
+
 [search]
 wrap = true
 ignore-case = true
diff --git a/src/config/config.nim b/src/config/config.nim
index 35ca1a17..50173057 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -43,7 +43,13 @@ type
   ActionMap = object
     t: Table[string, string]
 
-  SiteConfig* = object
+  FormRequestType* = enum
+    frtHttp = "http"
+    frtFtp = "ftp"
+    frtData = "data"
+    frtMailto = "mailto"
+
+  SiteConfig* = ref object
     url*: Option[Regex]
     host*: Option[Regex]
     rewrite_url*: Option[JSValueFunction]
@@ -59,7 +65,7 @@ type
     default_headers*: TableRef[string, string]
     insecure_ssl_no_verify*: Option[bool]
 
-  OmniRule* = object
+  OmniRule* = ref object
     match*: Regex
     substitute_url*: Option[JSValueFunction]
 
@@ -131,6 +137,9 @@ type
     force_pixels_per_column* {.jsgetset.}: bool
     force_pixels_per_line* {.jsgetset.}: bool
 
+  ProtocolConfig* = ref object
+    form_request*: FormRequestType
+
   Config* = ref object
     jsctx: JSContext
     jsvfns*: seq[JSValueFunction]
@@ -145,6 +154,7 @@ type
     input* {.jsget.}: InputConfig
     display* {.jsget.}: DisplayConfig
     #TODO getset
+    protocol*: Table[string, ProtocolConfig]
     siteconf*: seq[SiteConfig]
     omnirule*: seq[OmniRule]
     cmd*: CommandConfig
@@ -289,6 +299,8 @@ type ConfigParser = object
 
 proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue;
   k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var ref object; v: TomlValue;
+  k: string)
 proc parseConfigValue(ctx: var ConfigParser; x: var bool; v: TomlValue;
   k: string)
 proc parseConfigValue(ctx: var ConfigParser; x: var string; v: TomlValue;
@@ -365,6 +377,11 @@ proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue;
           fk
         ctx.parseConfigValue(fv, v[kebabk], kkk)
 
+proc parseConfigValue(ctx: var ConfigParser; x: var ref object; v: TomlValue;
+    k: string) =
+  new(x)
+  ctx.parseConfigValue(x[], v, k)
+
 proc parseConfigValue[U, V](ctx: var ConfigParser; x: var Table[U, V];
     v: TomlValue; k: string) =
   typeCheck(v, tvtTable, k)
diff --git a/src/loader/request.nim b/src/loader/request.nim
index fc6c43f5..277481f1 100644
--- a/src/loader/request.nim
+++ b/src/loader/request.nim
@@ -115,22 +115,6 @@ func newRequest*(url: URL; httpMethod = hmGet; headers = newHeaders();
     suspended: suspended
   )
 
-func newRequest*(url: URL; httpMethod = hmGet;
-    headers: seq[(string, string)] = @[]; body = none(string);
-    multipart = none(FormData); proxy: URL = nil): Request =
-  let hl = newHeaders()
-  for pair in headers:
-    let (k, v) = pair
-    hl.table[k] = @[v]
-  return newRequest(
-    url,
-    httpMethod,
-    hl,
-    body,
-    multipart,
-    proxy = proxy
-  )
-
 func createPotentialCORSRequest*(url: URL; destination: RequestDestination;
     cors: CORSAttribute; fallbackFlag = false): JSRequest =
   var mode = if cors == caNoCors:
diff --git a/src/local/container.nim b/src/local/container.nim
index 8439b32a..90e00ebb 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -1489,8 +1489,8 @@ proc readSuccess*(container: Container; s: string; fd = -1) =
   p.then(proc(res: ReadSuccessResult) =
     if res.repaint:
       container.needslines = true
-    if res.open.isSome:
-      container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open.get))
+    if res.open != nil:
+      container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open))
   )
 
 proc reshape(container: Container): EmptyPromise {.jsfunc.} =
@@ -1513,10 +1513,10 @@ proc displaySelect(container: Container; selectResult: SelectResult) =
 proc onclick(container: Container; res: ClickResult; save: bool) =
   if res.repaint:
     container.needslines = true
-  if res.open.isSome:
+  if res.open != nil:
     container.triggerEvent(ContainerEvent(
       t: cetOpen,
-      request: res.open.get,
+      request: res.open,
       save: save
     ))
   if res.select.isSome and not save:
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 8eb42b65..48856837 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -98,7 +98,6 @@ type
 
   LineDataAuth = ref object of LineData
     url: URL
-    username: string
 
   NavDirection = enum
     ndPrev = "prev"
@@ -124,6 +123,7 @@ type
     devRandom: PosixStream
     display: FixedGrid
     forkserver*: ForkServer
+    formRequestMap*: Table[string, FormRequestType]
     hasload*: bool # has a page been successfully loaded since startup?
     inputBuffer*: string # currently uninterpreted characters
     iregex: Result[Regex, string]
@@ -1026,6 +1026,7 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
     images: images,
     isdump: pager.config.start.headless,
     charsetOverride: charsetOverride,
+    protocol: pager.config.protocol
   )
 
 # Load request in a new buffer.
@@ -1231,18 +1232,13 @@ proc updateReadLine*(pager: Pager) =
       case pager.linemode
       of lmLocation: pager.loadURL(lineedit.news)
       of lmUsername:
-        LineDataAuth(pager.lineData).username = lineedit.news
+        LineDataAuth(pager.lineData).url.username = lineedit.news
         pager.setLineEdit(lmPassword, hide = true)
       of lmPassword:
-        let data = LineDataAuth(pager.lineData)
-        let url = newURL(data.url)
-        url.username = data.username
+        let url = LineDataAuth(pager.lineData).url
         url.password = lineedit.news
-        pager.gotoURL(
-          newRequest(url), some(pager.container.url),
-          replace = pager.container,
-          referrer = pager.container
-        )
+        pager.gotoURL(newRequest(url), some(pager.container.url),
+          replace = pager.container, referrer = pager.container)
         pager.lineData = nil
       of lmCommand:
         pager.scommand = lineedit.news
@@ -1676,6 +1672,9 @@ proc redirect(pager: Pager; container: Container; response: Response;
         container.url.scheme == "http" and request.url.scheme == "https" or
         container.url.scheme == "https" and request.url.scheme == "http":
       pager.redirectTo(container, request)
+    #TODO perhaps make following behavior configurable?
+    elif request.url.scheme == "cgi-bin":
+      pager.alert("Blocked redirection attempt to " & $request.url)
     else:
       let url = request.url
       pager.ask("Warning: switch protocols? " & $url).then(proc(x: bool) =
@@ -1708,7 +1707,7 @@ proc connected(pager: Pager; container: Container; response: Response) =
   container.applyResponse(response, pager.config.external.mime_types)
   if response.status == 401: # unauthorized
     pager.setLineEdit(lmUsername)
-    pager.lineData = LineDataAuth(url: container.url)
+    pager.lineData = LineDataAuth(url: newURL(container.url))
     istream.sclose()
     return
   # This forces client to ask for confirmation before quitting.
@@ -1863,8 +1862,15 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
       pager.setLineEdit(lmBufferFile, "")
   of cetOpen:
     let url = event.request.url
-    if not event.save and (pager.container != container or
-        not container.isHoverURL(url)):
+    let sameScheme = container.url.scheme == url.scheme
+    if event.request.httpMethod != hmGet and (not sameScheme or
+        container.url.scheme in ["http", "https"] and
+        url.scheme in ["http", "https"]):
+      pager.alert("Blocked cross-scheme POST: " & $url)
+      return
+    #TODO this is horrible UX, async actions shouldn't block input
+    if pager.container != container or
+        not event.save and not container.isHoverURL(url):
       pager.ask("Open pop-up? " & $url).then(proc(x: bool) =
         if x:
           pager.gotoURL(event.request, some(container.url),
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index e471c1ae..c2ed2fb7 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -14,7 +14,6 @@ import config/config
 import css/cascade
 import css/cssparser
 import css/cssvalues
-import css/mediaquery
 import css/sheet
 import css/stylednode
 import html/catom
@@ -48,7 +47,6 @@ import monoucha/tojs
 import types/blob
 import types/cell
 import types/color
-import types/cookie
 import types/formdata
 import types/opt
 import types/url
@@ -146,6 +144,7 @@ type
     isdump*: bool
     charsets*: seq[Charset]
     charsetOverride*: Charset
+    protocol*: Table[string, ProtocolConfig]
 
 proc getFromOpaque[T](opaque: pointer; res: var T) =
   let opaque = cast[InterfaceOpaque](opaque)
@@ -300,7 +299,7 @@ macro task(fun: typed) =
   pfun.istask = true
   fun
 
-func getTitleAttr(node: StyledNode): string =
+func getTitleAttr(buffer: Buffer; node: StyledNode): string =
   if node == nil:
     return ""
   if node.t == stElement and node.node != nil:
@@ -336,7 +335,8 @@ func getClickable(styledNode: StyledNode): Element =
       return Element(styledNode.node)
     styledNode = styledNode.parent
 
-proc submitForm(form: HTMLFormElement; submitter: Element): Option[Request]
+proc submitForm(buffer: Buffer; form: HTMLFormElement; submitter: Element):
+  Request
 
 func canSubmitOnClick(fae: FormAssociatedElement): bool =
   if fae.form == nil:
@@ -350,7 +350,7 @@ func canSubmitOnClick(fae: FormAssociatedElement): bool =
     return true
   return false
 
-proc getClickHover(styledNode: StyledNode): string =
+proc getClickHover(buffer: Buffer; styledNode: StyledNode): string =
   let clickable = styledNode.getClickable()
   if clickable != nil:
     if clickable of HTMLAnchorElement:
@@ -359,15 +359,15 @@ proc getClickHover(styledNode: StyledNode): string =
       #TODO this is inefficient and also quite stupid
       let fae = FormAssociatedElement(clickable)
       if fae.canSubmitOnClick():
-        let req = fae.form.submitForm(fae)
-        if req.isSome:
-          return $req.get.url
+        let req = buffer.submitForm(fae.form, fae)
+        if req != nil:
+          return $req.url
       return "<" & $clickable.tagType & ">"
     elif clickable of HTMLOptionElement:
       return "<option>"
   ""
 
-proc getImageHover(styledNode: StyledNode): string =
+proc getImageHover(buffer: Buffer; styledNode: StyledNode): string =
   var styledNode = styledNode
   while styledNode != nil:
     if styledNode.t == stElement:
@@ -838,7 +838,7 @@ proc updateHover*(buffer: Buffer; cursorx, cursory: int): UpdateHoverResult
           elem.hover = true
           repaint = true
     for ht in HoverType:
-      let s = HoverFun[ht](thisnode)
+      let s = HoverFun[ht](buffer, thisnode)
       if buffer.hoverText[ht] != s:
         hover.add((ht, s))
         buffer.hoverText[ht] = s
@@ -1290,124 +1290,104 @@ func pickCharset(form: HTMLFormElement): Charset =
     return CHARSET_UTF_8
   return form.document.charset.getOutputEncoding()
 
+proc getFormRequestType(buffer: Buffer; scheme: string): FormRequestType =
+  buffer.config.protocol.withValue(scheme, p):
+    return p[].form_request
+  return frtHttp
+
+proc makeFormRequest(buffer: Buffer; parsedAction: URL; httpMethod: HttpMethod;
+    entryList: seq[FormDataEntry]; enctype: FormEncodingType): Request =
+  assert httpMethod in {hmGet, hmPost}
+  case buffer.getFormRequestType(parsedAction.scheme)
+  of frtFtp:
+    return newRequest(parsedAction) # get action URL
+  of frtData:
+    if httpMethod == hmGet:
+      # mutate action URL
+      let kvlist = entryList.toNameValuePairs()
+      #TODO with charset
+      parsedAction.query = some(serializeApplicationXWWWFormUrlEncoded(kvlist))
+      return newRequest(parsedAction, httpMethod)
+    return newRequest(parsedAction) # get action URL
+  of frtMailto:
+    if httpMethod == hmGet:
+      # mailWithHeaders
+      let kvlist = entryList.toNameValuePairs()
+      #TODO with charset
+      let headers = serializeApplicationXWWWFormUrlEncoded(kvlist,
+        spaceAsPlus = false)
+      parsedAction.query = some(headers)
+      return newRequest(parsedAction, httpMethod)
+    # mail as body
+    let kvlist = entryList.toNameValuePairs()
+    let body = if enctype == fetTextPlain:
+      let text = serializePlainTextFormData(kvlist)
+      percentEncode(text, PathPercentEncodeSet)
+    else:
+      #TODO with charset
+      serializeApplicationXWWWFormUrlEncoded(kvlist)
+    if parsedAction.query.isNone:
+      parsedAction.query = some("")
+    if parsedAction.query.get != "":
+      parsedAction.query.get &= '&'
+    parsedAction.query.get &= "body=" & body
+    return newRequest(parsedAction, httpMethod)
+  of frtHttp:
+    if httpMethod == hmGet:
+      # mutate action URL
+      let kvlist = entryList.toNameValuePairs()
+      #TODO with charset
+      let query = serializeApplicationXWWWFormUrlEncoded(kvlist)
+      parsedAction.query = some(query)
+      return newRequest(parsedAction, httpMethod)
+    # submit as entity body
+    var body: Option[string]
+    var multipart: Option[FormData]
+    case enctype
+    of fetUrlencoded:
+      #TODO with charset
+      let kvlist = entryList.toNameValuePairs()
+      body = some(serializeApplicationXWWWFormUrlEncoded(kvlist))
+    of fetMultipart:
+      #TODO with charset
+      multipart = some(serializeMultipartFormData(entryList))
+    of fetTextPlain:
+      #TODO with charset
+      let kvlist = entryList.toNameValuePairs()
+      body = some(serializePlainTextFormData(kvlist))
+    let headers = newHeaders({"Content-Type": $enctype})
+    return newRequest(parsedAction, httpMethod, headers, body, multipart)
+
 # https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
-proc submitForm(form: HTMLFormElement; submitter: Element): Option[Request] =
+proc submitForm(buffer: Buffer; form: HTMLFormElement; submitter: Element): Request =
   if form.constructingEntryList:
-    return none(Request)
+    return nil
   #TODO submit()
   let charset = form.pickCharset()
   discard charset #TODO pass to constructEntryList
-  let entrylist = form.constructEntryList(submitter)
-
+  let entryList = form.constructEntryList(submitter)
   let subAction = submitter.action()
   let action = if subAction != "":
     subAction
   else:
     $form.document.url
-
   #TODO encoding-parse
   let url = submitter.document.parseURL(action)
   if url.isNone:
-    return none(Request)
-
-  var parsedaction = url.get
-  let scheme = parsedaction.scheme
+    return nil
+  let parsedAction = url.get
   let enctype = submitter.enctype()
-  let formmethod = submitter.formmethod()
-  if formmethod == fmDialog:
-    #TODO
-    return none(Request)
-  let httpmethod = if formmethod == fmGet:
-    hmGet
-  else:
-    assert formmethod == fmPost
-    hmPost
-
+  let formMethod = submitter.formmethod()
+  let httpMethod = case formMethod
+  of fmDialog: return nil #TODO
+  of fmGet: hmGet
+  of fmPost: hmPost
   #let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"):
   #  submitter.attr("formtarget")
   #else:
   #  submitter.target()
   #let noopener = true #TODO
-
-  template mutateActionUrl() =
-    let kvlist = entrylist.toNameValuePairs()
-    #TODO with charset
-    let query = serializeApplicationXWWWFormUrlEncoded(kvlist)
-    parsedaction.query = some(query)
-    return some(newRequest(parsedaction, httpmethod))
-
-  template submitAsEntityBody() =
-    var mimetype: string
-    var body: Option[string]
-    var multipart: Option[FormData]
-    case enctype
-    of fetUrlencoded:
-      #TODO with charset
-      let kvlist = entrylist.toNameValuePairs()
-      body = some(serializeApplicationXWWWFormUrlEncoded(kvlist))
-      mimeType = $enctype
-    of fetMultipart:
-      #TODO with charset
-      multipart = some(serializeMultipartFormData(entrylist))
-      mimetype = $enctype
-    of fetTextPlain:
-      #TODO with charset
-      let kvlist = entrylist.toNameValuePairs()
-      body = some(serializePlainTextFormData(kvlist))
-      mimetype = $enctype
-    let req = newRequest(parsedaction, httpmethod, @{"Content-Type": mimetype},
-      body, multipart)
-    return some(req) #TODO multipart
-
-  template getActionUrl() =
-    return some(newRequest(parsedaction))
-
-  template mailWithHeaders() =
-    let kvlist = entrylist.toNameValuePairs()
-    #TODO with charset
-    let headers = serializeApplicationXWWWFormUrlEncoded(kvlist,
-      spaceAsPlus = false)
-    parsedaction.query = some(headers)
-    return some(newRequest(parsedaction, httpmethod))
-
-  template mailAsBody() =
-    let kvlist = entrylist.toNameValuePairs()
-    let body = if enctype == fetTextPlain:
-      let text = serializePlainTextFormData(kvlist)
-      percentEncode(text, PathPercentEncodeSet)
-    else:
-      #TODO with charset
-      serializeApplicationXWWWFormUrlEncoded(kvlist)
-    if parsedaction.query.isNone:
-      parsedaction.query = some("")
-    if parsedaction.query.get != "":
-      parsedaction.query.get &= '&'
-    parsedaction.query.get &= "body=" & body
-    return some(newRequest(parsedaction, httpmethod))
-
-  case scheme
-  of "ftp", "javascript":
-    getActionUrl
-  of "data":
-    if formmethod == fmGet:
-      mutateActionUrl
-    else:
-      assert formmethod == fmPost
-      getActionUrl
-  of "mailto":
-    if formmethod == fmGet:
-      mailWithHeaders
-    else:
-      assert formmethod == fmPost
-      mailAsBody
-  else:
-    # Note: only http & https are defined by the standard.
-    # Assume an HTTP-like protocol.
-    if formmethod == fmGet:
-      mutateActionUrl
-    else:
-      assert formmethod == fmPost
-      submitAsEntityBody
+  return buffer.makeFormRequest(parsedAction, httpMethod, entryList, enctype)
 
 proc setFocus(buffer: Buffer; e: Element): bool =
   if buffer.document.focus != e:
@@ -1422,10 +1402,10 @@ proc restoreFocus(buffer: Buffer): bool =
     return true
 
 type ReadSuccessResult* = object
-  open*: Option[Request]
+  open*: Request
   repaint*: bool
 
-proc implicitSubmit(input: HTMLInputElement): Option[Request] =
+proc implicitSubmit(buffer: Buffer; input: HTMLInputElement): Request =
   let form = input.form
   if form != nil and form.canSubmitImplicitly():
     var defaultButton: Element
@@ -1434,9 +1414,10 @@ proc implicitSubmit(input: HTMLInputElement): Option[Request] =
         defaultButton = element
         break
     if defaultButton != nil:
-      return submitForm(form, defaultButton)
+      return buffer.submitForm(form, defaultButton)
     else:
-      return submitForm(form, form)
+      return buffer.submitForm(form, form)
+  return nil
 
 proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): ReadSuccessResult
     {.proxy.} =
@@ -1453,13 +1434,13 @@ proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): ReadSuccessResult
         input.invalid = true
         buffer.do_reshape()
         result.repaint = true
-        result.open = implicitSubmit(input)
+        result.open = buffer.implicitSubmit(input)
       else:
         input.value = s
         input.invalid = true
         buffer.do_reshape()
         result.repaint = true
-        result.open = implicitSubmit(input)
+        result.open = buffer.implicitSubmit(input)
     of TAG_TEXTAREA:
       let textarea = HTMLTextAreaElement(buffer.document.focus)
       textarea.value = s
@@ -1488,7 +1469,7 @@ type
     selected*: seq[int]
 
   ClickResult* = object
-    open*: Option[Request]
+    open*: Request
     readline*: Option[ReadLineResult]
     repaint*: bool
     select*: Option[SelectResult]
@@ -1542,26 +1523,20 @@ proc click(buffer: Buffer; anchor: HTMLAnchorElement): ClickResult =
   var repaint = buffer.restoreFocus()
   let url = parseURL(anchor.href, some(buffer.baseURL))
   if url.isSome:
-    let url = url.get
+    var url = url.get
     if url.scheme == "javascript":
-      if buffer.config.scripting:
-        let s = buffer.evalJSURL(url)
-        buffer.do_reshape()
-        repaint = true
-        if s.isSome:
-          let url = newURL("data:text/html," & s.get).get
-          let req = newRequest(url, hmGet)
-          return ClickResult(
-            repaint: repaint,
-            open: some(req)
-          )
-      return ClickResult(
-        repaint: repaint
-      )
-    return ClickResult(
-      repaint: repaint,
-      open: some(newRequest(url, hmGet))
-    )
+      if not buffer.config.scripting:
+        return ClickResult(repaint: repaint)
+      let s = buffer.evalJSURL(url)
+      buffer.do_reshape()
+      repaint = true
+      if s.isNone:
+        return ClickResult(repaint: repaint)
+      let urls = newURL("data:text/html," & s.get)
+      if urls.isNone:
+        return ClickResult(repaint: repaint)
+      url = urls.get
+    return ClickResult(repaint: repaint, open: newRequest(url, hmGet))
   return ClickResult(repaint: repaint)
 
 proc click(buffer: Buffer; option: HTMLOptionElement): ClickResult =
@@ -1572,10 +1547,10 @@ proc click(buffer: Buffer; option: HTMLOptionElement): ClickResult =
 
 proc click(buffer: Buffer; button: HTMLButtonElement): ClickResult =
   if button.form != nil:
-    var open = none(Request)
+    var open: Request = nil
     case button.ctype
     of btSubmit:
-      open = submitForm(button.form, button)
+      open = buffer.submitForm(button.form, button)
     of btReset:
       button.form.reset()
       buffer.do_reshape()
@@ -1651,7 +1626,10 @@ proc click(buffer: Buffer; input: HTMLInputElement): ClickResult =
     return ClickResult(repaint: false)
   of itSubmit, itButton:
     if input.form != nil:
-      return ClickResult(open: submitForm(input.form, input), repaint: repaint)
+      return ClickResult(
+        open: buffer.submitForm(input.form, input),
+        repaint: repaint
+      )
     return ClickResult(repaint: false)
   else:
     # default is text.