123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441# 1 "src/lib/eliom_request.client.ml"(* Ocsigen
* http://www.ocsigen.org
* Copyright (C) 2010 Vincent Balat
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, with linking exception;
* either version 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*)openJs_of_ocamlopenEliom_libexceptionLooping_redirectionexceptionFailed_requestofintexceptionProgram_terminatedexceptionNon_xml_contentmoduleXmlHttpRequest=Js_of_ocaml_lwt.XmlHttpRequestletsection=Lwt_log.Section.make"eliom:request"(* == ... *)letmax_redirection_level=12letshort_url_re=new%jsJs.regExp(Js.bytestring"^([^\\?]*)(\\?(.*))?$")letget_cookie_info_for_uri_jsuri_js=matchUrl.url_of_string(Js.to_stringuri_js)with|None->(* Decoding failed *)Js.Opt.caseshort_url_re##(execuri_js)(fun()->assertfalse)(funres->letmatch_result=Js.match_resultresinletpath=Url.path_of_path_string(Js.to_string(Js.Optdef.get(Js.array_getmatch_result1)(fun()->assertfalse)))inletpath=matchpathwith|""::_->path(* absolute *)|_->Eliom_common_base.make_actual_path(Eliom_request_info.get_csp_original_full_path()@path)inEliom_request_info.get_csp_ssl(),path)|Some(Url.Https{Url.hu_path=path;_})->true,path|Some(Url.Http{Url.hu_path=path;_})->false,path|Some(Url.File{Url.fu_path=path;_})->false,pathletget_cookie_info_for_uriuri=leturi_js=Js.bytestringuriinget_cookie_info_for_uri_jsuri_jstype'aresult=XmlHttpRequest.http_frame->'aletxml_resultx=matchx.XmlHttpRequest.content_xml()with|None->raiseNon_xml_content|Somev->vletstring_resultx=x.XmlHttpRequest.content(*TODO: use Url.Current.set *)letredirect_get?window_name?window_featuresurl=matchwindow_namewith|None->Dom_html.window##.location##.href:=Js.stringurl|Somewindow_name->ignoreDom_html.window##(open_(Js.stringurl)(Js.stringwindow_name)(Js.Opt.map(Js.Opt.optionwindow_features)Js.string))letredirect_post?window_nameurlparams=letf=Dom_html.createFormDom_html.documentinf##.action:=Js.stringurl;f##._method:=Js.string"post";(matchwindow_namewithNone->()|Somewn->f##.target:=Js.stringwn);List.iter(fun(n,v)->matchvwith|`Stringv->leti=Dom_html.createTextarea~name:(Js.stringn)Dom_html.documentini##.value:=v;Dom.appendChildfi|`File_->Lwt_log.raise_error~section"redirect_post not implemented for files")params;f##.style##.display:=Js.string"none";Dom.appendChildDom_html.document##.bodyf;(* firefox accepts submit only on forms in the document *)f##submit(* Forms cannot use PUT http method: do not redirect *)letredirect_put?window_name:__url_params=Lwt_log.raise_error~section"redirect_put not implemented"(* Forms cannot use DELETE http method: do not redirect *)letredirect_delete?window_name:__url_params=Lwt_log.raise_error~section"redirect_delete not implemented"letnl_template=Eliom_parameter.make_non_localized_parameters~prefix:"eliom"~name:"template"(Eliom_parameter.string"name")(* Warning: it must correspond to [nl_template]. *)letnl_template_string="__nl_n_eliom-template.name"moduleAdditional_headers=structmoduleHeaders=Map.Make(String)letheaders=refHeaders.emptyletaddheadervalue=headers:=Headers.update(String.lowercase_asciiheader)(fun_->Somevalue)!headersletremoveheader=headers:=Headers.removeheader!headersletto_list()=Headers.bindings!headersendletlocked,set_locked=React.S.createfalseletlock()=set_lockedtrueletunlock()=set_lockedfalse(** Same as XmlHttpRequest.perform_raw_url, but:
- sends tab cookies in an HTTP header
- does half and full XHR redirections according to headers
The optional parameter [~cookies_info] is a pair
containing the information (secure, path)
that is taken into account for finding tab cookies to send.
If not present, the path and protocol are taken from the URL.
*)letsend?with_credentials?(expecting_process_page=false)?cookies_info?get_args?post_args?progress?upload_progress?override_mime_typeurlresult=letrecauxi?cookies_info?(get_args=[])?post_argsurl=lethttps,path=matchcookies_infowith|Somec->c(* CCC Is it really necessary to allow to specify cookie_info here?
hence, is it necessary to send it with the links? (attribute data-eliom-cookie-info) *)|None->get_cookie_info_for_uriurlinlethost=matchUrl.url_of_stringurlwith|Some(Url.Httpurl)|Some(Url.Httpsurl)->Someurl.Url.hu_host|Some(Url.File_)->None|None->(* decoding failed: it is a relative link *)SomeUrl.Current.hostinlethost=matchhostwith|Somehostwhenhost=Url.Current.host->Some(Eliom_process.get_info()).Eliom_common.cpi_hostname|_->hostinletcookies=Eliommod_cookies.get_cookies_to_sendhosthttpspathinletheaders=matchcookieswith|[]->[]|_->[Eliom_common.tab_cookies_header_name,encode_header_valuecookies]inletheaders=ifJs.Optdef.testJs.Unsafe.global##.___eliom_use_cookie_substitutes_then(* Cookie substitutes are for iOS WKWebView *)letcookies=Eliommod_cookies.get_cookies_to_send~in_local_storage:truehosthttpspathin(Eliom_common.cookie_substitutes_header_name,encode_header_valuecookies)::headerselseheadersinletheaders=Additional_headers.to_list()@headersin(* CCC *
For now we assume that an eliom application is not distributed
among different server with different hostnames:
to do that It is needed to change that part a bit to be able to
send the process name to every host serving eliom pages.
Do not send it to everybody: when doing a cross domain request
with additional headers like thoose, an OPTION request is done
before to check if the request is authorized. Some server does
not support it ( like google ones for instance ) *)letheaders=matchhostwith|Somehostwhenhost=Url.Current.host->(Eliom_common.tab_cpi_header_name,encode_header_value(Eliom_process.get_info()))::headers|_->headersinletheaders=ifexpecting_process_pagethenletcontent_type=ifDom_html.onIE&¬(Js.Optdef.test(Js.Unsafe.coerceDom_html.document)##.adoptNode)then(* ie < 9 does not know xhtml+xml content type, but ie 9
can use it and need it to use adoptNode *)"application/xml"else"application/xhtml+xml"in("Accept",content_type)::(Eliom_common.expecting_process_page_name,encode_header_valuetrue)::headerselseheadersinletget_args=ifexpecting_process_page(* we add this parameter to ensure that the xhr request is
different from the normal ones: we can't ensure that the
browser won't cache the content of the page ( for instance
when clicking the back button ). That way we are sure that an
xhr answer won't be used in place of a normal answer. *)then(Eliom_common.nl_get_appl_parameter,"true")::get_argselseget_argsinletcheck_headerscodeheaders=ifexpecting_process_pagethenifcode=204thentrueelseheadersEliom_common.appl_name_header_name=Some(Eliom_process.get_application_name())elsetrueintry%lwtlet%lwtr=letcontents=matchpost_argswith|Somepost_args->Some(`POST_formpost_args)|None->NoneinXmlHttpRequest.perform_raw_url?with_credentials?headers:(Someheaders)?content_type:None?contents~get_args~check_headers?progress?upload_progress?override_mime_typeurlinletwait_for_unlock,unlock=Lwt.wait()in(ifnot@@React.S.valuelockedthenLwt.wakeupunlock()elseletunlock_event=React.E.once@@React.S.changeslockedinDom_reference.retain_genericwait_for_unlock~keep:(React.E.map(fun_->Lwt.wakeupunlock())unlock_event));let%lwt()=wait_for_unlockin(ifJs.Optdef.testJs.Unsafe.global##.___eliom_use_cookie_substitutes_thenmatch(* Cookie substitutes are for iOS WKWebView *)r.XmlHttpRequest.headersEliom_common.set_cookie_substitutes_header_namewith|None|Some""->()|Somecookie_substitutes->Eliommod_cookies.update_cookie_table~in_local_storage:truehost(Eliommod_cookies.cookieset_of_jsoncookie_substitutes));(matchr.XmlHttpRequest.headersEliom_common.set_tab_cookies_header_namewith|None|Some""->()(* Empty tab_cookies for IE compat *)|Sometab_cookies->lettab_cookies=Eliommod_cookies.cookieset_of_jsontab_cookiesinEliommod_cookies.update_cookie_tablehosttab_cookies);ifr.XmlHttpRequest.code=204thenmatchr.XmlHttpRequest.headersEliom_common.full_xhr_redir_headerwith|None|Some""->(matchr.XmlHttpRequest.headersEliom_common.half_xhr_redir_headerwith|None|Some""->Lwt.return(r.XmlHttpRequest.url,None)|Some_uri->redirect_posturl(matchpost_argswith|Somepost_args->post_args|None->[]);Lwt.failProgram_terminated)|Someuri->ifi<max_redirection_levelthenaux(i+1)(Url.resolveuri)elseLwt.failLooping_redirectionelseifexpecting_process_pagethenleturl=matchr.XmlHttpRequest.headersEliom_common.response_url_headerwith|None|Some""->Url.add_get_argsurl(List.tlget_args)|Someurl->Url.resolveurlinLwt.return(url,Some(resultr))elseifr.XmlHttpRequest.code=200||XmlHttpRequest.(r.code=0&&r.content<>"")(* HACK for file access within Cordova which yields code 0 *)(* Code 0 might mean a network error, but then we have no
content. *)thenLwt.return(r.XmlHttpRequest.url,Some(resultr))elseLwt.fail(Failed_requestr.XmlHttpRequest.code)withXmlHttpRequest.Wrong_headers(code,headers)->((* We are requesting application content and the headers tels
us that the answer is not application content *)matchheadersEliom_common.appl_name_header_namewith|None|Some""->(* Empty appl_name for IE compat. *)(matchpost_argswith|None->redirect_geturl|_->Lwt_log.raise_error~section"can't silently redirect a Post request to non application content");Lwt.failProgram_terminated|Someappl_name->letcurrent_appl_name=Eliom_process.get_application_name()inifappl_name=current_appl_namethenassertfalse(* we can't go here:
this case is already handled before *)else(Lwt_log.ign_warning_f~section"received content for application %S when running application %s"appl_namecurrent_appl_name;Lwt.fail(Failed_requestcode)))inlet%lwturl,content=aux0?cookies_info?get_args?post_argsurlinletfilter_urlurl={urlwithUrl.hu_arguments=List.filter(fun(e,_)->e<>nl_template_string)url.Url.hu_arguments}inLwt.return((matchUrl.url_of_stringurlwith|Some(Url.Httpurl)->Url.string_of_url(Url.Http(filter_urlurl))|Some(Url.Httpsurl)->Url.string_of_url(Url.Https(filter_urlurl))|_->url),content)(* BEGIN FORMDATA HACK *)letadd_button_arginjargsform=letbutton=Js.Unsafe.global##.eliomLastButtoninJs.Unsafe.global##.eliomLastButton:=None;matchbuttonwith|None->args|Someb->letname,value,b_form=matchDom_html.taggedbwith|Dom_html.Buttonb->b##.name,b##.value,b##.form|Dom_html.Inputb->b##.name,b##.value,b##.form|_->assertfalseinletname=Js.to_stringnameinifname<>""&&b_form=Js.someformthenmatchargswith|None->Some[name,injvalue]|Somel->Some((name,injvalue)::l)elseargs(* END FORMDATA HACK *)(** Send a GET form with tab cookies and half/full XHR.
If [~post_params] is present, the HTTP method will be POST,
with form data in the URL.
If [~get_params] is present, it will be appended to the form fields.
*)letsend_get_form?with_credentials?expecting_process_page?cookies_info?(get_args=[])?post_args?progress?upload_progress?override_mime_typeformurl=letget_args=get_args@Form.get_form_contentsformin(* BEGIN FORMDATA HACK *)letget_args=add_button_argJs.to_string(Someget_args)formin(* END FORMDATA HACK *)send?with_credentials?expecting_process_page?cookies_info?get_args?post_args?progress?upload_progress?override_mime_typeurl(** Send a POST form with tab cookies and half/full XHR. *)letsend_post_form?with_credentials?expecting_process_page?cookies_info?get_args?post_args?progress?upload_progress?override_mime_typeformurl=(* BEGIN FORMDATA HACK *)letpost_args=match(add_button_arg(funx->`Stringx)(Some(Form.form_elementsform))form,post_args)with|Somel,Somel'->Some(l@l')|Somel,_|_,Somel->Somel|None,None->Nonein(* END FORMDATA HACK *)send?with_credentials?expecting_process_page?cookies_info?get_args?post_args?progress?upload_progress?override_mime_typeurllethttp_get?with_credentials?expecting_process_page?cookies_info?progress?upload_progress?override_mime_typeurlget_args=send?with_credentials?expecting_process_page?cookies_info?progress?upload_progress?override_mime_type~get_argsurllethttp_post?with_credentials?expecting_process_page?cookies_info?progress?upload_progress?override_mime_typeurlpost_args=send?with_credentials?expecting_process_page?cookies_info~post_args?progress?upload_progress?override_mime_typeurllethttp_put?with_credentials?expecting_process_page?cookies_info?progress?upload_progress?override_mime_typeurlpost_args=send?with_credentials?expecting_process_page?cookies_info~post_args?progress?upload_progress?override_mime_typeurllethttp_delete?with_credentials?expecting_process_page?cookies_info?progress?upload_progress?override_mime_typeurlpost_args=send?with_credentials?expecting_process_page?cookies_info~post_args?progress?upload_progress?override_mime_typeurl