iweb.ml 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  10. *
  11. * iweb.ml
  12. *
  13. * Copyright (C) The #Seppo contributors. All rights reserved.
  14. *
  15. * This program is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU General Public License as published by
  17. * the Free Software Foundation, either version 3 of the License, or
  18. * (at your option) any later version.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU General Public License
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  27. *)
  28. let ( >>= ) = Result.bind
  29. let ( let* ) = Result.bind
  30. let chain a b =
  31. let f a = Ok (a, b) in
  32. Result.bind a f
  33. let seppo = Uri.make ~userinfo:"seppo" ~host:"seppo.social" ()
  34. let to_channel ~xsl oc l =
  35. assert (xsl |> St.ends_with ~suffix:".xsl");
  36. let xsl = Some ("../themes/current/" ^ xsl)
  37. and readme = Some {|
  38. The html you see here is for compatibility with https://sebsauvage.net/wiki/doku.php?id=php:shaarli
  39. The main reason is backward compatibility for e.g. http://mro.name/ShaarliOS and
  40. https://github.com/dimtion/Shaarlier
  41. |} in
  42. Xml.to_chan ~xsl ~readme l oc
  43. module MyCookie = struct
  44. let timeout tnow =
  45. 30 * 60
  46. |> Ptime.Span.of_int_s
  47. |> Ptime.add_span tnow
  48. |> Option.get
  49. (* payload *)
  50. let encode (Auth.Uid uid, t) =
  51. Logr.debug (fun m -> m "%s.%s" "Iweb.MyCookie" "encode");
  52. let t = t |> Ptime.to_rfc3339 in
  53. Csexp.(List [ Atom uid; Atom t ] |> to_string)
  54. (* payload *)
  55. let decode c =
  56. let open Csexp in
  57. match c |> parse_string with
  58. | Ok List [ Atom uid; Atom t ] -> (
  59. match t |> Ptime.of_rfc3339 with
  60. | Error _ -> Error "expected rfc3339"
  61. | Ok (t, _, _) -> Ok (Auth.Uid uid, t))
  62. | _ -> Error "expected cookie csexp"
  63. let name = "#session"
  64. let make (req : Cgi.Request.t) v =
  65. Cookie.to_string
  66. ~domain:req.host
  67. ~http_only:true
  68. ~path:req.script_name
  69. ~same_site:`Strict
  70. ~secure:false
  71. (name, v)
  72. let new_session
  73. ?(nonce12 = Cookie.random_nonce ())
  74. ?(tnow = Ptime_clock.now ())
  75. sec32
  76. req
  77. uid =
  78. assert (Cfg.CookieSecret.l32 = (sec32 |> Cstruct.length));
  79. assert (Cookie.l12 = (nonce12 |> Cstruct.length));
  80. (uid, tnow |> timeout)
  81. |> encode
  82. |> Cstruct.of_string
  83. |> Cookie.encrypt sec32 nonce12
  84. |> make req
  85. |> Http.H.set_cookie
  86. end
  87. (**
  88. * input type'textarea' => textarea
  89. * input type'submit' => button
  90. *)
  91. let xhtmlform tit name (ips : Http.Form.input list) err (frm : Http.Form.t) : _ Xmlm.frag =
  92. let sep n = `Data ("\n" ^ String.make (2*n) ' ') in
  93. let att (n,v) = (("", n), v) in
  94. let ns_h = "http://www.w3.org/1999/xhtml" in
  95. let fofi _err init ((n,t,atts) : Http.Form.input) =
  96. let atts = atts |> List.fold_left (fun init a -> att a :: init) [] in
  97. let atts = match List.assoc_opt n frm with
  98. | None -> atts
  99. | Some s -> att ("value", s |> String.concat "") :: atts in
  100. let txt v l = `Data (l |> List.assoc_opt v |> Option.value ~default:"") in
  101. (*
  102. init
  103. @ (err |> List.fold_left (fun init (f,e) ->
  104. if String.equal f n
  105. then `El (((ns_h,"div"), [att ("class","err"); att ("data-name",n)]), [`Data e]) :: init
  106. else init) [])
  107. @ *)
  108. sep 2 ::
  109. (match t with
  110. (* type is abused to mark textarea. Here we put it right again. *)
  111. | "textarea" -> let atts' = atts |> List.remove_assoc ("","value") in
  112. `El (((ns_h,"textarea"), (("","name"),n) :: atts'), [txt ("","value") atts] )
  113. | "submit" -> `El (((ns_h,"button"), (("","name"),n) :: (("","type"),t) :: atts), [txt ("","value") atts])
  114. | _ -> `El (((ns_h,"input"), (("","name"),n) :: (("","type"),t) :: atts), []))
  115. :: init
  116. in
  117. `El (((ns_h,"html"),
  118. ((Xmlm.ns_xml,"base"),"../")
  119. :: ((Xmlm.ns_xmlns,"xmlns"), ns_h)
  120. :: []),
  121. sep 0
  122. :: `El (((ns_h,"head"),[]),
  123. sep 1 :: `El (((ns_h,"link"), [(("","rel"),"icon"); (("","type"),"image/jpg"); (("","href"),"../me-avatar.jpg")] ),[])
  124. :: sep 1 :: `El (((ns_h,"meta"), [(("","name"),"generator"); (("","content"),St.seppo_s)] ),[])
  125. :: sep 1 :: `El (((ns_h,"title"), []),[`Data tit])
  126. :: [])
  127. :: sep 0
  128. :: `El (((ns_h,"body"),[]),
  129. sep 1
  130. :: `El (((ns_h,"form"),
  131. [(("","method"),"post");
  132. (("","name"),name);
  133. (("","id"),name)] ),
  134. sep 2
  135. :: `El (((ns_h,"ul"),[(("","id"),name ^ "_validation"); (("","class"),"validation")]),
  136. (* at first display all errors with key "" *)
  137. (err |> List.fold_left (fun init (f,e) -> match (f,e) with
  138. | "",e -> sep 2 :: `El (((ns_h,"li"),[]), [ `Data e ]) :: init
  139. | _ -> init) []) )
  140. :: (ips |> List.rev |> List.fold_left (fofi err) []) )
  141. :: sep 0 :: [])
  142. :: sep 0 :: [])
  143. module Ping = struct
  144. let get ~base _uuid (r : Cgi.Request.t) =
  145. let base : Uri.t = base ()
  146. and run_delay = 60 in
  147. match r.query_string |> Uri.query_of_encoded with
  148. | []
  149. | ["",[]]
  150. | [("nudge",_)] ->
  151. Main.Queue.ping_and_forget ~base ~run_delay
  152. | [("loop",_)] -> (
  153. match Ap.PubKeyPem.(private_of_pem pk_pem) with
  154. | Ok pk ->
  155. Main.Queue.(loop ~base ~run_delay (process_new_and_due ~pk ~base))
  156. | Error e ->
  157. Logr.warn (fun m -> m "%s.%s %s" "Iweb.Ping" "get" e);
  158. Lwt.return Http.s500 )
  159. | _ -> Lwt.return Http.s400
  160. end
  161. (* combine name and value *)
  162. let n ((n,_,_) : Http.Form.input) (v : string) : Http.Form.field = (n,[v])
  163. module Login = struct
  164. let path = "/login"
  165. module F = Http.Form
  166. let i_tok : F.input = ("token", "hidden", [])
  167. let i_uid : F.input = ("login", "text", [("required","required")])
  168. let i_pwd : F.input = ("password", "password", [("required","required")])
  169. let i_lol : F.input = ("longlastingsession", "checkbox", [])
  170. let i_ret : F.input = ("returnurl", "hidden", [])
  171. let i_but : F.input = ("Login", "submit", [])
  172. let get _uuid (tok, (r : Cgi.Request.t)) =
  173. Logr.debug (fun m -> m "%s.%s" "Iweb.Login" "get");
  174. let ur = r |> Cgi.Request.path_and_query |> Uri.of_string in
  175. Ok (`OK, [Http.H.ct_xml], (fun oc ->
  176. [
  177. n i_tok tok;
  178. n i_ret ("returnurl" |> Uri.get_query_param ur |> Option.value ~default:"");
  179. n i_but "Login";
  180. ]
  181. |> xhtmlform "👋 Login" "loginform" [i_tok;i_uid;i_pwd;i_lol;i_ret;i_but] []
  182. |> to_channel ~xsl:"loginform.xsl" oc))
  183. (* check uid+pwd, Always take at least 2 seconds, if ok set session cookie and
  184. redirect to returnurl, call ban_f otherwise. *)
  185. let post _uuid tnow (ban_f : Ptime.t -> string -> unit) (_tok, (frm, (req : Cgi.Request.t))) =
  186. let sleep = 2 in
  187. Logr.debug (fun m -> m "Iweb.Login.post, sleep %d seconds..." sleep);
  188. Unix.sleep sleep;
  189. let flt r = function
  190. | (("login", [_]) as v)
  191. | (("password", [_]) as v)
  192. | (("returnurl", [_]) as v)
  193. | (("token", [_]) as v) -> r |> List.cons v
  194. | (f, _) -> Logr.info (fun m -> m "unconsumed form field: '%s'" f); r
  195. and cmp (a, _) (b, _) = String.compare a b in
  196. match frm |> List.fold_left flt [] |> List.sort cmp with
  197. | [ ("login", [uid]);
  198. ("password", [pwd]);
  199. ("returnurl", [retu]);
  200. ("token", [_] (* token has to be already checked by the caller. *)); ] ->
  201. Ok (Auth.Uid uid, pwd)
  202. >>= Auth.chk_file Auth.fn
  203. >>= (fun uid ->
  204. Cfg.CookieSecret.(make "" >>= from_file)
  205. >>= chain (Ok uid))
  206. >>= (fun (uid, sec32) ->
  207. assert (32 = (sec32 |> Cstruct.length));
  208. MyCookie.new_session ~tnow sec32 req uid |> Result.ok)
  209. |> (function
  210. | Ok cv ->
  211. Http.s302 ~header:[ cv ] retu
  212. | Error "invalid username or password" ->
  213. ban_f tnow req.remote_addr;
  214. Http.s403
  215. | Error e ->
  216. Logr.err (fun m -> m "%s %s.%s: %s" E.e1021 "Login" "post" e);
  217. Http.s500)
  218. | _ ->
  219. Http.s401
  220. end
  221. module Logout = struct
  222. let path = "/logout"
  223. (* GET requests should be idempotent, have no side effects.
  224. TODO: We could use a form button for this and POST: https://stackoverflow.com/a/33880971/349514*)
  225. let get _uuid ((_ : Auth.uid option), req) =
  226. Http.s302 ~header:[ ("Set-Cookie", MyCookie.make req "") ] ".."
  227. end
  228. let check_token f_ok exp ((v : Http.Form.t), vv) =
  229. Logr.debug (fun m -> m "Iweb.check_token");
  230. match Uri.get_query_param (Uri.make ~query:v ()) "token" with
  231. | Some tok ->
  232. if String.equal exp tok
  233. then (f_ok ();
  234. Ok (tok, (v,vv)))
  235. else Http.s403
  236. | None ->
  237. Logr.warn (fun m -> m "check_token: no token in form: %s" (Uri.encoded_of_query v));
  238. Http.s400
  239. (** get uid from session if still running *)
  240. let ases tnow (r : Cgi.Request.t) =
  241. (* Logr.debug (fun m -> m "%s.%s" "Iweb" "ases"); *)
  242. let uid = function
  243. (* check if the session cookie carries a date in the future *)
  244. | ("#session" as n, pay) :: [] ->
  245. assert (n = MyCookie.name);
  246. let sec = Cfg.CookieSecret.(make "" >>= from_file) |> Result.get_ok in
  247. Option.bind
  248. (Cookie.decrypt sec pay)
  249. (fun c ->
  250. Logr.debug (fun m -> m "%s.%s cookie value '%s'" "Iweb" "ases" c);
  251. match c |> MyCookie.decode with
  252. Ok (uid, tend) ->
  253. if tend > tnow
  254. then Some uid
  255. else None
  256. | _ -> None)
  257. | _ ->
  258. Logr.debug (fun m -> m "%s.%s %s cookie not found." "Iweb" "ases" MyCookie.name);
  259. None
  260. in
  261. Ok (r.http_cookie |> Cookie.of_string |> uid, r)
  262. let rz = Webfinger.Server.rule
  263. :: Webfinger.rule
  264. :: Ap.Person.rule
  265. :: Ap.PersonX.rule
  266. :: Ap.PubKeyPem.pk_rule
  267. :: Ap.PubKeyPem.rule
  268. :: Cfg.Profile.ava
  269. :: Cfg.Profile.ban
  270. :: []
  271. module Passwd = struct
  272. let path = "/passwd"
  273. module F = Http.Form
  274. let i_tok : F.input = ("token", "hidden", [])
  275. let i_uid : F.input = ("setlogin", "text", [
  276. ("required","required");
  277. ("maxlength","50");
  278. ("minlength","1");
  279. ("pattern", {|^[a-zA-Z0-9_.-]+$|});
  280. ("placeholder","Your local name as 'alice' in @alice@example.com");
  281. ])
  282. let i_pwd : F.input = ("setpassword", "password", [
  283. ("required","required");
  284. ("maxlength","200");
  285. ("minlength","12");
  286. ("pattern", {|^\S([^\n\t]*\S)?$|});
  287. ("placeholder","good passwords: xkcd.com/936");
  288. ])
  289. let i_pw2 : F.input = ("confirmpassword", "password", [
  290. ("required","required");
  291. ("placeholder","the same once more");
  292. ])
  293. let i_but : F.input = ("Save", "submit", [])
  294. let get _uuid (token, (Auth.Uid uid, _req)) =
  295. let _need_uid = Auth.(is_setup fn) in
  296. Ok (`OK, [Http.H.ct_xml], (fun oc ->
  297. [
  298. n i_tok token;
  299. n i_uid uid;
  300. n i_but "Save config";
  301. ]
  302. |> xhtmlform "🌻 Change Password" "changepasswordform" [i_tok;i_uid;i_pwd;i_pw2;i_but] []
  303. |> to_channel ~xsl:"changepasswordform.xsl" oc))
  304. let post _uuid _ (_tok, (frm, (Auth.Uid _, (req : Cgi.Request.t)))) =
  305. let _boo = File.exists Auth.fn in
  306. Logr.debug (fun m -> m "Iweb.Passwd.post form name='%s'" "changepasswordform");
  307. assert (Http.Mime.app_form_url = req.content_type);
  308. let run() =
  309. (* funnel additional err messages into the form *)
  310. let err msg (name,_,_) pred = if pred
  311. then Ok ()
  312. else Error (name,msg) in
  313. let* uid = F.string i_uid frm in
  314. let* pwd = F.string i_pwd frm in
  315. let* pw2 = F.string i_pw2 frm in
  316. let* _ = String.equal pwd pw2 |> err "not identical to password" i_pw2 in
  317. Ok (Auth.Uid uid,pwd)
  318. in
  319. match run() with
  320. | Ok (uid,pwd) ->
  321. let* _ = Auth.((uid, pwd) |> to_file fn) in
  322. let* _ = req |> Cgi.Request.base |> Cfg.Base.(to_file fn) |> Result.map_error (Http.err500 "failed to save baseurl") in
  323. let* _ = Webfinger.Server.(Make.make rz rule.target) |> Result.map_error (Http.err500 "failed to update webfinger") in
  324. let* _ = Ap.PersonX.(Make.make rz rule.target) |> Result.map_error (Http.err500 "failed to update profile") in
  325. let* sec = Cfg.CookieSecret.(make "" >>= from_file) |> Result.map_error (Http.err500 "failed to read cookie secret") in
  326. let header = [ MyCookie.new_session sec req (uid) ] in
  327. if File.exists Storage.fn
  328. then
  329. (Logr.debug (fun m -> m "already exists: %s" Storage.fn);
  330. Http.s302 ~header "../")
  331. else (
  332. Logr.debug (fun m -> m "add the first post from welcome.en.txt");
  333. let Auth.Uid uid = uid in
  334. let* base = Cfg.Base.(fn |> from_file) |> Result.map_error (Http.err500 "failed to load baseurl") in
  335. let* profile = Cfg.Profile.(fn |> from_file) |> Result.map_error (fun e ->
  336. Logr.err (fun m -> m "%s.%s failed to load profile: %s" "Iweb.Profile" "post" e);
  337. Http.s500') in
  338. let author = Uri.make ~userinfo:uid ~host:(Uri.host base |> Option.value ~default:"example.com") ()
  339. and lang = Rfc4287.Rfc4646 "en"
  340. and msg = Res.read "/welcome.en.txt" |> Option.value ~default:"Ouch, missing welcome."
  341. and published = Rfc3339.T ( Ptime_clock.now() |> Ptime.to_rfc3339 )
  342. and uri = Uri.with_userinfo seppo None
  343. in match
  344. msg
  345. |> Rfc4287.Entry.from_text_plain ~published ~author ~lang ~uri "Hello, #Seppo!"
  346. >>= Main.sift_urls
  347. >>= Main.sift_tags Tag.cdb
  348. >>= Main.sift_handles
  349. >>= Main.Note.publish ~base ~profile ~author:author
  350. with
  351. | Ok _ -> Http.s302 "../"
  352. | Error _ -> Http.s500 )
  353. | Error ee ->
  354. Logr.err (fun m -> m "%s %s.%s" E.e1022 "Iweb.Passwd" "post");
  355. Ok (`Unprocessable_entity, [Http.H.ct_xml], (fun oc ->
  356. frm
  357. |> xhtmlform "🌻 Change Password" "changepasswordform" [i_tok;i_uid;i_pwd;i_pw2;i_but] [ee]
  358. |> to_channel ~xsl:"changepasswordform.xsl" oc))
  359. end
  360. (** if no uid then redirect to login/passwd page *)
  361. let uid_redir x : ((Auth.uid * Cgi.Request.t), Cgi.Response.t) result =
  362. match x with
  363. | (Some uid, r) -> Ok (uid, r)
  364. | (None, (r : Cgi.Request.t)) ->
  365. let r302 p =
  366. let path = r.script_name ^ p in
  367. let query = [("returnurl",[r |> Cgi.Request.abs])] in
  368. Uri.make ~path ~query () |> Uri.to_string |> Http.s302
  369. in
  370. if Auth.(is_setup fn)
  371. then r302 Login.path
  372. else if Passwd.path = r.path_info
  373. then (
  374. Logr.info (fun m -> m "passwd are not set, so go on with an empty uid. %s" r.path_info);
  375. Ok (Auth.dummy, r))
  376. else r302 Passwd.path
  377. module Actor = struct
  378. let path = "/activitypub/actor.xml"
  379. let get ~base uuid (token, (Auth.Uid _uid, (r : Cgi.Request.t))) =
  380. match Ap.PubKeyPem.(private_of_pem pk_pem) with
  381. | Error e ->
  382. Logr.warn (fun m -> m "%s.%s %s" "Iweb.Actor" "get" e);
  383. Lwt.return Http.s500
  384. | Ok pk ->
  385. let query = r.query_string |> Uri.query_of_encoded in
  386. let u = Uri.make ~query () in
  387. Logr.debug (fun m -> m "%s.%s %a %a" "Iweb.Actor" "get" Uuidm.pp uuid Uri.pp_hum u);
  388. match Uri.get_query_param u "id" with
  389. | None -> (* static, public profile of myself *)
  390. Http.s302 ("../../" ^ Ap.prox) |> Lwt.return
  391. | Some u -> (* dynamic, uncached remote actor profile converted to rdf *)
  392. let date = Ptime_clock.now () in
  393. let base = base () in
  394. let key_id = Uri.make ~path:Ap.proj () |> Http.reso ~base |> Ap.Person.key_id in
  395. let key : Http.t_sign_k option = Some (key_id,Ap.PubKeyPem.sign pk,date) in
  396. let%lwt act = u
  397. |> Uri.of_string
  398. |> Ap.Actor.http_get ~key in
  399. match act with
  400. | Error s ->
  401. Logr.warn (fun m -> m "%s.%s %a %s" "Actor" "get" Uuidm.pp uuid s);
  402. Http.s502 |> Lwt.return
  403. | Ok p ->
  404. let toc ?(indent = None) oc doc =
  405. (* similar St.to_chan *)
  406. let o = Xmlm.make_output ~decl:false (`Channel oc) ~nl:true ~indent in
  407. let id x = x in
  408. Xmlm.output_doc_tree id o (None, doc)
  409. in
  410. Ok (`OK, [Http.H.ct_xml], (fun oc ->
  411. Xml.pi oc "xml" ["version","1.0"];
  412. Xml.pi oc "xml-stylesheet" ["type","text/xsl"; "href","../../themes/current/" ^ "actor.xsl"];
  413. p
  414. |> Ap.Person.flatten
  415. |> Ap.Person.Rdf.encode
  416. ~token:(Some token)
  417. ~notify:(Ap.Followers.notify p.id)
  418. ~subscribed:(Ap.Following.subscribed p.id)
  419. ~blocked:(Ap.Following.blocked p.id)
  420. ~base
  421. ~context:None
  422. |> toc oc))
  423. |> Lwt.return
  424. let post
  425. ~(base : unit -> Uri.t)
  426. ?(que = Job.qn)
  427. ?(subscribed = Ap.Following.cdb)
  428. uuid tnow (_tok, ((frm : Http.Form.t), (Auth.Uid _uid, (req : Cgi.Request.t)))) =
  429. let dst_inbox = frm |> List.assoc "inbox" |> String.concat "|" |> Uri.of_string in
  430. let todo_id = frm |> List.assoc "id" |> String.concat "|" |> Uri.of_string in
  431. Logr.debug (fun m -> m "%s.%s %a data %a" "Iweb.Actor" "post" Uuidm.pp uuid Uri.pp_hum dst_inbox);
  432. let base = base () in
  433. let me = Uri.make ~path:Ap.proj () |> Http.reso ~base in
  434. let form_toggle_foldr k_of_old f_switch form init (k_old,v_old) =
  435. match k_old |> k_of_old with
  436. | None -> init
  437. | Some k ->
  438. let v = match form |> List.assoc_opt k with
  439. | None
  440. | Some ["no"] -> "no"
  441. | _ -> "on" in
  442. let v_old = match v_old with
  443. | ["no"] -> "no"
  444. | _ -> "on" in
  445. match f_switch (k, v_old, v) with
  446. | None -> init
  447. | Some x -> x :: init in
  448. let _ = frm |> List.fold_left
  449. (form_toggle_foldr
  450. (St.after ~prefix:"~")
  451. (function
  452. | "block",_,_ -> Some ()
  453. | "notify",_,_ -> Some ()
  454. | "subscribed","no","on" ->
  455. Logr.debug (fun m -> m "%s.%s send subscribed %a to %a" "Iweb.Actor" "post" Uri.pp todo_id Uri.pp dst_inbox);
  456. let fo = todo_id |> Ap.Following.follow ~me ~inbox:dst_inbox in
  457. let cs = fo
  458. |> As2_vocab.Encode.follow ~base
  459. |> Main.job_encode_notify fo.id (dst_inbox, fo.object_) in
  460. let _ = cs
  461. |> Csexp.to_string
  462. |> Bytes.of_string
  463. |> Job.enqueue ~due:tnow que 0 in
  464. let ke = fo.object_ |> Uri.to_string in
  465. let v = Ap.Followers.State.((Pending,tnow,dst_inbox,None,None,None) |> encode) |> Csexp.to_string in
  466. let _ = Mapcdb.update_string ke v subscribed in
  467. let _ = Ap.Following.Json.(Make.make [rule] target) in
  468. let _ = Ap.Following.Atom.(Make.make [rule] target) in
  469. Some ()
  470. | "subscribed","on","no" ->
  471. Logr.debug (fun m -> m "%s.%s send unsubscribe %a to %a" "Iweb.Actor" "post" Uri.pp todo_id Uri.pp dst_inbox);
  472. let ufo = todo_id |> Ap.Following.follow ~me ~inbox:dst_inbox |> Ap.Following.undo ~me in
  473. let j = ufo |> As2_vocab.Encode.(undo ~context:None (follow ~context:None ~base) ~base) in
  474. let _ = j |> Main.job_encode_notify ufo.id (dst_inbox, ufo.obj.object_)
  475. |> Csexp.to_string
  476. |> Bytes.of_string
  477. |> Job.enqueue ~due:tnow que 0 in
  478. let ke = ufo.obj.object_ |> Uri.to_string in
  479. let _ = Mapcdb.remove_string ke subscribed in
  480. let _ = Ap.Following.Json.(Make.make [rule] target) in
  481. let _ = Ap.Following.Atom.(Make.make [rule] target) in
  482. Some ()
  483. | k,v',v ->
  484. Logr.warn (fun m -> m "%s.%s unhandled %s: '%s' -> '%s'" "Iweb.Actor" "post" k v' v);
  485. None )
  486. frm)
  487. [] in
  488. let loc = req |> Cgi.Request.abs |> Uri.of_string in
  489. let loc = Uri.add_query_param' loc ("id", (todo_id |> Uri.to_string)) in
  490. Logr.debug (fun m -> m "%s.%s %a 302 back to %a" "Iweb.Actor" "post" Uuidm.pp uuid Uri.pp loc);
  491. let%lwt _ = Main.Queue.ping_and_forget ~base ~run_delay:60 in
  492. Lwt.return (Http.s302 (loc |> Uri.to_string))
  493. module Icon = struct
  494. (* forward to the avatar image of the id with explicit cache duration set by .htaccess/webserver config *)
  495. let get ~base uuid (r : Cgi.Request.t) =
  496. Logr.debug (fun m -> m "%s.%s" "Iweb.Actor.Icon" "get");
  497. let query = r.query_string |> Uri.query_of_encoded in
  498. match query with
  499. | ["id",[u]] ->
  500. (match Ap.PubKeyPem.(private_of_pem pk_pem) with
  501. | Error e ->
  502. Logr.warn (fun m -> m "%s.%s %s" "Iweb.Actor.Icon" "get" e);
  503. Lwt.return Http.s500
  504. | Ok pk ->
  505. let date = Ptime_clock.now () in
  506. let base = base () in
  507. let key_id = Uri.make ~path:Ap.proj () |> Http.reso ~base |> Ap.Person.key_id in
  508. let key : Http.t_sign_k option = Some (key_id,Ap.PubKeyPem.sign pk,date) in
  509. let%lwt act =
  510. u
  511. |> Uri.of_string
  512. |> Ap.Actor.http_get ~key in
  513. (match act with
  514. | Error s ->
  515. Logr.warn (fun m -> m "%s.%s %a %s" "Iweb.Avatar" "get" Uuidm.pp uuid s);
  516. Http.s502
  517. | Ok p ->
  518. p.icon
  519. |> Option.value ~default:Uri.empty
  520. |> Uri.to_string
  521. |> Http.s302)
  522. |> Lwt.return)
  523. | _ -> Http.s404
  524. |> Lwt.return
  525. end
  526. end
  527. module Health = struct
  528. let path = "/actor"
  529. let get ~base _uuid (Auth.Uid uid, (_r : Cgi.Request.t)) =
  530. let base = base () in
  531. let to_rdf ?(tz = 0) me (pem_url,x509) (cur,err,new_,run,tmp,wait) (ci_cur,ci_new) lock : _ Xmlm.frag =
  532. let _ = tz in
  533. let open Xml in
  534. let Webfinger.Client.(Localpart lopa,Domainpart dopa) = me in
  535. let sep n = `Data ("\n" ^ String.make (2*n) ' ') in
  536. let txt ?(datatype = None) (ns,tn) (s : string) =
  537. `El (((ns, tn), match datatype with
  538. | Some ty -> [((ns_rdf, "datatype"), ty)]
  539. | None -> []), [`Data s]) in
  540. let intg (ns,tn) (v : int) =
  541. `El (((ns, tn),
  542. [((ns_rdf, "datatype"), ns_xsd ^ "integer")]),
  543. [`Data (v |> Int.to_string)]) in
  544. let dati ?(tz_offset_s = 0) (ns,tn) (v : Ptime.t option) =
  545. `El (((ns, tn),
  546. [((ns_rdf, "datatype"), ns_xsd ^ "dateTime")]),
  547. [`Data (match v with
  548. | None -> "-"
  549. | Some v -> v |> Ptime.to_rfc3339 ~tz_offset_s)]) in
  550. `El (((ns_rdf, "RDF"),
  551. [
  552. ((Xmlm.ns_xmlns, "rdf"), ns_rdf);
  553. ((Xmlm.ns_xmlns, "seppo"), ns_seppo);
  554. ((Xmlm.ns_xmlns, "b"), ns_backoffice);
  555. ]),
  556. [sep 0;
  557. `El (((ns_rdf, "Description"),
  558. [((ns_rdf,"about"),"")]), [
  559. sep 1; txt (ns_backoffice, "domainpart") dopa;
  560. sep 1; txt (ns_backoffice, "localpart") lopa;
  561. sep 1; txt (ns_backoffice, "rfc7033") (me |> Webfinger.Client.to_string);
  562. sep 1; txt (ns_backoffice, "x509_pem_url") (pem_url |> Uri.to_string);
  563. sep 1; txt (ns_backoffice, "x509_fingerprint") (x509 |> X509.Public_key.fingerprint |> Cstruct.to_hex_string);
  564. sep 1; txt (ns_backoffice, "x509_id") (x509 |> X509.Public_key.id |> Cstruct.to_hex_string);
  565. sep 1; dati (ns_backoffice, "q_lock") lock;
  566. sep 1; intg (ns_backoffice, "spool_job_cur") cur;
  567. sep 1; intg (ns_backoffice, "spool_job_err") err;
  568. sep 1; intg (ns_backoffice, "spool_job_new") new_;
  569. sep 1; intg (ns_backoffice, "spool_job_run") run;
  570. sep 1; intg (ns_backoffice, "spool_job_tmp") tmp;
  571. sep 1; intg (ns_backoffice, "spool_job_wait") wait;
  572. sep 1; intg (ns_backoffice, "cache_inbox_new") ci_new;
  573. sep 1; intg (ns_backoffice, "cache_inbox_cur") ci_cur;
  574. ] )]) in
  575. let pat = Str.regexp {|.+\.\(s\|json\)$|} in
  576. let count dn =
  577. let pred f = Str.string_match pat f 0 in
  578. dn |> File.count_dir ~pred in
  579. let spool_job = (
  580. "app/var/spool/job/cur/" |> count,
  581. "app/var/spool/job/err/" |> count,
  582. "app/var/spool/job/new/" |> count,
  583. "app/var/spool/job/run/" |> count,
  584. "app/var/spool/job/tmp/" |> count,
  585. "app/var/spool/job/wait/"|> count
  586. )
  587. and cache_inbox = (
  588. "app/var/cache/inbox/cur/" |> count,
  589. "app/var/cache/inbox/new/" |> count
  590. )
  591. and qt = try
  592. (Main.Queue.run_fn
  593. |> Unix.stat).st_mtime
  594. |> Ptime.of_float_s
  595. with | _ -> None
  596. and x509 = Ap.PubKeyPem.target
  597. |> File.to_string
  598. |> Ap.PubKeyPem.of_pem
  599. |> Result.get_ok
  600. and me = Webfinger.Client.(Localpart uid,Domainpart (Uri.host base |> Option.value ~default:"")) in
  601. let x = to_rdf me (Ap.PubKeyPem.target |> Uri.of_string |> Http.reso ~base,x509) spool_job cache_inbox qt in
  602. let xsl = "backoffice.xsl" in
  603. let xsl = Some ("../../themes/current/" ^ xsl) in
  604. Ok (`OK, [Http.H.ct_xml], Xml.to_chan ~xsl x)
  605. end
  606. module Http_ = struct
  607. let path = "/http"
  608. let get ~base uuid now (Auth.Uid _, (r : Cgi.Request.t)) =
  609. let query = r.query_string |> Uri.query_of_encoded in
  610. let u = Uri.make ~query () in
  611. Logr.debug (fun m -> m "%s.%s %a %a" "Iweb.Http_" "get" Uuidm.pp uuid Uri.pp_hum u);
  612. match Uri.get_query_param u "get" with
  613. | None -> Http.s400 |> Lwt.return
  614. | Some u ->
  615. let base = base () in
  616. let me = Uri.make ~path:Ap.proj () |> Http.reso ~base in
  617. let keyid = me |> Ap.Person.key_id in
  618. let (let*%) = Http.(let*%) in
  619. let*% pk = Ap.PubKeyPem.(private_of_pem pk_pem) |> Result.map_error (fun s ->
  620. Logr.err (fun m -> m "%s %s.%s %s" E.e1009 "Iweb.Http_" "get" s);
  621. Http.s500') in
  622. Logr.debug (fun m -> m "%s.%s got keyid %a" "Iweb.Http_" "get" Uri.pp_hum keyid);
  623. let key : Http.t_sign_k option = Some (keyid,Ap.PubKeyPem.sign pk,now) in
  624. let headers = [ Http.H.acc_app_jlda ] |> Cohttp.Header.of_list in
  625. let%lwt p = u
  626. |> Uri.of_string
  627. |> Http.get ~key ~headers in
  628. match p with
  629. | Error e ->
  630. Logr.warn (fun m -> m "%s.%s %s" "Iweb.Http_" "get" e);
  631. Http.s422 |> Lwt.return
  632. | Ok (r,b) ->
  633. match r.status with
  634. | #Cohttp.Code.success_status ->
  635. let%lwt b = b |> Cohttp_lwt.Body.to_string in
  636. let ct = ("Content-Type", Cohttp.Header.get r.headers "content-type"
  637. |> Option.value ~default:Http.Mime.text_plain) in
  638. Ok (`OK, [ct], fun oc -> b |> output_string oc)
  639. |> Lwt.return
  640. | s ->
  641. let s = s |> Cohttp.Code.string_of_status in
  642. Logr.warn (fun m -> m "%s.%s %s" "Iweb.Http_" "get" s);
  643. Http.s400 |> Lwt.return
  644. end
  645. module Note = struct
  646. let path = "/note"
  647. (*
  648. curl -L https://example.com/seppo.cgi/note?id=https://digitalcourage.social/users/mro/statuses/111601127682690078
  649. *)
  650. let get uuid (token, (Auth.Uid _uid, (r : Cgi.Request.t))) : Cgi.Response.t' =
  651. let que = Ap.Note.ibc_dir in
  652. let query = r.query_string |> Uri.query_of_encoded in
  653. let u = Uri.make ~query () in
  654. Logr.debug (fun m -> m "%s.%s %a data %a" "Iweb.Note" "get" Uuidm.pp uuid Uri.pp_hum u);
  655. let _ = token in
  656. match Option.bind
  657. ("h" |> Uri.get_query_param u)
  658. (fun h ->
  659. Logr.debug (fun m -> m "%s.%s %s" "Iweb.Note" "get" h);
  660. try
  661. let h = Scanf.sscanf h "%[a-zA-Z0-9_-]" (fun a -> a) in
  662. Ok (`OK, [Http.H.ct_jlda], fun oc ->
  663. Printf.sprintf "%s%snote-%s.json" que "new/" h
  664. |> File.to_string
  665. |> output_string oc)
  666. |> Option.some
  667. with _ -> None) with
  668. | Some v -> v
  669. | None ->
  670. match Option.bind
  671. ("id" |> Uri.get_query_param u)
  672. (fun id ->
  673. let h = id
  674. |> Uri.of_string
  675. |> Ap.Note.uhash in
  676. let u = Uri.remove_query_param u "id" in
  677. Uri.add_query_param u ("h",[h])
  678. |> Uri.to_string
  679. |> Http.s302
  680. |> Option.some ) with
  681. | None -> Http.s404
  682. | Some v -> v
  683. end
  684. module Profile = struct
  685. let path = "/profile"
  686. module F = Http.Form
  687. let i_tok : F.input = ("token", "hidden", [])
  688. let i_tit : F.input = ("title", "text", [ ("required","required"); ("minlength","1"); ("maxlength","100"); ("placeholder","A one-liner describing this #Seppo!"); ])
  689. let i_bio : F.input = ("bio", "textarea", [ ("maxlength","2000"); ("rows","10"); ("placeholder","more text describing this #Seppo!"); ])
  690. let i_tzo : F.input = ("timezone", "text", [ ("required","required"); ("minlength","3"); ("maxlength","100"); ("placeholder","Europe/Amsterdam or what timezone do you usually write from"); ])
  691. let i_lng : F.input = ("language", "text", [ ("required","required"); ("minlength","2"); ("maxlength","2"); ("pattern", {|^[a-z]+$|}); ("placeholder","nl or what language do you usually write in"); ])
  692. let i_ppp : F.input = ("posts_per_page", "number", [ ("required","required"); ("min","10"); ("max","1000"); ("placeholder","50 or how many posts should go on one page"); ])
  693. let i_but : F.input = ("save", "submit", [])
  694. let get _uuid (token, (_uid, _req)) : Cgi.Response.t' =
  695. let p = Cfg.Profile.(load fn) in
  696. let rz = Cfg.Profile.ava
  697. :: Cfg.Profile.ban
  698. :: [] in
  699. let _ = rz |> List.fold_left (fun _ (r : Make.t) -> Make.make rz r.target) (Ok "") in
  700. Ok (`OK, [Http.H.ct_xml], (fun oc ->
  701. let Rfc4287.Rfc4646 lng = p.language in
  702. [
  703. n i_tok token;
  704. n i_tit p.title;
  705. n i_bio p.bio;
  706. n i_lng lng;
  707. n i_tzo (Timedesc.Time_zone.name p.timezone);
  708. n i_ppp (string_of_int p.posts_per_page);
  709. n i_but "Save";
  710. ]
  711. |> xhtmlform "🎭 Profile" "configform" [i_tok;i_tit;i_bio;i_lng;i_tzo;i_ppp;i_but] []
  712. |> to_channel ~xsl:"configform.xsl" oc))
  713. let post _uuid _tnow (_tok, (frm, (Auth.Uid uid, (_req : Cgi.Request.t)))) =
  714. let run () =
  715. Logr.debug (fun m -> m "%s.%s save" "Iweb.Profile" "post");
  716. let* title = F.string i_tit frm in
  717. let* bio = frm |> F.string i_bio in
  718. let* language= F.string i_lng frm in
  719. let language = Rfc4287.Rfc4646 language in
  720. let* timezone= F.string i_tzo frm in
  721. let timezone = Timedesc.Time_zone.(make timezone
  722. |> Option.value ~default:Rfc3339.fallback) in
  723. let* ppp = F.string i_ppp frm in
  724. let posts_per_page = ppp
  725. |> int_of_string_opt
  726. |> Option.value ~default:50 in
  727. let p : Cfg.Profile.t = {title;bio;language;timezone;posts_per_page} in
  728. let eee e = ("",e) in
  729. let* _ = Result.map_error eee Cfg.Profile.(p |> to_file fn) in
  730. let* _ = Result.map_error eee Ap.Person.(Make.make rz rule.target) in
  731. let* _ = Result.map_error eee Ap.PersonX.(Make.make rz rule.target) in
  732. let* ba = Result.map_error eee Cfg.Base.(from_file fn) in
  733. Ok (p,ba) in
  734. match run() with
  735. | Ok (profile,base) ->
  736. if File.exists Storage.fn
  737. then
  738. (Logr.debug (fun m -> m "already exists: %s" Storage.fn);
  739. Http.s302 "../")
  740. else (
  741. Logr.debug (fun m -> m "add the first post from welcome.en.txt");
  742. let author = Uri.make ~userinfo:uid ~host:(Uri.host base |> Option.value ~default:"example.com") ()
  743. and lang = Rfc4287.Rfc4646 "en"
  744. and msg = Res.read "/welcome.en.txt" |> Option.value ~default:"Ouch, missing welcome."
  745. and published = Ptime_clock.now() |> Rfc3339.of_ptime
  746. and uri = Uri.with_userinfo seppo None
  747. in match
  748. msg
  749. |> Rfc4287.Entry.from_text_plain ~published ~author ~lang ~uri "Hello, #Seppo!"
  750. >>= Main.sift_urls
  751. >>= Main.sift_tags Tag.cdb
  752. >>= Main.sift_handles
  753. >>= Main.Note.publish ~base ~profile ~author:author
  754. with
  755. | Ok _ -> Http.s302 "../"
  756. | Error _ -> Http.s500 )
  757. | Error ("",e) ->
  758. Logr.err (fun m -> m "%s %s.%s %s" E.e1024 "Iweb.Profile" "post" e);
  759. Http.s500
  760. | Error (_f,e) ->
  761. Logr.err (fun m -> m "%s %s.%s %s" E.e1025 "Iweb.Profile" "post" e);
  762. Ok (`Unprocessable_entity, [Http.H.ct_xml], (fun oc ->
  763. frm
  764. |> xhtmlform "🎭 Profile" "configform" [i_tok;i_tit;i_bio;i_lng;i_tzo;i_ppp;i_but] []
  765. |> to_channel ~xsl:"configform.xsl" oc))
  766. end
  767. module Post = struct
  768. let path = "/post"
  769. module F = Http.Form
  770. let epoch_shaarli = ((2011,9,13),((15,45,42),2*60*60))
  771. |> Ptime.of_date_time
  772. |> Option.value ~default:Ptime.min
  773. let s2d ?(tz_offset_s = 0) s =
  774. Scanf.sscanf
  775. s
  776. "%4i%2i%2i_%2i%2i%2i"
  777. (fun y m d ho mi se -> ((y,m,d),((ho,mi,se),tz_offset_s)) |> Ptime.of_date_time)
  778. let d2s ?(tz_offset_s = 0) d =
  779. let ((y,m,d),((ho,mi,se),_)) = Ptime.to_date_time ~tz_offset_s d in
  780. Printf.sprintf "%04i%02i%02i_%02i%02i%02i" y m d ho mi se
  781. type cmd = Cancel | Delete | Save
  782. type t = {
  783. scrape : bool;
  784. source : string option;
  785. dat : Ptime.t option;
  786. url : Uri.t option;
  787. tit : string option;
  788. dsc : string option;
  789. tag : string list;
  790. pri : bool;
  791. sav : cmd option;
  792. can : string option;
  793. tok : string;
  794. ret : Uri.t option;
  795. img : Uri.t option;
  796. }
  797. let empty = {
  798. scrape = false;
  799. source = None;
  800. dat = None;
  801. url = None;
  802. tit = None;
  803. dsc = None;
  804. tag = [];
  805. pri = false;
  806. sav = None;
  807. can = None;
  808. tok = "";
  809. ret = None;
  810. img = None;
  811. }
  812. let to_rfc4287
  813. ?(now = Ptime_clock.now ())
  814. ?(lang = Rfc4287.Rfc4646 "nl")
  815. ?(author = Uri.empty)
  816. ?(tz = Rfc3339.fallback)
  817. r =
  818. let dat = r.dat |> Option.value ~default:now |> Rfc3339.of_ptime ~tz in
  819. let lks = match r.url with
  820. | None -> []
  821. | Some l ->
  822. assert (l |> Uri.host|> Option.is_some);
  823. [ Rfc4287.Link.make l ] in
  824. let os = Option.value ~default:"" in
  825. Ok ({
  826. id = Uri.empty;
  827. (* assumes an antry has one language for title, tags, content. *)
  828. in_reply_to = [];
  829. lang;
  830. author;
  831. title = r.tit |> os;
  832. published = dat;
  833. updated = dat;
  834. links = lks;
  835. categories = r.tag |> List.fold_left ( fun i s ->
  836. let l = Rfc4287.Category.Label (Rfc4287.Single s) in
  837. let t = Rfc4287.Category.Term (Rfc4287.Single s) in
  838. (l,t,Uri.empty) :: i) [];
  839. content = r.dsc |> os;
  840. } : Rfc4287.Entry.t)
  841. let of_rfc4287
  842. tpl (e : Rfc4287.Entry.t) : t =
  843. let tit = Some e.title in
  844. let date t0 t =
  845. let Rfc3339.T t = t in
  846. match Ptime.of_rfc3339 t with
  847. | Error _ -> t0
  848. | Ok (t,_tz,_c) -> Some t
  849. in
  850. let dat = date tpl.dat e.published in
  851. let url = List.fold_left (fun init (u : Rfc4287.Link.t) ->
  852. match init with
  853. | Some _ as v -> v (* found the link, just pass it *)
  854. | None ->
  855. match u.rel with
  856. | None -> Some u.href
  857. | _ -> None) None e.links in
  858. let dsc = Some e.content in
  859. (* TODO: ensure no tags get lost *)
  860. {tpl with dat;url;tit;dsc}
  861. let sift_bookmarklet_get ?(tz = "Europe/Amsterdam") i (k,v) =
  862. let _ = tz in
  863. let v = v |> String.concat " " in
  864. let os v = let v = v |> String.trim in if v = "" then None else Some v
  865. and ou v = if "" = v then None else Some (v |> Uri.of_string) in
  866. match k,v with
  867. | "post", v -> (
  868. let u = v |> Uri.of_string in
  869. match u |> Uri.scheme with
  870. | None -> {i with tit = Some v}
  871. | Some _ -> {i with url = Some u})
  872. | "source", v -> {i with source = os v}
  873. | "scrape", v -> {i with scrape = v != "no"}
  874. | "title", v -> {i with tit = os v}
  875. | "tags", v -> {i with tag = v |> String.split_on_char ' '}
  876. | "image", v -> {i with img = ou v}
  877. | "description", v -> {i with dsc = os v}
  878. | _ -> i
  879. let sift_post ?(tz = "Europe/Amsterdam") i (k,v) =
  880. let _ = tz in
  881. let v = v |> String.concat " " in
  882. let os v = let v = v |> String.trim in if v = "" then None else Some v
  883. and ou v = if "" = v then None else Some (v |> Uri.of_string) in
  884. let oau v = let u = ou v in
  885. Option.bind u
  886. (fun u' -> Option.bind (u' |> Uri.scheme)
  887. (fun _ -> u) )
  888. in
  889. match k,v with
  890. | "lf_linkdate" , v -> {i with dat = v |> s2d }
  891. | "token" , v -> {i with tok = v}
  892. | "returnurl" , v -> {i with ret = ou v}
  893. | "lf_image" , v -> {i with img = oau v}
  894. | "lf_url" , v -> {i with url = oau v}
  895. | "lf_title" , v -> {i with tit = os v}
  896. | "lf_description", v -> {i with dsc = os v}
  897. | "cancel_edit" , ("Cancel") -> {i with sav = Some Cancel}
  898. | "delete_edit" , ("Delete") -> {i with sav = Some Delete}
  899. | "save_edit" , ("Save") -> {i with sav = Some Save}
  900. | k , v -> Logr.warn (fun m -> m "%s.%s %s: %s" "Iweb.Post" "sift_post" k v);
  901. i
  902. let i_dat : F.input = ("lf_linkdate", "hidden", [])
  903. let i_url : F.input = ("lf_url", "url", [])
  904. let i_tit : F.input = ("lf_title", "text", [("required","required"); ("minlength","1")])
  905. let i_dsc : F.input = ("lf_description", "textarea", [])
  906. let i_tag : F.input = ("lf_tags", "text", [("data-multiple","data-multiple")])
  907. let i_pri : F.input = ("lf_private", "checkbox", [])
  908. let i_sav : F.input = ("save_edit", "submit", [])
  909. let i_can : F.input = ("cancel_edit", "submit", [])
  910. let i_tok : F.input = ("token", "hidden", [])
  911. let i_ret : F.input = ("returnurl", "hidden", [])
  912. let i_img : F.input = ("lf_image", "hidden", [])
  913. (* only parameter is 'post'
  914. * https://code.mro.name/github/Shaarli-Vanilla/src/master/index.php#L427
  915. * https://code.mro.name/github/Shaarli-Vanilla/src/029f75f180f79cd581786baf1b37e810da1adfc3/index.php#L1548
  916. *)
  917. let get ~base uuid (_token, (_uid, (req : Cgi.Request.t))) =
  918. Logr.debug (fun m -> m "%s.%s %a %s" "Iweb.Post" "get" Uuidm.pp uuid req.query_string);
  919. let r = req.query_string |> Uri.query_of_encoded in
  920. let emp = {empty with dat = Some (Ptime_clock.now())} in
  921. let emp =
  922. match List.assoc_opt "id" r with
  923. | Some [id] ->
  924. let base = base () in
  925. let id = id
  926. (* revert substitution by posts.xsl *)
  927. |> String.map (function | '$' -> '#'
  928. | c -> c)
  929. |> Uri.of_string
  930. |> Http.abs_to_rel ~base in
  931. assert (id |> Uri.to_string |> St.starts_with ~prefix:"o/");
  932. (match id |> Storage.select with
  933. | Error e ->
  934. Logr.warn (fun m -> m "%s.%s %s" "Iweb.Post" "get" e);
  935. emp
  936. | Ok e ->
  937. of_rfc4287 emp e )
  938. | _ -> emp in
  939. let r = r |> List.fold_left sift_bookmarklet_get emp
  940. in
  941. (* - look up url in storage
  942. * - if not present:
  943. * - if title not present
  944. * then
  945. * try to get from url
  946. * use title, description, keywords
  947. * - show 'linkform'
  948. *)
  949. let os v = v |> Option.value ~default:"" in
  950. let od v = v |> Option.value ~default:epoch_shaarli |> d2s in
  951. let ou v = v |> Option.value ~default:Uri.empty |> Uri.to_string in
  952. let ol v = v |> String.concat " " in
  953. let ob v = if v then "on" else "no" in
  954. Ok (`OK, [Http.H.ct_xml], (fun oc ->
  955. [
  956. n i_dat (r.dat |> od);
  957. n i_url (r.url |> ou);
  958. n i_tit (r.tit |> os);
  959. n i_dsc (r.dsc |> os);
  960. n i_tag (r.tag |> ol);
  961. n i_pri (r.pri |> ob);
  962. n i_sav "save_edit";
  963. n i_can "cancel_edit";
  964. n i_tok _token;
  965. n i_ret (r.img |> ou);
  966. n i_img (r.img |> ou);
  967. ]
  968. |> xhtmlform "Add" "linkform" [i_dat;i_url;i_tit;i_dsc;i_tag;i_pri;i_sav;i_can;i_tok;i_ret;i_img;] []
  969. |> to_channel ~xsl:"linkform.xsl" oc))
  970. (* https://code.mro.name/github/Shaarli-Vanilla/src/master/index.php#L1479 *)
  971. let post ~base uuid _ (_tok, (frm, (Auth.Uid uid, (req : Cgi.Request.t)))) =
  972. Logr.debug (fun m -> m "%s.%s %a %s" "Iweb.Post" "post" Uuidm.pp uuid req.query_string);
  973. let base = base () in
  974. let f () =
  975. let s = frm |> Uri.with_query Uri.empty in
  976. Logr.debug (fun m -> m "%s.%s %a %a" "Iweb.Post" "post" Uuidm.pp uuid Uri.pp s);
  977. let eee e =
  978. Logr.warn (fun m -> m "%s.%s %a error loading Cfg.Profile: %s" "Iweb.Post" "post" Uuidm.pp uuid e);
  979. Http.s422' in
  980. let now = Ptime_clock.now () in
  981. let* profile = Result.map_error eee Cfg.Profile.(from_file fn) in
  982. let lang = profile.language in
  983. let author = Uri.make ~userinfo:uid ~host:(Uri.host base |> Option.value ~default:"example.com") () in
  984. let* _pk = Ap.PubKeyPem.(private_of_pem pk_pem) |> Result.map_error (fun ( s) ->
  985. Logr.err (fun m -> m "%s %s.%s invalid private key: %s" E.e1026 "Ap" "post" s);
  986. Http.s500') in
  987. let r = frm |> List.fold_left sift_post empty in
  988. match r.sav with
  989. | Some Cancel ->
  990. Logr.debug (fun m -> m "%s.%s %s" "Iweb.Post" "post" "Cancel");
  991. Http.s302 "../"
  992. | Some Delete ->
  993. Logr.debug (fun m -> m "%s.%s %s" "Iweb.Post" "post" "Delete");
  994. (match req.query_string |> Uri.query_of_encoded |> List.assoc_opt "id" with
  995. | Some [id] ->
  996. let id = id
  997. (* revert substitution by posts.xsl *)
  998. |> String.map (function | '$' -> '#'
  999. | c -> c)
  1000. |> Uri.of_string
  1001. |> Http.abs_to_rel ~base in
  1002. (match id
  1003. |> Main.Note.Delete.delete
  1004. >>= Main.Note.Delete.notify_subscribers ~due:now ~base
  1005. with
  1006. | Ok r ->
  1007. Logr.info (fun m -> m "TODO %s.%s Delete refresh affected files. %a" "Iweb.Post" "post" Uri.pp r.id);
  1008. Http.s302 "../"
  1009. | Error e ->
  1010. Logr.warn (fun m -> m "%s.%s Delete %s" "Iweb.Post" "post" e);
  1011. Http.s500)
  1012. | None
  1013. | _ -> Http.s500)
  1014. | Some Save ->
  1015. (match
  1016. r
  1017. |> to_rfc4287 ~tz:profile.timezone ~now ~lang ~author
  1018. >>= Main.sift_urls
  1019. >>= Main.sift_tags Tag.cdb
  1020. >>= Main.sift_handles
  1021. >>= Main.Note.publish ~base ~author ~profile
  1022. >>= Main.Note.Create.notify_subscribers ~due:now ~base
  1023. with
  1024. | Ok (_n : Rfc4287.Entry.t) -> Http.s302 "../"
  1025. | Error (_ : string) -> Http.s500)
  1026. | None ->
  1027. Logr.err (fun m -> m "%s.%s %s" "Iweb.Post" "post" "None");
  1028. Http.s500
  1029. in
  1030. let r = f () in
  1031. let%lwt _ = Main.Queue.ping_and_forget ~base ~run_delay:60 in
  1032. Lwt.return r
  1033. end
  1034. module Tools = struct
  1035. let get _uuid _ = Http.s501
  1036. end
  1037. module Session = struct
  1038. let get _uuid (uid, _req) =
  1039. match uid with
  1040. | None -> (* no ban penalty but 404 nevertheless. *)
  1041. Http.s404
  1042. | Some (Auth.Uid v) ->
  1043. Ok (`OK, [Http.H.ct_xml], (fun oc -> output_string oc v))
  1044. end
  1045. (* send a potential new to-be-notified to their home server to subscribe back.
  1046. Requires the other side to implement webfinger RFC7033 and provide
  1047. rel=http://ostatus.org/schema/1.0/subscribe. *)
  1048. module Notifyme = struct
  1049. let get ~base uuid _tnow (r : Cgi.Request.t) =
  1050. assert ("http://ostatus.org/schema/1.0/subscribe" = As2_vocab.Constants.Webfinger.ostatus_rel);
  1051. match r.query_string |> Uri.query_of_encoded with
  1052. | ["resource",[acct]; "rel",["http://ostatus.org/schema/1.0/subscribe"]] ->
  1053. (Logr.debug (fun m -> m "%s.%s %a %s" "Iweb.Notifyme" "get" Uuidm.pp uuid acct);
  1054. match Webfinger.(acct
  1055. |> of_string
  1056. |> well_known_uri) with
  1057. | Error _ -> Http.s400
  1058. |> Lwt.return
  1059. | Ok wk ->
  1060. Logr.debug (fun m -> m "%s.%s %a webfinger: %a" "Iweb.Notifyme" "get" Uuidm.pp uuid Uri.pp wk);
  1061. let%lwt wf = wk |> Webfinger.Client.http_get in
  1062. match wf with
  1063. | Error _ -> Http.s500
  1064. |> Lwt.return
  1065. | Ok wf ->
  1066. match wf.links |> As2_vocab.Types.Webfinger.ostatus_subscribe with
  1067. | None -> Http.s502
  1068. |> Lwt.return
  1069. | Some tpl ->
  1070. Logr.debug (fun m -> m "%s.%s %a got template %s" "Iweb.Notifyme" "get" Uuidm.pp uuid tpl);
  1071. let rx = Str.regexp_string "{uri}" in
  1072. let uri = Http.reso ~base:(base()) (Uri.make ~path:Ap.proj ())
  1073. |> Uri.to_string in
  1074. tpl
  1075. |> Str.replace_first rx uri
  1076. |> Http.s302
  1077. |> Lwt.return )
  1078. | _ -> Http.s400
  1079. |> Lwt.return
  1080. end