about summary refs log blame commit diff stats
path: root/src/html/xmlhttprequest.nim
blob: 1fc2e8d7b2fe5d740c7da0f4e88a2077785ddee7 (plain) (tree)
1
2
3
4
5
6
7
8
9

                   
                 
 
                 
               
                 
                 
                      
                     
                    

                      

                          
                       
                       
                    
                
                
                   


                                   





                                   
 
                            




                                                    

                           
                                                                            
 
                                                       



                                                                
                                   
                                          


                                  
                    
                      











                                                      



                                       
                           



                                                     
                   









                                                                              
   

                                            
 




                                                          






                             
                                 
                                                        
       
                                                    
 



                                                                               
                                           

                                                      
              
                                                        
                       



                                                                 
                                  

                                     
           
                             
       
                             
                                 
                             

                                    



                                               
             
 




























                                                                              
                                              

                          
                                                        












































































































































































                                                                                

                                                  



                                                                            
                                                  








                                                                                
                                                       
import std/options
import std/strutils
import std/tables

import html/catom
import html/dom
import html/event
import io/promise
import js/domexception
import loader/headers
import loader/loader
import loader/request
import loader/response
import monoucha/fromjs
import monoucha/javascript
import monoucha/jserror
import monoucha/quickjs
import monoucha/tojs
import types/opt
import types/url
import utils/twtstr

type
  XMLHttpRequestResponseType = enum
    xhrtUnknown = ""
    xhrtArraybuffer = "arraybuffer"
    xhrtBlob = "blob"
    xhrtDocument = "document"
    xhrtJSON = "json"
    xhrtText = "text"

  XMLHttpRequestState = enum
    xhrsUnsent = (0u16, "UNSENT")
    xhrsOpened = (1u16, "OPENED")
    xhrsHeadersReceived = (2u16, "HEADERS_RECEIVED")
    xhrsLoading = (3u16, "LOADING")
    xhrsDone = (4u16, "DONE")

  XMLHttpRequestFlag = enum
    xhrfSend, xhrfUploadListener, xhrfSync, xhrfUploadComplete, xhrfTimedOut

  XMLHttpRequestEventTarget = ref object of EventTarget

  XMLHttpRequestUpload = ref object of XMLHttpRequestEventTarget

  XMLHttpRequest = ref object of XMLHttpRequestEventTarget
    readyState: XMLHttpRequestState
    upload {.jsget.}: XMLHttpRequestUpload
    flags: set[XMLHttpRequestFlag]
    requestMethod: HttpMethod
    requestURL: URL
    headers: Headers
    response: Response
    responseType {.jsget.}: XMLHttpRequestResponseType
    timeout {.jsget.}: uint32

  ProgressEvent = ref object of Event
    lengthComputable {.jsget.}: bool
    loaded {.jsget.}: uint32
    total {.jsget.}: uint32

  ProgressEventInit = object of EventInit
    lengthComputable: bool
    loaded: uint32
    total: uint32

jsDestructor(XMLHttpRequestEventTarget)
jsDestructor(XMLHttpRequestUpload)
jsDestructor(XMLHttpRequest)
jsDestructor(ProgressEvent)

func newXMLHttpRequest(): XMLHttpRequest {.jsctor.} =
  let upload = XMLHttpRequestUpload()
  return XMLHttpRequest(
    upload: upload,
    headers: newHeaders()
  )

proc newProgressEvent(ctype: CAtom; init = ProgressEventInit()): ProgressEvent
    {.jsctor.} =
  let event = ProgressEvent(
    ctype: ctype,
    lengthComputable: init.lengthComputable,
    loaded: init.loaded,
    total: init.total
  )
  Event(event).innerEventCreationSteps(init)
  return event

func readyState(this: XMLHttpRequest): uint16 {.jsfget.} =
  return uint16(this.readyState)

proc parseMethod(s: string): DOMResult[HttpMethod] =
  return case s.toLowerAscii()
  of "get": ok(hmGet)
  of "delete": ok(hmDelete)
  of "head": ok(hmHead)
  of "options": ok(hmOptions)
  of "patch": ok(hmPatch)
  of "post": ok(hmPost)
  of "put": ok(hmPut)
  of "connect", "trace", "track":
    errDOMException("Forbidden method", "SecurityError")
  else:
    errDOMException("Invalid method", "SyntaxError")

