Skip to content

Commit 0d25f20

Browse files
committed
Offer better protections against solution overwriting
Signed-off-by: Yann Regis-Gianas <[email protected]>
1 parent 6e3fa0f commit 0d25f20

File tree

12 files changed

+150
-38
lines changed

12 files changed

+150
-38
lines changed

src/ace-lib/ace.ml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2020 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -20,6 +20,8 @@ type 'a editor = {
2020
editor: ('a editor * 'a option) Ace_types.editor Js.t;
2121
mutable marks: int list;
2222
mutable keybinding_menu: bool;
23+
mutable synchronized : bool;
24+
mutable sync_observers : (bool -> unit) list;
2325
}
2426

2527
let ace : Ace_types.ace Js.t = Js.Unsafe.variable "ace"
@@ -30,10 +32,13 @@ let create_position r c =
3032
pos##.row := r;
3133
pos##.column := c;
3234
pos
35+
3336
let greater_position p1 p2 =
3437
p1##.row > p2##.row ||
3538
(p1##.row = p2##.row && p1##.column > p2##.column)
3639

40+
let register_sync_observer editor obs =
41+
editor.sync_observers <- obs :: editor.sync_observers
3742

3843
let create_range s e =
3944
let range : range Js.t = Js.Unsafe.obj [||] in
@@ -77,15 +82,34 @@ let get_contents ?range e =
7782
let r = create_range (create_position r1 c1) (create_position r2 c2) in
7883
Js.to_string @@ document##(getTextRange r)
7984

80-
let create_editor editor_div =
85+
let set_synchronized_status editor status =
86+
List.iter (fun obs -> obs status) editor.sync_observers;
87+
editor.synchronized <- status
88+
89+
let create_editor editor_div check_valid_state =
8190
let editor = edit editor_div in
8291
Js.Unsafe.set editor "$blockScrolling" (Js.Unsafe.variable "Infinity");
8392
let data =
84-
{ editor; editor_div; marks = []; keybinding_menu = false; } in
93+
{ editor; editor_div;
94+
marks = [];
95+
keybinding_menu = false;
96+
synchronized = true;
97+
sync_observers = []
98+
}
99+
in
85100
editor##.customData := (data, None);
86101
editor##setOption (Js.string "displayIndentGuides") (Js.bool false);
102+
editor##on (Js.string "change") (fun () ->
103+
check_valid_state (set_contents data) ;
104+
set_synchronized_status data false);
87105
data
88106

107+
let set_synchronized editor =
108+
set_synchronized_status editor true
109+
110+
let is_synchronized editor =
111+
editor.synchronized
112+
89113
let get_custom_data { editor } =
90114
match snd editor##.customData with
91115
| None -> raise Not_found

src/ace-lib/ace.mli

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ type loc = {
1717
loc_end: int * int;
1818
}
1919

20-
val create_editor: Dom_html.divElement Js.t -> 'a editor
20+
val create_editor: Dom_html.divElement Js.t -> ((string -> unit) -> unit) -> 'a editor
21+
22+
val is_synchronized : 'a editor -> bool
23+
24+
val set_synchronized : 'a editor -> unit
25+
26+
val register_sync_observer : 'a editor -> (bool -> unit) -> unit
2127

2228
val set_mode: 'a editor -> string -> unit
2329

src/ace-lib/ocaml_mode.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,8 @@ let do_delete ace_editor =
514514
Ace.remove ace_editor "left"
515515
end
516516

517-
let create_ocaml_editor div =
518-
let ace = Ace.create_editor div in
517+
let create_ocaml_editor div check_valid_state =
518+
let ace = Ace.create_editor div check_valid_state in
519519
Ace.set_mode ace "ace/mode/ocaml.ocp";
520520
Ace.set_tab_size ace !config.indent.IndentConfig.i_base;
521521
let editor = { ace; current_error = None; current_warnings = [] } in

src/ace-lib/ocaml_mode.mli

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ type msg = {
2020
msg: string;
2121
}
2222

23-
2423
type error = msg list
2524

2625
type warning = error
2726

28-
val create_ocaml_editor: Dom_html.divElement Js.t -> editor
27+
val create_ocaml_editor: Dom_html.divElement Js.t -> ((string -> unit) -> unit) -> editor
2928
val get_editor: editor -> editor Ace.editor
3029

