Skip to content

Commit 380596f

Browse files
committed
Offer better protections again solution overwriting
Signed-off-by: Yann Regis-Gianas <[email protected]>
1 parent 28eedb3 commit 380596f

File tree

12 files changed

+154
-38
lines changed

12 files changed

+154
-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
@@ -71,15 +76,34 @@ let get_line {editor} line =
7176
let document = (editor##getSession)##getDocument in
7277
Js.to_string @@ document##(getLine line)
7378

74-
let create_editor editor_div =
79+
let set_synchronized_status editor status =
80+
List.iter (fun obs -> obs status) editor.sync_observers;
81+
editor.synchronized <- status
82+
83+
let create_editor editor_div check_valid_state =
7584
let editor = edit editor_div in
7685
Js.Unsafe.set editor "$blockScrolling" (Js.Unsafe.variable "Infinity");
7786
let data =
78-
{ editor; editor_div; marks = []; keybinding_menu = false; } in
87+
{ editor; editor_div;
88+
marks = [];
89+
keybinding_menu = false;
90+
synchronized = true;
91+
sync_observers = []
92+
}
93+
in
7994
editor##.customData := (data, None);
8095
editor##setOption (Js.string "displayIndentGuides") (Js.bool false);
96+
editor##on (Js.string "change") (fun () ->
97+
check_valid_state (set_contents data) ;
98+
set_synchronized_status data false);
8199
data
82100

101+
let set_synchronized editor =
102+
set_synchronized_status editor true
103+
104+
let is_synchronized editor =
105+
editor.synchronized
106+
83107
let get_custom_data { editor } =
84108
match snd editor##.customData with
85109
| 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
@@ -506,8 +506,8 @@ let do_delete ace_editor =
506506
Ace.remove ace_editor "left"
507507
end
508508

509-
let create_ocaml_editor div =
510-
let ace = Ace.create_editor div in
509+
let create_ocaml_editor div check_valid_state =
510+
let ace = Ace.create_editor div check_valid_state in
511511
Ace.set_mode ace "ace/mode/ocaml.ocp";
512512
Ace.set_tab_size ace !config.indent.IndentConfig.i_base;
513513
let editor = { ace; current_error = None; current_warnings = [] } in

src/ace-lib/ocaml_mode.mli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type warning = {
2525
msg: string;
2626
}
2727

28-
val create_ocaml_editor: Dom_html.divElement Js.t -> editor
28+
val create_ocaml_editor: Dom_html.divElement Js.t -> ((string -> unit) -> unit) -> editor
2929
val get_editor: editor -> editor Ace.editor
3030

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

src/app/learnocaml_common.ml

Lines changed: 69 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
@@ -432,30 +432,34 @@ let get_state_as_save_file ?(include_reports = false) () =
432432
all_exercise_toplevel_histories = retrieve all_exercise_toplevel_histories;
433433
}
434434