#TODO the standard says that no async should be treated differently from
# undefined. idk if (and where) this actually matters.
proc open(ctx: JSContext; this: XMLHttpRequest; httpMethod, url: string;
    async = true; username = ""; password = ""): Err[DOMException] {.jsfunc.} =
  let httpMethod = ?parseMethod(httpMethod)
  let global = ctx.getGlobal()
  let x = parseURL(url, some(global.document.baseURL))
  if x.isNone:
    return errDOMException("Invalid URL", "SyntaxError")
  let parsedURL = x.get
  if not async and ctx.getWindow() != nil and
      (this.timeout != 0 or this.responseType != xhrtUnknown):
    return errDOMException("Today's horoscope: don't go outside",
      "InvalidAccessError")
  #TODO terminate fetch controller
  this.flags.excl(xhrfSend)
  this.flags.excl(xhrfUploadListener)
  if async:
    this.flags.excl(xhrfSync)
  else:
    this.flags.incl(xhrfSync)
  this.requestMethod = httpMethod
  this.headers = newHeaders()
  this.response = makeNetworkError()
  this.requestURL = parsedURL
  #TODO response object, received bytes
  if this.readyState != xhrsOpened:
    this.readyState = xhrsOpened
    global.fireEvent(satReadystatechange, this)
  return ok()

proc checkOpened(this: XMLHttpRequest): DOMResult[void] =
  if this.readyState != xhrsOpened:
    return errDOMException("ready state was expected to be `opened'",
      "InvalidStateError")
  ok()

proc checkSendFlag(this: XMLHttpRequest): DOMResult[void] =
  if xhrfSend in this.flags:
    return errDOMException("`send' flag is set", "InvalidStateError")
  ok()

proc setRequestHeader(this: XMLHttpRequest; name, value: string):
    DOMResult[void] {.jsfunc.} =
  ?this.checkOpened()
  ?this.checkSendFlag()
  if not name.isValidHeaderName() or not value.isValidHeaderValue():
    return errDOMException("Invalid header name or value", "SyntaxError")
  if isForbiddenHeader(name, value):
    return ok()
  this.headers.table[name.toHeaderCase()] = @[value]
  ok()

proc fireProgressEvent(window: Window; target: EventTarget; name: StaticAtom;
    loaded, length: uint32) =
  let event = newProgressEvent(window.factory.toAtom(name), ProgressEventInit(
    loaded: loaded,
    total: length,
    lengthComputable: length != 0
  ))
  discard window.jsctx.dispatch(target, event)

# Forward declaration hack
var windowFetch*: proc(window: Window; input: JSRequest;
  init = none(RequestInit)): JSResult[FetchPromise] {.nimcall.} = nil

proc errorSteps(window: Window; this: XMLHttpRequest; name: StaticAtom) =
  this.readyState = xhrsDone
  this.response = makeNetworkError()
  this.flags.excl(xhrfSend)
  #TODO sync?
  window.fireEvent(satReadystatechange, this)
  if xhrfUploadComplete notin this.flags:
    this.flags.incl(xhrfUploadComplete)
    if xhrfUploadListener in this.flags:
      window.fireProgressEvent(this.upload, name, 0, 0)
      window.fireProgressEvent(this.upload, satLoadend, 0, 0)
  window.fireProgressEvent(this, name, 0, 0)
  window.fireProgressEvent(this, satLoadend, 0, 0)

proc handleErrors(window: Window; this: XMLHttpRequest): DOMException =
  if xhrfSend notin this.flags:
    return nil
  if xhrfTimedOut in this.flags:
    window.errorSteps(this, satTimeout)
    if xhrfSync in this.flags:
      return newDOMException("XHR timed out", "TimeoutError")
  elif rfAborted in this.response.flags:
    window.errorSteps(this, satAbort)
    if xhrfSync in this.flags:
      return newDOMException("XHR aborted", "AbortError")
  elif this.response.responseType == rtError:
    window.errorSteps(this, satError)
    if xhrfSync in this.flags:
      return newDOMException("Network error in XHR", "NetworkError")
  return nil

proc send(ctx: JSContext; this: XMLHttpRequest; body = JS_NULL): DOMResult[void]
    {.jsfunc.} =
  ?this.checkOpened()
  ?this.checkSendFlag()
  var body = body
  if this.requestMethod in {hmGet, hmHead}:
    body = JS_NULL
  let request = newRequest(this.requestURL, this.requestMethod, this.headers)
  if not JS_IsNull(body):
    let document = fromJS[Document](ctx, body)
    if document.isSome:
      #TODO replace surrogates
      let document = document.get
      request.body = RequestBody(t: rbtString, s: document.serializeFragment())
    #TODO else...
    if "Content-Type" in this.headers:
      request.headers["Content-Type"].setContentTypeAttr("charset", "UTF-8")
    elif document.isSome:
      request.headers["Content-Type"] = "text/html;charset=UTF-8"
  let jsRequest = JSRequest(
    #TODO unsafe request flag, client, cors credentials mode,
    # use-url-credentials, initiator type
    request: request,
    mode: rmCors,
    credentialsMode: cmSameOrigin,
  )
  if JS_IsNull(body):
    this.flags.incl(xhrfUploadComplete)
  else:
    this.flags.excl(xhrfUploadComplete)
  this.flags.excl(xhrfTimedOut)
  this.flags.incl(xhrfSend)
  let window = ctx.getWindow()
  if xhrfSync notin this.flags: # async
    window.fireProgressEvent(this, satLoadstart, 0, 0)
    let p = window.windowFetch(jsRequest)
    if p.isSome:
      p.get.then(proc(res: JSResult[Response]) =
        if res.isNone:
          this.response = makeNetworkError()
          discard window.handleErrors(this)
          return
        let response = res.get
        this.response = response
        this.readyState = xhrsHeadersReceived
        window.fireEvent(satReadystatechange, this)
      )
  else: # sync
    discard #TODO
  ok()