3130
val report_error: editor -> ?set_class: bool -> error option -> warning list -> unit Lwt.t

src/app/learnocaml_common.ml

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2020 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -434,30 +434,34 @@ let get_state_as_save_file ?(include_reports = false) () =
434434
all_exercise_toplevel_histories = retrieve all_exercise_toplevel_histories;
435435
}
436436

437-
let rec sync_save token save_file =
437+
let rec sync_save token save_file on_sync =
438438
Server_caller.request (Learnocaml_api.Update_save (token, save_file))
439439
>>= function
440-
| Ok save -> set_state_from_save_file ~token save; Lwt.return save
440+
| Ok save ->
441+
set_state_from_save_file ~token save;
442+
on_sync ();
443+
Lwt.return save
441444
| Error (`Not_found _) ->
442445
Server_caller.request_exn
443446
(Learnocaml_api.Create_token ("", Some token, None)) >>= fun _token ->
444447
assert (_token = token);
445448
Server_caller.request_exn
446449
(Learnocaml_api.Update_save (token, save_file)) >>= fun save ->
447450
set_state_from_save_file ~token save;
451+
on_sync ();
448452
Lwt.return save
449453
| Error e ->
450454
lwt_alert ~title:[%i"SYNC FAILED"] [
451455
H.p [H.txt [%i"Could not synchronise save with the server"]];
452456
H.code [H.txt (Server_caller.string_of_error e)];
453457
] ~buttons:[
454-
[%i"Retry"], (fun () -> sync_save token save_file);
455-
[%i"Ignore"], (fun () -> Lwt.return save_file);
458+
[%i"Retry"], (fun () -> sync_save token save_file on_sync);
459+
[%i"Ignore"], (fun () -> Lwt.return save_file);
456460
]
457461

458-
let sync token = sync_save token (get_state_as_save_file ())
462+
let sync token on_sync = sync_save token (get_state_as_save_file ()) on_sync
459463

460-
let sync_exercise token ?answer ?editor id =
464+
let sync_exercise token ?answer ?editor id on_sync =
461465
let handle_serverless () =
462466
(* save the text at least locally (but not the report & grade, that could
463467
be misleading) *)
@@ -494,7 +498,7 @@ let sync_exercise token ?answer ?editor id =
494498
} in
495499
match token with
496500
| Some token ->
497-
Lwt.catch (fun () -> sync_save token save_file)
501+
Lwt.catch (fun () -> sync_save token save_file on_sync)
498502
(fun e ->
499503
handle_serverless ();
500504
raise e)
@@ -708,11 +712,48 @@ let mouseover_toggle_signal elt sigvalue setter =
708712
in
709713
Manip.Ev.onmouseover elt hdl
710714
715+
(*
716+
717+
If a user has made no change to a solution for the exercise [id]
718+
for 180 seconds, [check_valid_editor_state id] ensures that there is
719+
no more recent version of this solution in the server. If this is
720+
the case, the user is asked if we should download this solution
721+
from the server.
722+
723+
This function reduces the risk of an involuntary overwriting of a
724+
student solution when the solution is open in several clients.
725+
726+
*)
727+
let check_valid_editor_state id =
728+
let last_changed = ref (Unix.gettimeofday ()) in
729+
fun update_content ->
730+
let update_local_copy checking_time () =
731+
match Learnocaml_local_storage.(retrieve (exercise_state id)) with
732+
| { Answer.mtime; solution; _ } ->
733+
if mtime > checking_time then (
734+
if Js_utils.confirm
735+
[%i "A more recent answer exists on the server. \
736+
Do you want to update the current one?"]
737+
then
738+
update_content solution;
739+
);
740+
Lwt.return ()
741+
| exception Not_found -> Lwt.return ()
742+
in
743+
let now = Unix.gettimeofday () in
744+
if now -. !last_changed > 180. then (
745+
let checking_time = !last_changed in
746+
last_changed := now;
747+
Lwt.async (update_local_copy checking_time)
748+
)
749+
750+
711751
let ace_display tab =
712752
let ace = lazy (
713753
let answer =
714754
Ocaml_mode.create_ocaml_editor
715755
(Tyxml_js.To_dom.of_div tab)
756+
ignore
716757
in
717758
let ace = Ocaml_mode.get_editor answer in
718759
Ace.set_font_size ace 16;
@@ -874,7 +915,8 @@ end
874915
875916
module Editor_button (E : Editor_info) = struct
876917
877-
let editor_button = button ~container:E.buttons_container ~theme:"light"
918+
let editor_button =
919+
button ~container:E.buttons_container ~theme:"light"
878920
879921
let cleanup template =
880922
editor_button
@@ -901,16 +943,26 @@ module Editor_button (E : Editor_info) = struct
901943
select_tab "toplevel";
902944
Lwt.return_unit
903945
904-
let sync token id =
905-
editor_button
946+
let sync token id on_sync =
947+
let state = button_state () in
948+
(editor_button
949+
~state
906950
~icon: "sync" [%i"Sync"] @@ fun () ->
907951
token >>= fun token ->
908-
sync_exercise token id ~editor:(Ace.get_contents E.ace) >|= fun _save -> ()
952+
sync_exercise token id ~editor:(Ace.get_contents E.ace) on_sync
953+
>|= fun _save -> ());
954+
Ace.register_sync_observer E.ace (fun sync ->
955+
if sync then disable_button state else enable_button state)
956+
909957
end
910958
911-
let setup_editor solution =
959+
let setup_editor id solution =
912960
let editor_pane = find_component "learnocaml-exo-editor-pane" in
913-
let editor = Ocaml_mode.create_ocaml_editor (Tyxml_js.To_dom.of_div editor_pane) in
961+
let editor =
962+
Ocaml_mode.create_ocaml_editor
963+
(Tyxml_js.To_dom.of_div editor_pane)
964+
(check_valid_editor_state id)
965+
in
914966
let ace = Ocaml_mode.get_editor editor in
915967
Ace.set_contents ace ~reset_undo:true solution;
916968
Ace.set_font_size ace 18;

src/app/learnocaml_common.mli

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,22 @@ val set_state_from_save_file :
119119
(** Gets a save file containing the locally stored data *)
120120
val get_state_as_save_file : ?include_reports:bool -> unit -> Save.t
121121

122-
(** Sync the local save state with the server state, and returns the merged save
123-
file. The save will be created on the server if it doesn't exist.
122+
(**
123+
[sync token on_sync] synchronizes the local save state with the server state,
124+
and returns the merged save file. The save will be created on the server
125+
if it doesn't exist. [on_sync ()] is called when this is done.
124126
125-
This syncs student {b content}, but never the reports which are only synched
126-
on "Grade" *)
127-
val sync: Token.t -> Save.t Lwt.t
127+
Notice that this function synchronizes student {b,content} but not the
128+
reports which are only synchronized when an actual "grading" is done.
129+
*)
130+
val sync: Token.t -> (unit -> unit) -> Save.t Lwt.t
128131

129132
(** The same, but limiting the submission to the given exercise, using the given
130133
answer if any, and the given editor text, if any. *)
131134
val sync_exercise:
132135
Token.t option -> ?answer:Learnocaml_data.Answer.t -> ?editor:string ->
133136
Learnocaml_data.Exercise.id ->
137+
(unit -> unit) ->
134138
Save.t Lwt.t
135139

136140
val countdown:
@@ -211,10 +215,10 @@ module Editor_button (_ : Editor_info) : sig
211215
val cleanup : string -> unit
212216
val download : string -> unit
213217
val eval : Learnocaml_toplevel.t -> (string -> unit) -> unit
214-
val sync : Token.t option Lwt.t -> Learnocaml_data.SMap.key -> unit
218+
val sync : Token.t option Lwt.t -> Learnocaml_data.SMap.key -> (unit -> unit) -> unit
215219
end
216220

217-
val setup_editor : string -> Ocaml_mode.editor * Ocaml_mode.editor Ace.editor
221+
val setup_editor : string -> string -> Ocaml_mode.editor * Ocaml_mode.editor Ace.editor
218222

219223
val typecheck :
220224
Learnocaml_toplevel.t ->

src/app/learnocaml_exercise_main.ml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2020 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -179,10 +179,10 @@ let () =
179179
Tyxml_js.Html5.[ h1 [ txt ex_meta.Exercise.Meta.title ] ;
180180
Tyxml_js.Of_dom.of_iFrame text_iframe ] ;
181181
(* ---- editor pane --------------------------------------------------- *)
182-
let editor, ace = setup_editor solution in
182+
let editor, ace = setup_editor id solution in
183183
let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in
184184
EB.cleanup (Learnocaml_exercise.(access File.template exo));
185-
EB.sync token id;
185+
EB.sync token id (fun () -> Ace.set_synchronized ace) ;
186186
EB.download id;
187187
EB.eval top select_tab;
188188
let typecheck = typecheck top ace editor in
@@ -267,7 +267,8 @@ let () =
267267
Some solution, None
268268
in
269269
token >>= fun token ->
270-
sync_exercise token id ?answer ?editor >>= fun _save ->
270+
sync_exercise token id ?answer ?editor (fun () -> Ace.set_synchronized ace)
271+
>>= fun _save ->
271272
select_tab "report" ;
272273
Lwt_js.yield () >>= fun () ->
273274
Ace.focus ace ;
@@ -286,7 +287,7 @@ let () =
286287
Ace.focus ace ;
287288
typecheck true
288289
end ;
289-
Window.onunload (fun _ev -> local_save ace id; true);
290+
Window.onbeforeunload (fun _ -> (not (Ace.is_synchronized ace), false));
290291
(* ---- return -------------------------------------------------------- *)
291292
toplevel_launch >>= fun _ ->
292293
typecheck false >>= fun () ->

src/app/learnocaml_index_main.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@ let () =
815815
Lwt.return_unit);
816816
[%i"Sync workspace"], "sync", (fun () ->
817817
catch_with_alert @@ fun () ->
818-
sync () >>= fun _ -> Lwt.return_unit);
818+
sync () ignore >>= fun _ -> Lwt.return_unit);
819819
[%i"Export to file"], "download", download_save;
820820
[%i"Import"], "upload", import_save;
821821
[%i"Download all source files"], "download", download_all;

src/app/learnocaml_playground_main.ml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ let main () =
2727
disable_button_group toplevel_buttons_group (* enabled after init *) ;
2828
let toplevel_toolbar = find_component "learnocaml-exo-toplevel-toolbar" in
2929
let editor_toolbar = find_component "learnocaml-exo-editor-toolbar" in
30-
let toplevel_button =
31-
button ~container: toplevel_toolbar ~theme: "dark" ~group:toplevel_buttons_group ?state:None in
30+
let toplevel_button ~icon label cb =
31+
ignore @@
32+
button
33+
~icon ~container: toplevel_toolbar
34+
~theme: "dark" ~group:toplevel_buttons_group ?state:None label cb
35+
in
3236
let id = match Url.Current.path with
3337
| "" :: "playground" :: p | "playground" :: p ->
3438
String.concat "/" (List.map Url.urldecode (List.filter ((<>) "") p))
@@ -60,7 +64,7 @@ let main () =
6064
(* ---- toplevel pane ------------------------------------------------- *)
6165
init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ;
6266
(* ---- editor pane --------------------------------------------------- *)
63-
let editor, ace = setup_editor solution in
67+
let editor, ace = setup_editor id solution in
6468
let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in
6569
EB.cleanup playground.Playground.template;
6670
EB.download id;

src/utils/js_utils.ml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,16 @@ module Window = struct
11581158
let head win = Tyxml_js.Of_dom.of_head win##.document##.head
11591159
let onunload ?(win = Dom_html.window) f =
11601160
win##.onunload := Dom_html.handler (fun ev -> Js.bool (f ev))
1161+
let onbeforeunload ?(win = Dom_html.window) f =
1162+
win##.onbeforeunload := Dom_html.handler (fun ev ->
1163+
let (status, propagate) = f ev in
1164+
if status then (
1165+
Js.bool propagate
1166+
) else
1167+
(
1168+
Js.(Unsafe.eval_string "undefined");
1169+
)
1170+
)
11611171
let onresize ?(win = Dom_html.window) f =
11621172
win##.onresize := Dom_html.handler (fun ev -> Js.bool (f ev))
11631173
let prompt ?(win = Dom_html.window) ?(value = "") msg =

0 commit comments

Comments
 (0)