435-
let rec sync_save token save_file =
435+
let rec sync_save token save_file on_sync =
436436
Server_caller.request (Learnocaml_api.Update_save (token, save_file))
437437
>>= function
438-
| Ok save -> set_state_from_save_file ~token save; Lwt.return save
438+
| Ok save ->
439+
set_state_from_save_file ~token save;
440+
on_sync ();
441+
Lwt.return save
439442
| Error (`Not_found _) ->
440443
Server_caller.request_exn
441444
(Learnocaml_api.Create_token ("", Some token, None)) >>= fun _token ->
442445
assert (_token = token);
443446
Server_caller.request_exn
444447
(Learnocaml_api.Update_save (token, save_file)) >>= fun save ->
445448
set_state_from_save_file ~token save;
449+
on_sync ();
446450
Lwt.return save
447451
| Error e ->
448452
lwt_alert ~title:[%i"SYNC FAILED"] [
449453
H.p [H.txt [%i"Could not synchronise save with the server"]];
450454
H.code [H.txt (Server_caller.string_of_error e)];
451455
] ~buttons:[
452-
[%i"Retry"], (fun () -> sync_save token save_file);
453-
[%i"Ignore"], (fun () -> Lwt.return save_file);
456+
[%i"Retry"], (fun () -> sync_save token save_file on_sync);
457+
[%i"Ignore"], (fun () -> Lwt.return save_file);
454458
]
455459

456-
let sync token = sync_save token (get_state_as_save_file ())
460+
let sync token on_sync = sync_save token (get_state_as_save_file ()) on_sync
457461

458-
let sync_exercise token ?answer ?editor id =
462+
let sync_exercise token ?answer ?editor id on_sync =
459463
let handle_serverless () =
460464
(* save the text at least locally (but not the report & grade, that could
461465
be misleading) *)
@@ -492,7 +496,7 @@ let sync_exercise token ?answer ?editor id =
492496
} in
493497
match token with
494498
| Some token ->
495-
Lwt.catch (fun () -> sync_save token save_file)
499+
Lwt.catch (fun () -> sync_save token save_file on_sync)
496500
(fun e ->
497501
handle_serverless ();
498502
raise e)
@@ -706,11 +710,51 @@ let mouseover_toggle_signal elt sigvalue setter =
706710
in
707711
Manip.Ev.onmouseover elt hdl
708712
713+
(*
714+
715+
If a user has made no change to a solution for the exercise [id]
716+
for 180 seconds, [check_valid_editor_state id] ensures that there is
717+
no more recent version of this solution in the server. If this is
718+
the case, the user is asked if we should download this solution
719+
from the server.
720+
721+
This function reduces the risk of an involuntary overwriting of a
722+
student solution when the solution is open in several clients.
723+
724+
*)
725+
let check_valid_editor_state id =
726+
let last_changed = ref (Unix.gettimeofday ()) in
727+
fun update_content ->
728+
let update_local_copy checking_time () =
729+
match id with
730+
| None -> Lwt.return ()
731+
| Some id ->
732+
match Learnocaml_local_storage.(retrieve (exercise_state id)) with
733+
| { Answer.mtime; solution; _ } ->
734+
if mtime > checking_time then (
735+
if Js_utils.confirm
736+
[%i "A more recent answer exists on the server. \
737+
Do you want to update the current one?"]
738+
then
739+
update_content solution;
740+
);
741+
Lwt.return ()
742+
| exception Not_found -> Lwt.return ()
743+
in
744+
let now = Unix.gettimeofday () in
745+
if now -. !last_changed > 180. then (
746+
let checking_time = !last_changed in
747+
last_changed := now;
748+
Lwt.async (update_local_copy checking_time)
749+
)
750+
751+
709752
let ace_display tab =
710753
let ace = lazy (
711754
let answer =
712755
Ocaml_mode.create_ocaml_editor
713756
(Tyxml_js.To_dom.of_div tab)
757+
(check_valid_editor_state None)
714758
in
715759
let ace = Ocaml_mode.get_editor answer in
716760
Ace.set_font_size ace 16;
@@ -872,7 +916,8 @@ end
872916
873917
module Editor_button (E : Editor_info) = struct
874918
875-
let editor_button = button ~container:E.buttons_container ~theme:"light"
919+
let editor_button =
920+
button ~container:E.buttons_container ~theme:"light"
876921
877922
let cleanup template =
878923
editor_button
@@ -898,16 +943,26 @@ module Editor_button (E : Editor_info) = struct
898943
select_tab "toplevel";
899944
Lwt.return_unit
900945
901-
let sync token id =
902-
editor_button
946+
let sync token id on_sync =
947+
let state = button_state () in
948+
(editor_button
949+
~state
903950
~icon: "sync" [%i"Sync"] @@ fun () ->
904951
token >>= fun token ->
905-
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+
906957
end
907958
908-
let setup_editor solution =
959+
let setup_editor id solution =
909960
let editor_pane = find_component "learnocaml-exo-editor-pane" in
910-
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 (Some id))
965+
in
911966
let ace = Ocaml_mode.get_editor editor in
912967
Ace.set_contents ace ~reset_undo:true solution;
913968
Ace.set_font_size ace 18;

src/app/learnocaml_common.mli

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

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

128132
(** The same, but limiting the submission to the given exercise, using the given
129133
answer if any, and the given editor text, if any. *)
130134
val sync_exercise:
131135
Token.t option -> ?answer:Learnocaml_data.Answer.t -> ?editor:string ->
132136
Learnocaml_data.Exercise.id ->
137+
(unit -> unit) ->
133138
Save.t Lwt.t
134139

135140
val countdown:
@@ -210,10 +215,10 @@ module Editor_button (E : Editor_info) : sig
210215
val cleanup : string -> unit
211216
val download : string -> unit
212217
val eval : Learnocaml_toplevel.t -> (string -> 'a) -> unit
213-
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
214219
end
215220

216-
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
217222

218223
val typecheck :
219224
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
@@ -177,10 +177,10 @@ let () =
177177
Tyxml_js.Html5.[ h1 [ txt ex_meta.Exercise.Meta.title ] ;
178178
Tyxml_js.Of_dom.of_iFrame text_iframe ] ;
179179
(* ---- editor pane --------------------------------------------------- *)
180-
let editor, ace = setup_editor solution in
180+
let editor, ace = setup_editor id solution in
181181
let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in
182182
EB.cleanup (Learnocaml_exercise.(access File.template exo));
183-
EB.sync token id;
183+
EB.sync token id (fun () -> Ace.set_synchronized ace) ;
184184
EB.download id;
185185
EB.eval top select_tab;
186186
let typecheck = typecheck top ace editor in
@@ -264,7 +264,8 @@ let () =
264264
Some solution, None
265265
in
266266
token >>= fun token ->
267-
sync_exercise token id ?answer ?editor >>= fun _save ->
267+
sync_exercise token id ?answer ?editor (fun () -> Ace.set_synchronized ace)
268+
>>= fun _save ->
268269
select_tab "report" ;
269270
Lwt_js.yield () >>= fun () ->
270271
Ace.focus ace ;
@@ -283,7 +284,7 @@ let () =
283284
Ace.focus ace ;
284285
typecheck true
285286
end ;
286-
Window.onunload (fun _ev -> local_save ace id; true);
287+
Window.onbeforeunload (fun _ -> (not (Ace.is_synchronized ace), false));
287288
(* ---- return -------------------------------------------------------- *)
288289
toplevel_launch >>= fun _ ->
289290
typecheck false >>= fun () ->

src/app/learnocaml_index_main.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ let () =
811811
Lwt.return_unit);
812812
[%i"Sync workspace"], "sync", (fun () ->
813813
catch_with_alert @@ fun () ->
814-
sync () >>= fun _ -> Lwt.return_unit);
814+
sync () ignore >>= fun _ -> Lwt.return_unit);
815815
[%i"Export to file"], "download", download_save;
816816
[%i"Import"], "upload", import_save;
817817
[%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
@@ -26,8 +26,12 @@ let main () =
2626
disable_button_group toplevel_buttons_group (* enabled after init *) ;
2727
let toplevel_toolbar = find_component "learnocaml-exo-toplevel-toolbar" in
2828
let editor_toolbar = find_component "learnocaml-exo-editor-toolbar" in
29-
let toplevel_button =
30-
button ~container: toplevel_toolbar ~theme: "dark" ~group:toplevel_buttons_group ?state:None in
29+
let toplevel_button ~icon label cb =
30+
ignore @@
31+
button
32+
~icon ~container: toplevel_toolbar
33+
~theme: "dark" ~group:toplevel_buttons_group ?state:None label cb
34+
in
3135
let id = match Url.Current.path with
3236
| "" :: "playground" :: p | "playground" :: p ->
3337
String.concat "/" (List.map Url.urldecode (List.filter ((<>) "") p))
@@ -59,7 +63,7 @@ let main () =
5963
(* ---- toplevel pane ------------------------------------------------- *)
6064
init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ;
6165
(* ---- editor pane --------------------------------------------------- *)
62-
let editor, ace = setup_editor solution in
66+
let editor, ace = setup_editor id solution in
6367
let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in
6468
EB.cleanup playground.Playground.template;
6569
EB.download id;

src/utils/js_utils.ml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,16 @@ module Window = struct
11431143
let head win = Tyxml_js.Of_dom.of_head win##.document##.head
11441144
let onunload ?(win = Dom_html.window) f =
11451145
win##.onunload := Dom_html.handler (fun ev -> Js.bool (f ev))
1146+
let onbeforeunload ?(win = Dom_html.window) f =
1147+
win##.onbeforeunload := Dom_html.handler (fun ev ->
1148+
let (status, propagate) = f ev in
1149+
if status then (
1150+
Js.bool propagate
1151+
) else
1152+
(
1153+
Js.(Unsafe.eval_string "undefined");
1154+
)
1155+
)
11461156
let onresize ?(win = Dom_html.window) f =
11471157
win##.onresize := Dom_html.handler (fun ev -> Js.bool (f ev))
11481158
let prompt ?(win = Dom_html.window) ?(value = "") msg =

0 commit comments

Comments
 (0)