#TODO abort

proc responseURL(this: XMLHttpRequest): string {.jsfget.} =
  return this.response.surl

proc status(this: XMLHttpRequest): uint16 {.jsfget.} =
  return this.response.status

proc statusText(this: XMLHttpRequest): string {.jsfget.} =
  return ""

proc getResponseHeader(this: XMLHttpRequest; name: string): string {.jsfunc.} =
  #TODO ?
  return this.response.headers.table.getOrDefault(name)[0]

#TODO getAllResponseHeaders

proc setResponseType(ctx: JSContext; this: XMLHttpRequest;
    value: XMLHttpRequestResponseType): Err[DOMException]
    {.jsfset: "responseType".} =
  let window = ctx.getWindow()
  if window == nil and value == xhrtDocument:
    return ok()
  if this.readyState in {xhrsLoading, xhrsDone}:
    return errDOMException("readyState must not be loading or done",
      "InvalidStateError")
  if window != nil and xhrfSync in this.flags:
    return errDOMException("responseType may not be set on synchronous XHR",
      "InvalidAccessError")
  this.responseType = value
  ok()

proc setTimeout(ctx: JSContext; this: XMLHttpRequest; value: uint32):
    Err[DOMException] {.jsfset: "timeout".} =
  if ctx.getWindow() != nil and xhrfSync in this.flags:
    return errDOMException("timeout may not be set on synchronous XHR",
      "InvalidAccessError")
  this.timeout = value
  ok()

# Event reflection

const ReflectMap = [
  cint(0): satLoadstart,
  satProgress,
  satAbort,
  satError,
  satLoad,
  satTimeout,
  satLoadend,
  satReadystatechange
]

proc jsReflectGet(ctx: JSContext; this: JSValue; magic: cint): JSValue
    {.cdecl.} =
  let val = toJS(ctx, $ReflectMap[magic])
  let atom = JS_ValueToAtom(ctx, val)
  var res = JS_NULL
  var desc: JSPropertyDescriptor
  if JS_GetOwnProperty(ctx, addr desc, this, atom) > 0:
    JS_FreeValue(ctx, desc.setter)
    JS_FreeValue(ctx, desc.getter)
    res = JS_GetProperty(ctx, this, atom)
  JS_FreeValue(ctx, val)
  JS_FreeAtom(ctx, atom)
  return res

proc jsReflectSet(ctx: JSContext; this, val: JSValue; magic: cint): JSValue
    {.cdecl.} =
  if JS_IsFunction(ctx, val):
    let atom = ReflectMap[magic]
    let target = fromJS[EventTarget](ctx, this).get
    ctx.definePropertyC(this, "on" & $atom, JS_DupValue(ctx, val))
    #TODO I haven't checked but this might also be wrong
    doAssert ctx.addEventListener(target, ctx.toAtom(atom), val).isSome
  return JS_DupValue(ctx, val)

func xhretGetSet(): seq[TabGetSet] =
  result = @[]
  for i, it in ReflectMap:
    if it == satReadystatechange:
      break
    result.add(TabGetSet(
      name: "on" & $it,
      get: jsReflectGet,
      set: jsReflectSet,
      magic: int16(i)
    ))

proc addXMLHttpRequestModule*(ctx: JSContext) =
  let eventTargetCID = ctx.getClass("EventTarget")
  let eventCID = ctx.getClass("Event")
  const getset0 = xhretGetSet()
  let xhretCID = ctx.registerType(XMLHttpRequestEventTarget, eventTargetCID,
    hasExtraGetSet = true, extraGetSet = getset0)
  ctx.registerType(XMLHttpRequestUpload, xhretCID)
  ctx.registerType(ProgressEvent, eventCID)
  const getset1 = [TabGetSet(
    name: "onreadystatechange",
    get: jsReflectGet,
    set: jsReflectSet,
    magic: int16(ReflectMap.high)
  )]
  let xhrCID = ctx.registerType(XMLHttpRequest, xhretCID, hasExtraGetSet = true,
    extraGetSet = getset1)
  ctx.defineConsts(xhrCID, XMLHttpRequestState, uint16)