@fiduswriter/editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +22 -0
- package/dist/bibliography/tools.js +53 -0
- package/dist/bibliography/tools.js.map +1 -0
- package/dist/citations/index.js +339 -0
- package/dist/citations/index.js.map +1 -0
- package/dist/citations/render.js +47 -0
- package/dist/citations/render.js.map +1 -0
- package/dist/clipboard/copy/index.js +2 -0
- package/dist/clipboard/copy/index.js.map +1 -0
- package/dist/clipboard/copy/schema.js +54 -0
- package/dist/clipboard/copy/schema.js.map +1 -0
- package/dist/clipboard/copy/serializers.js +132 -0
- package/dist/clipboard/copy/serializers.js.map +1 -0
- package/dist/clipboard/paste/fidus_writer.js +45 -0
- package/dist/clipboard/paste/fidus_writer.js.map +1 -0
- package/dist/clipboard/paste/general.js +76 -0
- package/dist/clipboard/paste/general.js.map +1 -0
- package/dist/clipboard/paste/google_docs.js +24 -0
- package/dist/clipboard/paste/google_docs.js.map +1 -0
- package/dist/clipboard/paste/html.js +68 -0
- package/dist/clipboard/paste/html.js.map +1 -0
- package/dist/clipboard/paste/index.js +3 -0
- package/dist/clipboard/paste/index.js.map +1 -0
- package/dist/clipboard/paste/libreoffice_writer.js +27 -0
- package/dist/clipboard/paste/libreoffice_writer.js.map +1 -0
- package/dist/clipboard/paste/microsoft_word.js +79 -0
- package/dist/clipboard/paste/microsoft_word.js.map +1 -0
- package/dist/clipboard/paste/text.js +41 -0
- package/dist/clipboard/paste/text.js.map +1 -0
- package/dist/collab/chat.js +154 -0
- package/dist/collab/chat.js.map +1 -0
- package/dist/collab/colors.js +62 -0
- package/dist/collab/colors.js.map +1 -0
- package/dist/collab/doc.js +1090 -0
- package/dist/collab/doc.js.map +1 -0
- package/dist/collab/index.js +75 -0
- package/dist/collab/index.js.map +1 -0
- package/dist/collab/merge/changeset.js +90 -0
- package/dist/collab/merge/changeset.js.map +1 -0
- package/dist/collab/merge/editor.js +708 -0
- package/dist/collab/merge/editor.js.map +1 -0
- package/dist/collab/merge/footnotes.js +149 -0
- package/dist/collab/merge/footnotes.js.map +1 -0
- package/dist/collab/merge/index.js +270 -0
- package/dist/collab/merge/index.js.map +1 -0
- package/dist/collab/merge/recreate_transform.js +259 -0
- package/dist/collab/merge/recreate_transform.js.map +1 -0
- package/dist/collab/merge/schema.js +117 -0
- package/dist/collab/merge/schema.js.map +1 -0
- package/dist/collab/merge/state_plugins/action.js +138 -0
- package/dist/collab/merge/state_plugins/action.js.map +1 -0
- package/dist/collab/merge/state_plugins/clipboard.js +32 -0
- package/dist/collab/merge/state_plugins/clipboard.js.map +1 -0
- package/dist/collab/merge/state_plugins/diff.js +541 -0
- package/dist/collab/merge/state_plugins/diff.js.map +1 -0
- package/dist/collab/merge/state_plugins/index.js +3 -0
- package/dist/collab/merge/state_plugins/index.js.map +1 -0
- package/dist/collab/merge/tools.js +175 -0
- package/dist/collab/merge/tools.js.map +1 -0
- package/dist/collab/templates.js +10 -0
- package/dist/collab/templates.js.map +1 -0
- package/dist/comments/comment.js +17 -0
- package/dist/comments/comment.js.map +1 -0
- package/dist/comments/editors/answer.js +43 -0
- package/dist/comments/editors/answer.js.map +1 -0
- package/dist/comments/editors/comment.js +278 -0
- package/dist/comments/editors/comment.js.map +1 -0
- package/dist/comments/editors/index.js +4 -0
- package/dist/comments/editors/index.js.map +1 -0
- package/dist/comments/editors/notify.js +13 -0
- package/dist/comments/editors/notify.js.map +1 -0
- package/dist/comments/editors/schema.js +60 -0
- package/dist/comments/editors/schema.js.map +1 -0
- package/dist/comments/index.js +11 -0
- package/dist/comments/index.js.map +1 -0
- package/dist/comments/interactions.js +379 -0
- package/dist/comments/interactions.js.map +1 -0
- package/dist/comments/store.js +438 -0
- package/dist/comments/store.js.map +1 -0
- package/dist/contacts/add_dialog.js +88 -0
- package/dist/contacts/add_dialog.js.map +1 -0
- package/dist/contacts/templates.js +23 -0
- package/dist/contacts/templates.js.map +1 -0
- package/dist/copyright_dialog/index.js +148 -0
- package/dist/copyright_dialog/index.js.map +1 -0
- package/dist/copyright_dialog/templates.js +48 -0
- package/dist/copyright_dialog/templates.js.map +1 -0
- package/dist/databases/bibliography.js +162 -0
- package/dist/databases/bibliography.js.map +1 -0
- package/dist/databases/images.js +260 -0
- package/dist/databases/images.js.map +1 -0
- package/dist/databases/index.js +55 -0
- package/dist/databases/index.js.map +1 -0
- package/dist/dialogs/citation.js +321 -0
- package/dist/dialogs/citation.js.map +1 -0
- package/dist/dialogs/code_block.js +192 -0
- package/dist/dialogs/code_block.js.map +1 -0
- package/dist/dialogs/contributor.js +132 -0
- package/dist/dialogs/contributor.js.map +1 -0
- package/dist/dialogs/figure.js +378 -0
- package/dist/dialogs/figure.js.map +1 -0
- package/dist/dialogs/index.js +11 -0
- package/dist/dialogs/index.js.map +1 -0
- package/dist/dialogs/language.js +47 -0
- package/dist/dialogs/language.js.map +1 -0
- package/dist/dialogs/link.js +243 -0
- package/dist/dialogs/link.js.map +1 -0
- package/dist/dialogs/math.js +121 -0
- package/dist/dialogs/math.js.map +1 -0
- package/dist/dialogs/ordered_list.js +67 -0
- package/dist/dialogs/ordered_list.js.map +1 -0
- package/dist/dialogs/revision.js +36 -0
- package/dist/dialogs/revision.js.map +1 -0
- package/dist/dialogs/subsup.js +109 -0
- package/dist/dialogs/subsup.js.map +1 -0
- package/dist/dialogs/table.js +217 -0
- package/dist/dialogs/table.js.map +1 -0
- package/dist/dialogs/templates.js +366 -0
- package/dist/dialogs/templates.js.map +1 -0
- package/dist/document_template/exporter.js +83 -0
- package/dist/document_template/exporter.js.map +1 -0
- package/dist/document_template/index.js +327 -0
- package/dist/document_template/index.js.map +1 -0
- package/dist/documents/access_rights/index.js +532 -0
- package/dist/documents/access_rights/index.js.map +1 -0
- package/dist/documents/access_rights/templates.js +150 -0
- package/dist/documents/access_rights/templates.js.map +1 -0
- package/dist/e2ee/encryptor.js +201 -0
- package/dist/e2ee/encryptor.js.map +1 -0
- package/dist/e2ee/key-manager.js +171 -0
- package/dist/e2ee/key-manager.js.map +1 -0
- package/dist/e2ee/passphrase-crypto.js +365 -0
- package/dist/e2ee/passphrase-crypto.js.map +1 -0
- package/dist/e2ee/passphrase-dialog.js +548 -0
- package/dist/e2ee/passphrase-dialog.js.map +1 -0
- package/dist/e2ee/passphrase-manager.js +350 -0
- package/dist/e2ee/passphrase-manager.js.map +1 -0
- package/dist/e2ee/password-dialog.js +552 -0
- package/dist/e2ee/password-dialog.js.map +1 -0
- package/dist/e2ee/snapshot-manager.js +238 -0
- package/dist/e2ee/snapshot-manager.js.map +1 -0
- package/dist/exporter/native/copy.js +29 -0
- package/dist/exporter/native/copy.js.map +1 -0
- package/dist/exporter/native/file.js +19 -0
- package/dist/exporter/native/file.js.map +1 -0
- package/dist/exporter/native/index.js +4 -0
- package/dist/exporter/native/index.js.map +1 -0
- package/dist/exporter/native/revision.js +54 -0
- package/dist/exporter/native/revision.js.map +1 -0
- package/dist/exporter/native/zip.js +2 -0
- package/dist/exporter/native/zip.js.map +1 -0
- package/dist/footnotes/editor.js +180 -0
- package/dist/footnotes/editor.js.map +1 -0
- package/dist/footnotes/index.js +15 -0
- package/dist/footnotes/index.js.map +1 -0
- package/dist/footnotes/layout.js +125 -0
- package/dist/footnotes/layout.js.map +1 -0
- package/dist/footnotes/state_plugins/access_rights.js +25 -0
- package/dist/footnotes/state_plugins/access_rights.js.map +1 -0
- package/dist/footnotes/state_plugins/index.js +2 -0
- package/dist/footnotes/state_plugins/index.js.map +1 -0
- package/dist/images/edit_dialog/edit_dialog/index.js +186 -0
- package/dist/images/edit_dialog/edit_dialog/index.js.map +1 -0
- package/dist/images/edit_dialog/edit_dialog/model.js +152 -0
- package/dist/images/edit_dialog/edit_dialog/model.js.map +1 -0
- package/dist/images/edit_dialog/edit_dialog/templates.js +33 -0
- package/dist/images/edit_dialog/edit_dialog/templates.js.map +1 -0
- package/dist/images/edit_dialog/model.js +6 -0
- package/dist/images/edit_dialog/model.js.map +1 -0
- package/dist/images/selection_dialog/index.js +149 -0
- package/dist/images/selection_dialog/index.js.map +1 -0
- package/dist/index.js +1002 -0
- package/dist/index.js.map +1 -0
- package/dist/keymap.js +73 -0
- package/dist/keymap.js.map +1 -0
- package/dist/marginboxes/index.js +674 -0
- package/dist/marginboxes/index.js.map +1 -0
- package/dist/marginboxes/templates.js +447 -0
- package/dist/marginboxes/templates.js.map +1 -0
- package/dist/menus/code_block/index.js +2 -0
- package/dist/menus/code_block/index.js.map +1 -0
- package/dist/menus/code_block/model.js +49 -0
- package/dist/menus/code_block/model.js.map +1 -0
- package/dist/menus/figure/index.js +3 -0
- package/dist/menus/figure/index.js.map +1 -0
- package/dist/menus/figure/model.js +80 -0
- package/dist/menus/figure/model.js.map +1 -0
- package/dist/menus/figure/utils.js +6 -0
- package/dist/menus/figure/utils.js.map +1 -0
- package/dist/menus/headerbar/index.js +3 -0
- package/dist/menus/headerbar/index.js.map +1 -0
- package/dist/menus/headerbar/model.js +743 -0
- package/dist/menus/headerbar/model.js.map +1 -0
- package/dist/menus/headerbar/view.js +579 -0
- package/dist/menus/headerbar/view.js.map +1 -0
- package/dist/menus/image/index.js +2 -0
- package/dist/menus/image/index.js.map +1 -0
- package/dist/menus/image/model.js +32 -0
- package/dist/menus/image/model.js.map +1 -0
- package/dist/menus/index.js +10 -0
- package/dist/menus/index.js.map +1 -0
- package/dist/menus/navigator/index.js +2 -0
- package/dist/menus/navigator/index.js.map +1 -0
- package/dist/menus/navigator/model.js +29 -0
- package/dist/menus/navigator/model.js.map +1 -0
- package/dist/menus/ordered_list/index.js +2 -0
- package/dist/menus/ordered_list/index.js.map +1 -0
- package/dist/menus/ordered_list/model.js +17 -0
- package/dist/menus/ordered_list/model.js.map +1 -0
- package/dist/menus/selection/index.js +3 -0
- package/dist/menus/selection/index.js.map +1 -0
- package/dist/menus/selection/model.js +108 -0
- package/dist/menus/selection/model.js.map +1 -0
- package/dist/menus/selection/view.js +125 -0
- package/dist/menus/selection/view.js.map +1 -0
- package/dist/menus/table/index.js +2 -0
- package/dist/menus/table/index.js.map +1 -0
- package/dist/menus/table/model.js +319 -0
- package/dist/menus/table/model.js.map +1 -0
- package/dist/menus/toolbar/index.js +3 -0
- package/dist/menus/toolbar/index.js.map +1 -0
- package/dist/menus/toolbar/model.js +747 -0
- package/dist/menus/toolbar/model.js.map +1 -0
- package/dist/menus/toolbar/view.js +314 -0
- package/dist/menus/toolbar/view.js.map +1 -0
- package/dist/navigator/index.js +435 -0
- package/dist/navigator/index.js.map +1 -0
- package/dist/no_collab_save/index.js +169 -0
- package/dist/no_collab_save/index.js.map +1 -0
- package/dist/plugins/citation_dialog/index.js +2 -0
- package/dist/plugins/citation_dialog/index.js.map +1 -0
- package/dist/plugins/editor/index.js +2 -0
- package/dist/plugins/editor/index.js.map +1 -0
- package/dist/state_plugins/access_rights.js +30 -0
- package/dist/state_plugins/access_rights.js.map +1 -0
- package/dist/state_plugins/citation_render.js +78 -0
- package/dist/state_plugins/citation_render.js.map +1 -0
- package/dist/state_plugins/clipboard.js +217 -0
- package/dist/state_plugins/clipboard.js.map +1 -0
- package/dist/state_plugins/code_block.js +165 -0
- package/dist/state_plugins/code_block.js.map +1 -0
- package/dist/state_plugins/collab_carets.js +115 -0
- package/dist/state_plugins/collab_carets.js.map +1 -0
- package/dist/state_plugins/comments.js +197 -0
- package/dist/state_plugins/comments.js.map +1 -0
- package/dist/state_plugins/contributor_input/add_button.js +101 -0
- package/dist/state_plugins/contributor_input/add_button.js.map +1 -0
- package/dist/state_plugins/contributor_input/dropup.js +273 -0
- package/dist/state_plugins/contributor_input/dropup.js.map +1 -0
- package/dist/state_plugins/contributor_input/helpers.js +23 -0
- package/dist/state_plugins/contributor_input/helpers.js.map +1 -0
- package/dist/state_plugins/contributor_input/index.js +3 -0
- package/dist/state_plugins/contributor_input/index.js.map +1 -0
- package/dist/state_plugins/contributor_input/node_view.js +76 -0
- package/dist/state_plugins/contributor_input/node_view.js.map +1 -0
- package/dist/state_plugins/contributor_input/plugin.js +236 -0
- package/dist/state_plugins/contributor_input/plugin.js.map +1 -0
- package/dist/state_plugins/document_template.js +213 -0
- package/dist/state_plugins/document_template.js.map +1 -0
- package/dist/state_plugins/figure.js +90 -0
- package/dist/state_plugins/figure.js.map +1 -0
- package/dist/state_plugins/footnote_markers.js +187 -0
- package/dist/state_plugins/footnote_markers.js.map +1 -0
- package/dist/state_plugins/headerbar.js +10 -0
- package/dist/state_plugins/headerbar.js.map +1 -0
- package/dist/state_plugins/index.js +27 -0
- package/dist/state_plugins/index.js.map +1 -0
- package/dist/state_plugins/inline_math.js +719 -0
- package/dist/state_plugins/inline_math.js.map +1 -0
- package/dist/state_plugins/inline_reference/dropup.js +182 -0
- package/dist/state_plugins/inline_reference/dropup.js.map +1 -0
- package/dist/state_plugins/inline_reference/index.js +2 -0
- package/dist/state_plugins/inline_reference/index.js.map +1 -0
- package/dist/state_plugins/inline_reference/plugin.js +1134 -0
- package/dist/state_plugins/inline_reference/plugin.js.map +1 -0
- package/dist/state_plugins/jump_hidden_nodes.js +47 -0
- package/dist/state_plugins/jump_hidden_nodes.js.map +1 -0
- package/dist/state_plugins/links.js +622 -0
- package/dist/state_plugins/links.js.map +1 -0
- package/dist/state_plugins/marginboxes.js +13 -0
- package/dist/state_plugins/marginboxes.js.map +1 -0
- package/dist/state_plugins/ordered_list_menu.js +72 -0
- package/dist/state_plugins/ordered_list_menu.js.map +1 -0
- package/dist/state_plugins/placeholders.js +98 -0
- package/dist/state_plugins/placeholders.js.map +1 -0
- package/dist/state_plugins/search.js +212 -0
- package/dist/state_plugins/search.js.map +1 -0
- package/dist/state_plugins/selection_menu.js +10 -0
- package/dist/state_plugins/selection_menu.js.map +1 -0
- package/dist/state_plugins/settings.js +191 -0
- package/dist/state_plugins/settings.js.map +1 -0
- package/dist/state_plugins/table.js +104 -0
- package/dist/state_plugins/table.js.map +1 -0
- package/dist/state_plugins/tag_input/helpers.js +55 -0
- package/dist/state_plugins/tag_input/helpers.js.map +1 -0
- package/dist/state_plugins/tag_input/index.js +3 -0
- package/dist/state_plugins/tag_input/index.js.map +1 -0
- package/dist/state_plugins/tag_input/node_view.js +77 -0
- package/dist/state_plugins/tag_input/node_view.js.map +1 -0
- package/dist/state_plugins/tag_input/plugin.js +199 -0
- package/dist/state_plugins/tag_input/plugin.js.map +1 -0
- package/dist/state_plugins/tag_input/tag_editor.js +159 -0
- package/dist/state_plugins/tag_input/tag_editor.js.map +1 -0
- package/dist/state_plugins/tag_input/tag_editor_plugins.js +61 -0
- package/dist/state_plugins/tag_input/tag_editor_plugins.js.map +1 -0
- package/dist/state_plugins/toc_render.js +142 -0
- package/dist/state_plugins/toc_render.js.map +1 -0
- package/dist/state_plugins/toolbar.js +10 -0
- package/dist/state_plugins/toolbar.js.map +1 -0
- package/dist/state_plugins/track/find_selected_changes.js +82 -0
- package/dist/state_plugins/track/find_selected_changes.js.map +1 -0
- package/dist/state_plugins/track/helpers.js +67 -0
- package/dist/state_plugins/track/helpers.js.map +1 -0
- package/dist/state_plugins/track/index.js +3 -0
- package/dist/state_plugins/track/index.js.map +1 -0
- package/dist/state_plugins/track/plugin.js +114 -0
- package/dist/state_plugins/track/plugin.js.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/key_bindings/index.js +20 -0
- package/dist/tools/key_bindings/index.js.map +1 -0
- package/dist/tools/key_bindings/templates.js +68 -0
- package/dist/tools/key_bindings/templates.js.map +1 -0
- package/dist/tools/search_replace/index.js +275 -0
- package/dist/tools/search_replace/index.js.map +1 -0
- package/dist/tools/search_replace/templates.js +18 -0
- package/dist/tools/search_replace/templates.js.map +1 -0
- package/dist/tools/word_count/index.js +40 -0
- package/dist/tools/word_count/index.js.map +1 -0
- package/dist/tools/word_count/templates.js +27 -0
- package/dist/tools/word_count/templates.js.map +1 -0
- package/dist/track/accept.js +66 -0
- package/dist/track/accept.js.map +1 -0
- package/dist/track/accept_all.js +51 -0
- package/dist/track/accept_all.js.map +1 -0
- package/dist/track/accept_all_no_insertions.js +63 -0
- package/dist/track/accept_all_no_insertions.js.map +1 -0
- package/dist/track/amend_transaction.js +390 -0
- package/dist/track/amend_transaction.js.map +1 -0
- package/dist/track/delete.js +62 -0
- package/dist/track/delete.js.map +1 -0
- package/dist/track/index.js +6 -0
- package/dist/track/index.js.map +1 -0
- package/dist/track/mod.js +86 -0
- package/dist/track/mod.js.map +1 -0
- package/dist/track/reject.js +67 -0
- package/dist/track/reject.js.map +1 -0
- package/dist/track/reject_all.js +56 -0
- package/dist/track/reject_all.js.map +1 -0
- package/package.json +80 -0
- package/src/bibliography/tools.js +58 -0
- package/src/citations/index.js +443 -0
- package/src/citations/render.js +68 -0
- package/src/clipboard/copy/index.js +1 -0
- package/src/clipboard/copy/schema.js +62 -0
- package/src/clipboard/copy/serializers.js +179 -0
- package/src/clipboard/paste/fidus_writer.js +55 -0
- package/src/clipboard/paste/general.js +80 -0
- package/src/clipboard/paste/google_docs.js +26 -0
- package/src/clipboard/paste/html.js +76 -0
- package/src/clipboard/paste/index.js +2 -0
- package/src/clipboard/paste/libreoffice_writer.js +32 -0
- package/src/clipboard/paste/microsoft_word.js +87 -0
- package/src/clipboard/paste/text.js +58 -0
- package/src/collab/chat.js +195 -0
- package/src/collab/colors.js +67 -0
- package/src/collab/doc.js +1436 -0
- package/src/collab/index.js +81 -0
- package/src/collab/merge/changeset.js +103 -0
- package/src/collab/merge/editor.js +1065 -0
- package/src/collab/merge/footnotes.js +175 -0
- package/src/collab/merge/index.js +401 -0
- package/src/collab/merge/recreate_transform.js +326 -0
- package/src/collab/merge/schema.js +135 -0
- package/src/collab/merge/state_plugins/action.js +171 -0
- package/src/collab/merge/state_plugins/clipboard.js +32 -0
- package/src/collab/merge/state_plugins/diff.js +780 -0
- package/src/collab/merge/state_plugins/index.js +2 -0
- package/src/collab/merge/tools.js +217 -0
- package/src/collab/templates.js +10 -0
- package/src/comments/comment.js +29 -0
- package/src/comments/editors/answer.js +55 -0
- package/src/comments/editors/comment.js +318 -0
- package/src/comments/editors/index.js +4 -0
- package/src/comments/editors/notify.js +13 -0
- package/src/comments/editors/schema.js +65 -0
- package/src/comments/index.js +11 -0
- package/src/comments/interactions.js +465 -0
- package/src/comments/store.js +513 -0
- package/src/contacts/add_dialog.js +110 -0
- package/src/contacts/templates.js +28 -0
- package/src/copyright_dialog/index.js +179 -0
- package/src/copyright_dialog/templates.js +56 -0
- package/src/databases/bibliography.js +179 -0
- package/src/databases/images.js +320 -0
- package/src/databases/index.js +69 -0
- package/src/dialogs/citation.js +428 -0
- package/src/dialogs/code_block.js +242 -0
- package/src/dialogs/contributor.js +173 -0
- package/src/dialogs/figure.js +479 -0
- package/src/dialogs/index.js +10 -0
- package/src/dialogs/language.js +58 -0
- package/src/dialogs/link.js +358 -0
- package/src/dialogs/math.js +148 -0
- package/src/dialogs/ordered_list.js +84 -0
- package/src/dialogs/revision.js +43 -0
- package/src/dialogs/subsup.js +116 -0
- package/src/dialogs/table.js +258 -0
- package/src/dialogs/templates.js +426 -0
- package/src/document_template/exporter.js +97 -0
- package/src/document_template/index.js +483 -0
- package/src/documents/access_rights/index.js +693 -0
- package/src/documents/access_rights/templates.js +178 -0
- package/src/e2ee/encryptor.js +228 -0
- package/src/e2ee/key-manager.js +202 -0
- package/src/e2ee/passphrase-crypto.js +606 -0
- package/src/e2ee/passphrase-dialog.js +655 -0
- package/src/e2ee/passphrase-manager.js +502 -0
- package/src/e2ee/password-dialog.js +652 -0
- package/src/e2ee/snapshot-manager.js +305 -0
- package/src/exporter/native/copy.js +54 -0
- package/src/exporter/native/file.js +37 -0
- package/src/exporter/native/index.js +3 -0
- package/src/exporter/native/revision.js +86 -0
- package/src/exporter/native/zip.js +1 -0
- package/src/footnotes/editor.js +227 -0
- package/src/footnotes/index.js +16 -0
- package/src/footnotes/layout.js +190 -0
- package/src/footnotes/state_plugins/access_rights.js +39 -0
- package/src/footnotes/state_plugins/index.js +1 -0
- package/src/images/edit_dialog/edit_dialog/index.js +222 -0
- package/src/images/edit_dialog/edit_dialog/model.js +183 -0
- package/src/images/edit_dialog/edit_dialog/templates.js +41 -0
- package/src/images/edit_dialog/model.js +5 -0
- package/src/images/selection_dialog/index.js +176 -0
- package/src/index.js +1252 -0
- package/src/keymap.js +102 -0
- package/src/marginboxes/index.js +946 -0
- package/src/marginboxes/templates.js +573 -0
- package/src/menus/code_block/index.js +1 -0
- package/src/menus/code_block/model.js +52 -0
- package/src/menus/figure/index.js +2 -0
- package/src/menus/figure/model.js +91 -0
- package/src/menus/figure/utils.js +5 -0
- package/src/menus/headerbar/index.js +2 -0
- package/src/menus/headerbar/model.js +1077 -0
- package/src/menus/headerbar/view.js +675 -0
- package/src/menus/image/index.js +1 -0
- package/src/menus/image/model.js +32 -0
- package/src/menus/index.js +9 -0
- package/src/menus/navigator/index.js +1 -0
- package/src/menus/navigator/model.js +28 -0
- package/src/menus/ordered_list/index.js +1 -0
- package/src/menus/ordered_list/model.js +19 -0
- package/src/menus/selection/index.js +2 -0
- package/src/menus/selection/model.js +134 -0
- package/src/menus/selection/view.js +154 -0
- package/src/menus/table/index.js +1 -0
- package/src/menus/table/model.js +414 -0
- package/src/menus/toolbar/index.js +2 -0
- package/src/menus/toolbar/model.js +920 -0
- package/src/menus/toolbar/view.js +354 -0
- package/src/navigator/index.js +515 -0
- package/src/no_collab_save/index.js +209 -0
- package/src/plugins/citation_dialog/index.js +1 -0
- package/src/plugins/editor/index.js +1 -0
- package/src/state_plugins/access_rights.js +47 -0
- package/src/state_plugins/citation_render.js +88 -0
- package/src/state_plugins/clipboard.js +289 -0
- package/src/state_plugins/code_block.js +201 -0
- package/src/state_plugins/collab_carets.js +152 -0
- package/src/state_plugins/comments.js +259 -0
- package/src/state_plugins/contributor_input/add_button.js +134 -0
- package/src/state_plugins/contributor_input/dropup.js +318 -0
- package/src/state_plugins/contributor_input/helpers.js +24 -0
- package/src/state_plugins/contributor_input/index.js +2 -0
- package/src/state_plugins/contributor_input/node_view.js +86 -0
- package/src/state_plugins/contributor_input/plugin.js +318 -0
- package/src/state_plugins/document_template.js +315 -0
- package/src/state_plugins/figure.js +108 -0
- package/src/state_plugins/footnote_markers.js +238 -0
- package/src/state_plugins/headerbar.js +12 -0
- package/src/state_plugins/index.js +73 -0
- package/src/state_plugins/inline_math.js +918 -0
- package/src/state_plugins/inline_reference/dropup.js +208 -0
- package/src/state_plugins/inline_reference/index.js +5 -0
- package/src/state_plugins/inline_reference/plugin.js +1429 -0
- package/src/state_plugins/jump_hidden_nodes.js +58 -0
- package/src/state_plugins/links.js +830 -0
- package/src/state_plugins/marginboxes.js +14 -0
- package/src/state_plugins/ordered_list_menu.js +84 -0
- package/src/state_plugins/placeholders.js +123 -0
- package/src/state_plugins/search.js +251 -0
- package/src/state_plugins/selection_menu.js +12 -0
- package/src/state_plugins/settings.js +223 -0
- package/src/state_plugins/table.js +127 -0
- package/src/state_plugins/tag_input/helpers.js +69 -0
- package/src/state_plugins/tag_input/index.js +2 -0
- package/src/state_plugins/tag_input/node_view.js +85 -0
- package/src/state_plugins/tag_input/plugin.js +278 -0
- package/src/state_plugins/tag_input/tag_editor.js +202 -0
- package/src/state_plugins/tag_input/tag_editor_plugins.js +74 -0
- package/src/state_plugins/toc_render.js +160 -0
- package/src/state_plugins/toolbar.js +12 -0
- package/src/state_plugins/track/find_selected_changes.js +107 -0
- package/src/state_plugins/track/helpers.js +108 -0
- package/src/state_plugins/track/index.js +6 -0
- package/src/state_plugins/track/plugin.js +140 -0
- package/src/tools/index.js +3 -0
- package/src/tools/key_bindings/index.js +21 -0
- package/src/tools/key_bindings/templates.js +68 -0
- package/src/tools/search_replace/index.js +421 -0
- package/src/tools/search_replace/templates.js +19 -0
- package/src/tools/word_count/index.js +59 -0
- package/src/tools/word_count/templates.js +34 -0
- package/src/track/accept.js +102 -0
- package/src/track/accept_all.js +103 -0
- package/src/track/accept_all_no_insertions.js +102 -0
- package/src/track/amend_transaction.js +562 -0
- package/src/track/delete.js +92 -0
- package/src/track/index.js +8 -0
- package/src/track/mod.js +116 -0
- package/src/track/reject.js +116 -0
- package/src/track/reject_all.js +121 -0
|
@@ -0,0 +1,1436 @@
|
|
|
1
|
+
import {SchemaExport} from "@fiduswriter/document/schema/export"
|
|
2
|
+
import {Dialog, activateWait, deactivateWait, makeWorker} from "fwtoolkit"
|
|
3
|
+
import {receiveTransaction, sendableSteps} from "prosemirror-collab"
|
|
4
|
+
import {EditorState} from "prosemirror-state"
|
|
5
|
+
import {Step} from "prosemirror-transform"
|
|
6
|
+
import {E2EEEncryptor} from "../e2ee/encryptor"
|
|
7
|
+
import {E2EEKeyManager} from "../e2ee/key-manager"
|
|
8
|
+
import {enterPassphraseDialog} from "../e2ee/passphrase-dialog"
|
|
9
|
+
import {PassphraseManager} from "../e2ee/passphrase-manager"
|
|
10
|
+
import {enterPasswordDialog} from "../e2ee/password-dialog"
|
|
11
|
+
import {
|
|
12
|
+
getSelectionUpdate,
|
|
13
|
+
removeCollaboratorSelection,
|
|
14
|
+
updateCollaboratorSelection
|
|
15
|
+
} from "../state_plugins"
|
|
16
|
+
import {Merge} from "./merge"
|
|
17
|
+
|
|
18
|
+
export class ModCollabDoc {
|
|
19
|
+
constructor(mod) {
|
|
20
|
+
mod.doc = this
|
|
21
|
+
this.mod = mod
|
|
22
|
+
this.merge = new Merge(mod)
|
|
23
|
+
this.unconfirmedDiffs = {}
|
|
24
|
+
this.confirmStepsRequestCounter = 0
|
|
25
|
+
this.awaitingDiffResponse = false
|
|
26
|
+
this.receiving = false
|
|
27
|
+
this.currentlyCheckingVersion = false
|
|
28
|
+
this.footnoteRender = false // If the offline user edited a footnote , it needs to be rendered properly to connected users too!
|
|
29
|
+
this.initialVersionConfirmed = false
|
|
30
|
+
this.initialDocLoaded = false
|
|
31
|
+
this.templateAdjustmentPending = false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
cancelCurrentlyCheckingVersion() {
|
|
35
|
+
this.currentlyCheckingVersion = false
|
|
36
|
+
window.clearTimeout(this.enableCheckVersion)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
checkVersion(offline = false) {
|
|
40
|
+
// Guard: WebSocket may not be initialized yet (e.g. during
|
|
41
|
+
// loadDocument before the WS connector is created).
|
|
42
|
+
if (!this.mod.editor.ws) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
this.mod.editor.ws.send(() => {
|
|
46
|
+
if (
|
|
47
|
+
this.currentlyCheckingVersion ||
|
|
48
|
+
this.mod.editor.docInfo.version === undefined
|
|
49
|
+
) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
this.currentlyCheckingVersion = true
|
|
53
|
+
this.enableCheckVersion = window.setTimeout(() => {
|
|
54
|
+
this.currentlyCheckingVersion = false
|
|
55
|
+
}, 1000)
|
|
56
|
+
if (this.mod.editor.ws.connected) {
|
|
57
|
+
this.disableDiffSending()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const msg = {
|
|
61
|
+
type: "check_version",
|
|
62
|
+
v: this.mod.editor.docInfo.version
|
|
63
|
+
}
|
|
64
|
+
if (offline) {
|
|
65
|
+
msg.offline = true
|
|
66
|
+
}
|
|
67
|
+
return msg
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
disableDiffSending() {
|
|
72
|
+
this.awaitingDiffResponse = true
|
|
73
|
+
// If no answer has been received from the server within 2 seconds,
|
|
74
|
+
// check the version
|
|
75
|
+
this.sendNextDiffTimer = window.setTimeout(() => {
|
|
76
|
+
this.awaitingDiffResponse = false
|
|
77
|
+
this.sendToCollaborators()
|
|
78
|
+
}, 8000)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
enableDiffSending() {
|
|
82
|
+
window.clearTimeout(this.sendNextDiffTimer)
|
|
83
|
+
this.awaitingDiffResponse = false
|
|
84
|
+
this.sendToCollaborators()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
receiveDocument(data) {
|
|
88
|
+
this.cancelCurrentlyCheckingVersion()
|
|
89
|
+
if (
|
|
90
|
+
this.mod.editor.docInfo.confirmedDoc &&
|
|
91
|
+
!this.mod.editor.e2ee?.encrypted
|
|
92
|
+
) {
|
|
93
|
+
this.merge.adjustDocument(data)
|
|
94
|
+
} else {
|
|
95
|
+
this.loadDocument(data)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
finishInitialLoad() {
|
|
100
|
+
if (
|
|
101
|
+
this.initialDocLoaded &&
|
|
102
|
+
this.initialVersionConfirmed &&
|
|
103
|
+
!this.templateAdjustmentPending
|
|
104
|
+
) {
|
|
105
|
+
deactivateWait()
|
|
106
|
+
this.mod.editor.waitingForDocument = false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
confirmVersion(version) {
|
|
111
|
+
if (!this.mod.editor.docInfo.confirmedDoc) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (!this.initialVersionConfirmed) {
|
|
115
|
+
this.initialVersionConfirmed = true
|
|
116
|
+
if (version === this.mod.editor.docInfo.version) {
|
|
117
|
+
this.finishInitialLoad()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
loadDocument({doc, time, doc_info}) {
|
|
123
|
+
const isInitialLoad = !this.mod.editor.docInfo.confirmedDoc
|
|
124
|
+
// Reset collaboration
|
|
125
|
+
this.unconfirmedDiffs = {}
|
|
126
|
+
if (this.awaitingDiffResponse) {
|
|
127
|
+
this.enableDiffSending()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.mod.editor.clientTimeAdjustment = Date.now() - time
|
|
131
|
+
|
|
132
|
+
const token = this.mod.editor.docInfo.token
|
|
133
|
+
this.mod.editor.docInfo = doc_info
|
|
134
|
+
this.mod.editor.docInfo.token = token
|
|
135
|
+
// For guests, update user object with the token UUID (stable identity)
|
|
136
|
+
// and session_id (for display). Token UUID persists across reconnections.
|
|
137
|
+
// session_id may not be present when loading from REST (it comes via
|
|
138
|
+
// the session_info WebSocket message). Only update if available.
|
|
139
|
+
if (
|
|
140
|
+
!this.mod.editor.user.is_authenticated &&
|
|
141
|
+
doc_info.session_id !== undefined
|
|
142
|
+
) {
|
|
143
|
+
const sessionId = doc_info.session_id
|
|
144
|
+
this.mod.editor.user = {
|
|
145
|
+
id: token, // stable — same token UUID even after reconnect
|
|
146
|
+
username: `guest${sessionId}`, // display name per session
|
|
147
|
+
name: `Guest ${sessionId}`,
|
|
148
|
+
is_authenticated: false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.mod.editor.docInfo.token = token
|
|
152
|
+
this.mod.editor.docInfo.version = doc.v
|
|
153
|
+
this.mod.editor.docInfo.updated = new Date()
|
|
154
|
+
|
|
155
|
+
// Check if this is an E2EE document. If so, we need to decrypt
|
|
156
|
+
// the content before loading it into ProseMirror.
|
|
157
|
+
const isE2EE = doc_info.e2ee === true
|
|
158
|
+
if (isE2EE) {
|
|
159
|
+
this._loadE2EEDocument(doc, doc_info, isInitialLoad)
|
|
160
|
+
} else {
|
|
161
|
+
this.mod.editor.e2ee = {encrypted: false}
|
|
162
|
+
this._loadUnencryptedDocument(doc, isInitialLoad)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Load an E2EE document: try passphrase/DEK first, then fall back to
|
|
168
|
+
* per-document password prompt, decrypt, then load.
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
async _loadE2EEDocument(doc, doc_info, isInitialLoad) {
|
|
172
|
+
// Extract password from URL fragment if present (share link format:
|
|
173
|
+
// https://example.com/share/TOKEN/#target?password=PASSWORD). The fragment is never
|
|
174
|
+
// sent to the server, so this is safe.
|
|
175
|
+
const locationHash = window.location.hash
|
|
176
|
+
let urlFragmentPassword = ""
|
|
177
|
+
if (locationHash && locationHash.includes("?")) {
|
|
178
|
+
urlFragmentPassword = decodeURIComponent(
|
|
179
|
+
new URLSearchParams(locationHash.split("?")[1]).get(
|
|
180
|
+
"password"
|
|
181
|
+
) || ""
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Get the salt and iterations. These may come from the REST response
|
|
186
|
+
// (doc_info) or from the WebSocket session_info message (already
|
|
187
|
+
// stored in this.mod.editor.e2ee).
|
|
188
|
+
let salt, iterations
|
|
189
|
+
if (this.mod.editor.e2ee) {
|
|
190
|
+
salt = this.mod.editor.e2ee.encryptionSalt
|
|
191
|
+
iterations = this.mod.editor.e2ee.encryptionIterations
|
|
192
|
+
}
|
|
193
|
+
if (doc_info.e2ee_salt) {
|
|
194
|
+
salt = doc_info.e2ee_salt
|
|
195
|
+
}
|
|
196
|
+
if (doc_info.e2ee_iterations) {
|
|
197
|
+
iterations = doc_info.e2ee_iterations
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const docId = this.mod.editor.docInfo.id
|
|
201
|
+
|
|
202
|
+
// Helper to save password for passphrase users after successful decryption
|
|
203
|
+
// (migrates per-document-password docs into the passphrase system)
|
|
204
|
+
const maybeSavePasswordForPassphrase = async password => {
|
|
205
|
+
if (!PassphraseManager.hasKeysInSession()) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const existing =
|
|
210
|
+
await PassphraseManager.getDocumentPassword(docId)
|
|
211
|
+
if (!existing) {
|
|
212
|
+
await PassphraseManager.saveDocumentPassword(
|
|
213
|
+
docId,
|
|
214
|
+
password,
|
|
215
|
+
null,
|
|
216
|
+
"user",
|
|
217
|
+
true
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
} catch (_e) {
|
|
221
|
+
console.error(
|
|
222
|
+
"E2EE: Failed to save document password for passphrase:",
|
|
223
|
+
_e
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Helper to resolve a password to a key and attempt decryption
|
|
229
|
+
const tryPassword = async password => {
|
|
230
|
+
const key = await E2EEKeyManager.resolvePasswordToKey(
|
|
231
|
+
password,
|
|
232
|
+
salt
|
|
233
|
+
? new Uint8Array(
|
|
234
|
+
atob(salt)
|
|
235
|
+
.split("")
|
|
236
|
+
.map(c => c.charCodeAt(0))
|
|
237
|
+
)
|
|
238
|
+
: null,
|
|
239
|
+
iterations || 600000
|
|
240
|
+
)
|
|
241
|
+
await this._decryptAndLoadDoc(
|
|
242
|
+
doc,
|
|
243
|
+
key,
|
|
244
|
+
salt,
|
|
245
|
+
iterations,
|
|
246
|
+
isInitialLoad,
|
|
247
|
+
urlFragmentPassword
|
|
248
|
+
)
|
|
249
|
+
// Cache password and key for this session
|
|
250
|
+
E2EEKeyManager.storePasswordInSession(docId, password)
|
|
251
|
+
await E2EEKeyManager.storeKeyInSession(docId, key)
|
|
252
|
+
if (this.mod.editor.e2ee) {
|
|
253
|
+
this.mod.editor.e2ee.key = key
|
|
254
|
+
this.mod.editor.e2ee.password = password
|
|
255
|
+
}
|
|
256
|
+
// Migrate per-document-password docs to passphrase system
|
|
257
|
+
await maybeSavePasswordForPassphrase(password)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- Try passphrase path first ---
|
|
261
|
+
if (PassphraseManager.hasKeysInSession()) {
|
|
262
|
+
const password = await PassphraseManager.getDocumentPassword(docId)
|
|
263
|
+
if (password) {
|
|
264
|
+
try {
|
|
265
|
+
await tryPassword(password)
|
|
266
|
+
if (this.mod.editor.e2ee) {
|
|
267
|
+
this.mod.editor.e2ee.usesPassphrase = true
|
|
268
|
+
}
|
|
269
|
+
return
|
|
270
|
+
} catch (_error) {
|
|
271
|
+
// Password didn't work — fall through
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If passphrase keys exist but are not in session, try to unlock
|
|
277
|
+
if (!PassphraseManager.hasKeysInSession()) {
|
|
278
|
+
const hasKeys = await PassphraseManager.hasEncryptionKeys()
|
|
279
|
+
if (hasKeys) {
|
|
280
|
+
let errorMessage = ""
|
|
281
|
+
let unlocked = false
|
|
282
|
+
while (!unlocked) {
|
|
283
|
+
const result = await new Promise(resolve => {
|
|
284
|
+
enterPassphraseDialog(
|
|
285
|
+
pwd => resolve({action: "unlock", passphrase: pwd}),
|
|
286
|
+
() => resolve({action: "recover"}),
|
|
287
|
+
{errorMessage}
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
if (result.action === "unlock" && result.passphrase) {
|
|
291
|
+
try {
|
|
292
|
+
await PassphraseManager.unlockWithPassphrase(
|
|
293
|
+
result.passphrase
|
|
294
|
+
)
|
|
295
|
+
const password =
|
|
296
|
+
await PassphraseManager.getDocumentPassword(
|
|
297
|
+
docId
|
|
298
|
+
)
|
|
299
|
+
if (password) {
|
|
300
|
+
await tryPassword(password)
|
|
301
|
+
if (this.mod.editor.e2ee) {
|
|
302
|
+
this.mod.editor.e2ee.usesPassphrase = true
|
|
303
|
+
}
|
|
304
|
+
unlocked = true
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
} catch (_e) {
|
|
308
|
+
errorMessage = gettext(
|
|
309
|
+
"Incorrect passphrase. Please try again."
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
} else if (result?.action === "recover") {
|
|
313
|
+
// Recovery flow
|
|
314
|
+
const {recoverWithKeyDialog} = await import(
|
|
315
|
+
"../e2ee/passphrase-dialog.js"
|
|
316
|
+
)
|
|
317
|
+
const recoverResult = await new Promise(resolve => {
|
|
318
|
+
recoverWithKeyDialog(resolve)
|
|
319
|
+
})
|
|
320
|
+
if (recoverResult) {
|
|
321
|
+
try {
|
|
322
|
+
const {newRecoveryKey} =
|
|
323
|
+
await PassphraseManager.recoverWithRecoveryKey(
|
|
324
|
+
recoverResult.recoveryKey,
|
|
325
|
+
recoverResult.newPassphrase
|
|
326
|
+
)
|
|
327
|
+
const {showRecoveryKeyDialog} = await import(
|
|
328
|
+
"../e2ee/passphrase-dialog.js"
|
|
329
|
+
)
|
|
330
|
+
await new Promise(resolve => {
|
|
331
|
+
showRecoveryKeyDialog(
|
|
332
|
+
newRecoveryKey,
|
|
333
|
+
resolve
|
|
334
|
+
)
|
|
335
|
+
})
|
|
336
|
+
// After recovery, try to get document password again
|
|
337
|
+
const password =
|
|
338
|
+
await PassphraseManager.getDocumentPassword(
|
|
339
|
+
docId
|
|
340
|
+
)
|
|
341
|
+
if (password) {
|
|
342
|
+
await tryPassword(password)
|
|
343
|
+
if (this.mod.editor.e2ee) {
|
|
344
|
+
this.mod.editor.e2ee.usesPassphrase = true
|
|
345
|
+
}
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
} catch (e) {
|
|
349
|
+
console.error("E2EE: Recovery failed:", e)
|
|
350
|
+
const errorDialog = new Dialog({
|
|
351
|
+
title: gettext("Recovery Failed"),
|
|
352
|
+
id: "e2ee-recovery-failed",
|
|
353
|
+
body: gettext(
|
|
354
|
+
"The recovery key you entered is incorrect, or the recovery process failed. Please try again."
|
|
355
|
+
),
|
|
356
|
+
buttons: [
|
|
357
|
+
{
|
|
358
|
+
text: gettext("Retry"),
|
|
359
|
+
classes: "fw-dark",
|
|
360
|
+
click: () => {
|
|
361
|
+
errorDialog.close()
|
|
362
|
+
this._loadE2EEDocument(
|
|
363
|
+
doc,
|
|
364
|
+
doc_info,
|
|
365
|
+
isInitialLoad,
|
|
366
|
+
urlFragmentPassword
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
text: gettext("Cancel"),
|
|
372
|
+
classes: "fw-light",
|
|
373
|
+
click: () => errorDialog.close()
|
|
374
|
+
}
|
|
375
|
+
],
|
|
376
|
+
canClose: false
|
|
377
|
+
})
|
|
378
|
+
errorDialog.open()
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// If recovery was cancelled, continue the loop to show
|
|
383
|
+
// the passphrase dialog again.
|
|
384
|
+
continue
|
|
385
|
+
} else {
|
|
386
|
+
// User cancelled the passphrase dialog — break out of
|
|
387
|
+
// the loop and fall through to other password sources.
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// --- Try sessionStorage password ---
|
|
395
|
+
const sessionPassword = E2EEKeyManager.getPasswordFromSession(docId)
|
|
396
|
+
if (sessionPassword) {
|
|
397
|
+
try {
|
|
398
|
+
await tryPassword(sessionPassword)
|
|
399
|
+
return
|
|
400
|
+
} catch (_error) {
|
|
401
|
+
E2EEKeyManager.clearPasswordFromSession(docId)
|
|
402
|
+
E2EEKeyManager.clearKeyFromSession(docId)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// --- Try URL fragment password ---
|
|
407
|
+
if (urlFragmentPassword) {
|
|
408
|
+
try {
|
|
409
|
+
await tryPassword(urlFragmentPassword)
|
|
410
|
+
return
|
|
411
|
+
} catch (_error) {
|
|
412
|
+
// URL password didn't work — fall through to prompt
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// --- Try existing key (e.g. from _createE2EEDocument) ---
|
|
417
|
+
const existingKey = this.mod.editor.e2ee?.key
|
|
418
|
+
if (existingKey) {
|
|
419
|
+
try {
|
|
420
|
+
await this._decryptAndLoadDoc(
|
|
421
|
+
doc,
|
|
422
|
+
existingKey,
|
|
423
|
+
salt,
|
|
424
|
+
iterations,
|
|
425
|
+
isInitialLoad,
|
|
426
|
+
urlFragmentPassword
|
|
427
|
+
)
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error(
|
|
430
|
+
"E2EE: Decryption failed with existing key",
|
|
431
|
+
error
|
|
432
|
+
)
|
|
433
|
+
if (this.mod.editor.e2ee) {
|
|
434
|
+
this.mod.editor.e2ee.key = null
|
|
435
|
+
}
|
|
436
|
+
await this._promptPasswordAndDecrypt(
|
|
437
|
+
doc,
|
|
438
|
+
doc_info,
|
|
439
|
+
salt,
|
|
440
|
+
iterations,
|
|
441
|
+
isInitialLoad,
|
|
442
|
+
urlFragmentPassword
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// No key available — prompt for the password
|
|
449
|
+
await this._promptPasswordAndDecrypt(
|
|
450
|
+
doc,
|
|
451
|
+
doc_info,
|
|
452
|
+
salt,
|
|
453
|
+
iterations,
|
|
454
|
+
isInitialLoad,
|
|
455
|
+
urlFragmentPassword
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Prompt for a password, derive the key, and decrypt/load the document.
|
|
461
|
+
* @private
|
|
462
|
+
*/
|
|
463
|
+
async _promptPasswordAndDecrypt(
|
|
464
|
+
doc,
|
|
465
|
+
doc_info,
|
|
466
|
+
salt,
|
|
467
|
+
iterations,
|
|
468
|
+
isInitialLoad,
|
|
469
|
+
urlFragmentPassword
|
|
470
|
+
) {
|
|
471
|
+
// Decode the salt from Base64 to Uint8Array
|
|
472
|
+
let saltBytes = null
|
|
473
|
+
if (salt) {
|
|
474
|
+
const binary = atob(salt)
|
|
475
|
+
saltBytes = new Uint8Array(binary.length)
|
|
476
|
+
for (let i = 0; i < binary.length; i++) {
|
|
477
|
+
saltBytes[i] = binary.charCodeAt(i)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const goBack = () => {
|
|
482
|
+
const folderPath = this.mod.editor.docInfo.path.slice(
|
|
483
|
+
0,
|
|
484
|
+
this.mod.editor.docInfo.path.lastIndexOf("/")
|
|
485
|
+
)
|
|
486
|
+
if (this.mod.editor.app && this.mod.editor.app.goTo) {
|
|
487
|
+
if (!folderPath.length) {
|
|
488
|
+
this.mod.editor.app.goTo("/")
|
|
489
|
+
} else {
|
|
490
|
+
this.mod.editor.app.goTo(`/documents${folderPath}/`)
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
window.location.href = "/"
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
await enterPasswordDialog(
|
|
498
|
+
async password => {
|
|
499
|
+
try {
|
|
500
|
+
const key = await E2EEKeyManager.resolvePasswordToKey(
|
|
501
|
+
password,
|
|
502
|
+
saltBytes,
|
|
503
|
+
iterations || 600000
|
|
504
|
+
)
|
|
505
|
+
await this._decryptAndLoadDoc(
|
|
506
|
+
doc,
|
|
507
|
+
key,
|
|
508
|
+
salt,
|
|
509
|
+
iterations,
|
|
510
|
+
isInitialLoad,
|
|
511
|
+
urlFragmentPassword
|
|
512
|
+
)
|
|
513
|
+
// Cache password and key for this session
|
|
514
|
+
E2EEKeyManager.storePasswordInSession(
|
|
515
|
+
this.mod.editor.docInfo.id,
|
|
516
|
+
password
|
|
517
|
+
)
|
|
518
|
+
await E2EEKeyManager.storeKeyInSession(
|
|
519
|
+
this.mod.editor.docInfo.id,
|
|
520
|
+
key
|
|
521
|
+
)
|
|
522
|
+
if (this.mod.editor.e2ee) {
|
|
523
|
+
this.mod.editor.e2ee.key = key
|
|
524
|
+
this.mod.editor.e2ee.password = password
|
|
525
|
+
}
|
|
526
|
+
// Migrate per-document-password docs to passphrase system
|
|
527
|
+
if (PassphraseManager.hasKeysInSession()) {
|
|
528
|
+
try {
|
|
529
|
+
const existing =
|
|
530
|
+
await PassphraseManager.getDocumentPassword(
|
|
531
|
+
this.mod.editor.docInfo.id
|
|
532
|
+
)
|
|
533
|
+
if (!existing) {
|
|
534
|
+
await PassphraseManager.saveDocumentPassword(
|
|
535
|
+
this.mod.editor.docInfo.id,
|
|
536
|
+
password,
|
|
537
|
+
null,
|
|
538
|
+
"user",
|
|
539
|
+
true
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
} catch (_e) {
|
|
543
|
+
console.error(
|
|
544
|
+
"E2EE: Failed to save document password for passphrase:",
|
|
545
|
+
_e
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch (_error) {
|
|
550
|
+
console.error("E2EE DECRYPT ERROR:", _error)
|
|
551
|
+
const errorDialog = new Dialog({
|
|
552
|
+
title: gettext("Decryption Failed"),
|
|
553
|
+
id: "e2ee-decryption-failed",
|
|
554
|
+
body: gettext(
|
|
555
|
+
"That password didn't work. Please try again."
|
|
556
|
+
),
|
|
557
|
+
buttons: [
|
|
558
|
+
{
|
|
559
|
+
text: gettext("Retry"),
|
|
560
|
+
classes: "fw-dark",
|
|
561
|
+
click: () => {
|
|
562
|
+
this._promptPasswordAndDecrypt(
|
|
563
|
+
doc,
|
|
564
|
+
doc_info,
|
|
565
|
+
salt,
|
|
566
|
+
iterations,
|
|
567
|
+
isInitialLoad,
|
|
568
|
+
""
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
text: gettext("Cancel"),
|
|
574
|
+
classes: "fw-light",
|
|
575
|
+
click: () => {
|
|
576
|
+
errorDialog.close()
|
|
577
|
+
goBack()
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
],
|
|
581
|
+
canClose: false
|
|
582
|
+
})
|
|
583
|
+
errorDialog.open()
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
urlFragmentPassword,
|
|
587
|
+
goBack
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Decrypt document content/comments/bibliography with the given key
|
|
593
|
+
* and load into ProseMirror.
|
|
594
|
+
*
|
|
595
|
+
* For a newly created E2EE document, no encrypted snapshot has been
|
|
596
|
+
* saved yet, so the content/comments/bibliography are still plaintext
|
|
597
|
+
* JSON objects (the template content). We detect this by checking
|
|
598
|
+
* whether the value is a string (encrypted = Base64 ciphertext) or
|
|
599
|
+
* an object (plaintext).
|
|
600
|
+
*
|
|
601
|
+
* @private
|
|
602
|
+
*/
|
|
603
|
+
async _decryptAndLoadDoc(
|
|
604
|
+
doc,
|
|
605
|
+
key,
|
|
606
|
+
salt,
|
|
607
|
+
iterations,
|
|
608
|
+
isInitialLoad,
|
|
609
|
+
urlFragmentPassword
|
|
610
|
+
) {
|
|
611
|
+
let decryptedContent = doc.content
|
|
612
|
+
if (typeof doc.content === "string") {
|
|
613
|
+
decryptedContent = await E2EEEncryptor.decryptObject(
|
|
614
|
+
doc.content,
|
|
615
|
+
key
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
let decryptedComments = doc.comments
|
|
620
|
+
if (typeof doc.comments === "string") {
|
|
621
|
+
decryptedComments = await E2EEEncryptor.decryptObject(
|
|
622
|
+
doc.comments,
|
|
623
|
+
key
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let decryptedBibliography = doc.bibliography
|
|
628
|
+
if (typeof doc.bibliography === "string") {
|
|
629
|
+
decryptedBibliography = await E2EEEncryptor.decryptObject(
|
|
630
|
+
doc.bibliography,
|
|
631
|
+
key
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Store the E2EE state on the editor
|
|
636
|
+
this.mod.editor.e2ee = {
|
|
637
|
+
encrypted: true,
|
|
638
|
+
encryptionSalt: salt,
|
|
639
|
+
encryptionIterations: iterations || 600000,
|
|
640
|
+
key: key,
|
|
641
|
+
snapshotManager: this.mod.editor.e2ee?.snapshotManager || null
|
|
642
|
+
}
|
|
643
|
+
// Initialize snapshot manager now that we have the key
|
|
644
|
+
if (!this.mod.editor.e2ee.snapshotManager) {
|
|
645
|
+
const {E2EESnapshotManager} = await import(
|
|
646
|
+
"../e2ee/snapshot-manager"
|
|
647
|
+
)
|
|
648
|
+
this.mod.editor.e2ee.snapshotManager = new E2EESnapshotManager(
|
|
649
|
+
this.mod.editor
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
this.mod.editor.e2ee.snapshotManager.setKey(key)
|
|
653
|
+
|
|
654
|
+
// Cache the key in sessionStorage so the user doesn't have to
|
|
655
|
+
// re-enter the password when reopening the document this session.
|
|
656
|
+
try {
|
|
657
|
+
await E2EEKeyManager.storeKeyInSession(
|
|
658
|
+
this.mod.editor.docInfo.id,
|
|
659
|
+
key
|
|
660
|
+
)
|
|
661
|
+
} catch (_e) {
|
|
662
|
+
// If the key is non-extractable, we can't store it.
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Remove the password from the URL fragment to avoid
|
|
666
|
+
// leaving it in the address bar or browser history.
|
|
667
|
+
if (urlFragmentPassword && window.history.replaceState) {
|
|
668
|
+
const hash = window.location.hash // "#title?password=fish&targetting=true"
|
|
669
|
+
const [anchor, queryString] = hash.split("?") // ["#title", "password=fish&targetting=true"]
|
|
670
|
+
|
|
671
|
+
const params = new URLSearchParams(queryString || "")
|
|
672
|
+
params.delete("password") // Removes "password" if it exists
|
|
673
|
+
|
|
674
|
+
const newQueryString = params.toString()
|
|
675
|
+
const newHash = newQueryString
|
|
676
|
+
? `${anchor}?${newQueryString}`
|
|
677
|
+
: anchor
|
|
678
|
+
|
|
679
|
+
window.history.replaceState(null, "", newHash)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Decrypt image copyright metadata for E2EE documents.
|
|
683
|
+
// The server stores it as an opaque encrypted string; we decrypt
|
|
684
|
+
// it here so the image DB receives plaintext objects.
|
|
685
|
+
let decryptedImages = doc.images
|
|
686
|
+
if (doc.images) {
|
|
687
|
+
decryptedImages = {}
|
|
688
|
+
const {E2EEEncryptor} = await import("../e2ee/encryptor")
|
|
689
|
+
for (const [id, image] of Object.entries(doc.images)) {
|
|
690
|
+
decryptedImages[id] = image
|
|
691
|
+
if (typeof image.copyright === "string") {
|
|
692
|
+
try {
|
|
693
|
+
decryptedImages[id].copyright =
|
|
694
|
+
await E2EEEncryptor.decryptObject(
|
|
695
|
+
image.copyright,
|
|
696
|
+
key
|
|
697
|
+
)
|
|
698
|
+
} catch (_e) {
|
|
699
|
+
// If decryption fails, leave as-is (legacy plaintext)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Now load the decrypted document using the standard path
|
|
706
|
+
const decryptedDoc = {
|
|
707
|
+
...doc,
|
|
708
|
+
content: decryptedContent,
|
|
709
|
+
comments: decryptedComments,
|
|
710
|
+
bibliography: decryptedBibliography,
|
|
711
|
+
images: decryptedImages
|
|
712
|
+
}
|
|
713
|
+
this._loadUnencryptedDocument(decryptedDoc, isInitialLoad)
|
|
714
|
+
|
|
715
|
+
// Cache the decrypted title in sessionStorage so the document overview
|
|
716
|
+
// can display the real title without prompting for the password again.
|
|
717
|
+
let title = ""
|
|
718
|
+
this.mod.editor.view.state.doc.firstChild.forEach(child => {
|
|
719
|
+
if (!child.marks.find(mark => mark.type.name === "deletion")) {
|
|
720
|
+
title += child.textContent
|
|
721
|
+
}
|
|
722
|
+
})
|
|
723
|
+
sessionStorage.setItem(
|
|
724
|
+
`e2ee_title_${this.mod.editor.docInfo.id}`,
|
|
725
|
+
title
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
// For newly created E2EE documents, the initial content is still
|
|
729
|
+
// plaintext (it came from the template). We need to send an initial
|
|
730
|
+
// encrypted snapshot so the server stores encrypted content.
|
|
731
|
+
// We detect a new document by checking if the content was plaintext
|
|
732
|
+
// (not a Base64 string) and this is the initial load.
|
|
733
|
+
const isNewE2EEDocument =
|
|
734
|
+
isInitialLoad && typeof doc.content !== "string"
|
|
735
|
+
if (isNewE2EEDocument && this.mod.editor.e2ee.snapshotManager) {
|
|
736
|
+
this.mod.editor.e2ee.snapshotManager.sendInitialSnapshot(
|
|
737
|
+
decryptedContent,
|
|
738
|
+
decryptedComments,
|
|
739
|
+
decryptedBibliography,
|
|
740
|
+
this.mod.editor.docInfo.version
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Now that the key is available and the document is loaded, open the
|
|
745
|
+
// WebSocket connection. This ensures encrypted catch-up diffs (ep)
|
|
746
|
+
// from the server can be decrypted as soon as they arrive.
|
|
747
|
+
if (this.mod.editor.ws && !this.mod.editor.ws.connected) {
|
|
748
|
+
this.mod.editor.startWebSocket()
|
|
749
|
+
} else if (!this.mod.editor.ws) {
|
|
750
|
+
// Non-collaborative mode: mark version as confirmed so the editor
|
|
751
|
+
// finishes loading and periodic saves can begin.
|
|
752
|
+
this.mod.editor.mod.collab.doc.confirmVersion(
|
|
753
|
+
this.mod.editor.docInfo.version
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Load an unencrypted (or already decrypted) document into ProseMirror.
|
|
760
|
+
* This is the original loadDocument logic, extracted for reuse.
|
|
761
|
+
* @private
|
|
762
|
+
*/
|
|
763
|
+
_loadUnencryptedDocument(doc, isInitialLoad) {
|
|
764
|
+
// Remember location hash to scroll there subsequently.
|
|
765
|
+
const [locationHash, _queryString] = window.location.hash.split("?")
|
|
766
|
+
this.mod.editor.mod.db.bibDB.setDB(doc.bibliography)
|
|
767
|
+
this.mod.editor.mod.db.imageDB.setDB(doc.images)
|
|
768
|
+
const stateDoc = this.mod.editor.schema.nodeFromJSON(doc.content)
|
|
769
|
+
const plugins = this.mod.editor.statePlugins.map(plugin => {
|
|
770
|
+
if (plugin[1]) {
|
|
771
|
+
return plugin[0](plugin[1](doc))
|
|
772
|
+
} else {
|
|
773
|
+
return plugin[0]()
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
const stateConfig = {
|
|
778
|
+
schema: this.mod.editor.schema,
|
|
779
|
+
doc: stateDoc,
|
|
780
|
+
plugins
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Set document in prosemirror
|
|
784
|
+
this.mod.editor.view.setProps({state: EditorState.create(stateConfig)})
|
|
785
|
+
this.mod.editor.view.setProps({nodeViews: {}}) // Needed to initialize nodeViews in plugins
|
|
786
|
+
// Set initial confirmed doc
|
|
787
|
+
this.mod.editor.docInfo.confirmedDoc = this.mod.editor.view.state.doc
|
|
788
|
+
|
|
789
|
+
// Render footnotes based on main doc
|
|
790
|
+
this.mod.editor.mod.footnotes.fnEditor.renderAllFootnotes()
|
|
791
|
+
|
|
792
|
+
// Setup comment handling
|
|
793
|
+
this.mod.editor.mod.comments.store.reset()
|
|
794
|
+
this.mod.editor.mod.comments.store.loadComments(doc.comments)
|
|
795
|
+
this.mod.editor.mod.marginboxes.view(this.mod.editor.view)
|
|
796
|
+
if (locationHash && locationHash.length) {
|
|
797
|
+
this.mod.editor.scrollIdIntoView(locationHash.slice(1))
|
|
798
|
+
}
|
|
799
|
+
// Update the header bar to reflect the loaded document's title.
|
|
800
|
+
// This is needed because setStyles() (which calls headerView.update())
|
|
801
|
+
// runs before loadDocument(), so the header still shows "Untitled"
|
|
802
|
+
// from the empty initial document.
|
|
803
|
+
if (this.mod.editor.menu.headerView) {
|
|
804
|
+
this.mod.editor.menu.headerView.update()
|
|
805
|
+
}
|
|
806
|
+
if (isInitialLoad) {
|
|
807
|
+
this.initialDocLoaded = true
|
|
808
|
+
} else {
|
|
809
|
+
deactivateWait()
|
|
810
|
+
this.mod.editor.waitingForDocument = false
|
|
811
|
+
}
|
|
812
|
+
if (doc.template) {
|
|
813
|
+
// We received the template. That means we are the first user present with write access.
|
|
814
|
+
// We will adjust the document to the template if necessary.
|
|
815
|
+
// For E2EE documents, the content has already been decrypted
|
|
816
|
+
// by this point, so template adjustment works normally.
|
|
817
|
+
if (isInitialLoad) {
|
|
818
|
+
this.templateAdjustmentPending = true
|
|
819
|
+
}
|
|
820
|
+
activateWait(true, gettext("Updating document. Please wait..."))
|
|
821
|
+
const activateWaitTimer = setTimeout(() => {
|
|
822
|
+
activateWait(
|
|
823
|
+
true,
|
|
824
|
+
gettext(
|
|
825
|
+
"It's taking a bit longer than usual, but it should be ready soon. Please wait..."
|
|
826
|
+
)
|
|
827
|
+
)
|
|
828
|
+
}, 60000)
|
|
829
|
+
const adjustWorker = makeWorker(
|
|
830
|
+
staticUrl("js/adjust_doc_to_template_worker.js")
|
|
831
|
+
)
|
|
832
|
+
adjustWorker.onmessage = message => {
|
|
833
|
+
if (message.data.type === "result") {
|
|
834
|
+
if (message.data.steps.length) {
|
|
835
|
+
const tr = this.mod.editor.view.state.tr
|
|
836
|
+
message.data.steps.forEach(step =>
|
|
837
|
+
tr.step(Step.fromJSON(this.mod.editor.schema, step))
|
|
838
|
+
)
|
|
839
|
+
tr.setMeta("remote", true)
|
|
840
|
+
this.mod.editor.view.dispatch(tr)
|
|
841
|
+
}
|
|
842
|
+
// clearing timer for updating message since operation is completed
|
|
843
|
+
clearTimeout(activateWaitTimer)
|
|
844
|
+
if (isInitialLoad) {
|
|
845
|
+
this.templateAdjustmentPending = false
|
|
846
|
+
this.finishInitialLoad()
|
|
847
|
+
} else {
|
|
848
|
+
deactivateWait()
|
|
849
|
+
}
|
|
850
|
+
this.setDocSettings()
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
adjustWorker.onerror = error => console.error(error)
|
|
854
|
+
const schemaExporter = new SchemaExport()
|
|
855
|
+
adjustWorker.postMessage({
|
|
856
|
+
schemaSpec: JSON.parse(schemaExporter.init()),
|
|
857
|
+
doc: doc.content,
|
|
858
|
+
template: doc.template.content,
|
|
859
|
+
documentStyleSlugs:
|
|
860
|
+
this.mod.editor.mod.documentTemplate.documentStyles.map(
|
|
861
|
+
style => style.slug
|
|
862
|
+
)
|
|
863
|
+
})
|
|
864
|
+
} else {
|
|
865
|
+
this.setDocSettings()
|
|
866
|
+
if (isInitialLoad) {
|
|
867
|
+
this.finishInitialLoad()
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
setDocSettings() {
|
|
873
|
+
// Set part specific settings
|
|
874
|
+
this.mod.editor.mod.documentTemplate.addDocPartSettings()
|
|
875
|
+
this.mod.editor.mod.documentTemplate.addCitationStylesMenuEntries()
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
sendToCollaborators() {
|
|
879
|
+
// Guard: WebSocket may not be initialized yet (e.g. during
|
|
880
|
+
// loadDocument before the WS connector is created).
|
|
881
|
+
if (!this.mod.editor.ws) {
|
|
882
|
+
return
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// For E2EE documents, we need to encrypt diffs before sending.
|
|
886
|
+
// Since encryption is async, we use a separate code path.
|
|
887
|
+
const isE2EE =
|
|
888
|
+
this.mod.editor.e2ee &&
|
|
889
|
+
this.mod.editor.e2ee.encrypted &&
|
|
890
|
+
this.mod.editor.e2ee.key
|
|
891
|
+
|
|
892
|
+
if (isE2EE) {
|
|
893
|
+
this._sendE2EEDiff()
|
|
894
|
+
return
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Handle either doc change and comment updates OR caret update. Priority
|
|
898
|
+
// for doc change/comment update.
|
|
899
|
+
this.mod.editor.ws.send(() => {
|
|
900
|
+
if (
|
|
901
|
+
this.awaitingDiffResponse ||
|
|
902
|
+
this.mod.editor.waitingForDocument ||
|
|
903
|
+
this.receiving
|
|
904
|
+
) {
|
|
905
|
+
return false
|
|
906
|
+
} else if (
|
|
907
|
+
sendableSteps(this.mod.editor.view.state) ||
|
|
908
|
+
this.mod.editor.mod.comments.store.unsentEvents().length ||
|
|
909
|
+
this.mod.editor.mod.db.bibDB.unsentEvents().length ||
|
|
910
|
+
this.mod.editor.mod.db.imageDB.unsentEvents().length
|
|
911
|
+
) {
|
|
912
|
+
this.disableDiffSending()
|
|
913
|
+
const stepsToSend = sendableSteps(this.mod.editor.view.state),
|
|
914
|
+
fnStepsToSend = sendableSteps(
|
|
915
|
+
this.mod.editor.mod.footnotes.fnEditor.view.state
|
|
916
|
+
),
|
|
917
|
+
commentUpdates =
|
|
918
|
+
this.mod.editor.mod.comments.store.unsentEvents(),
|
|
919
|
+
bibliographyUpdates =
|
|
920
|
+
this.mod.editor.mod.db.bibDB.unsentEvents(),
|
|
921
|
+
imageUpdates = this.mod.editor.mod.db.imageDB.unsentEvents()
|
|
922
|
+
|
|
923
|
+
if (
|
|
924
|
+
!stepsToSend &&
|
|
925
|
+
!fnStepsToSend &&
|
|
926
|
+
!commentUpdates.length &&
|
|
927
|
+
!bibliographyUpdates.length &&
|
|
928
|
+
!imageUpdates.length
|
|
929
|
+
) {
|
|
930
|
+
// no diff. abandon operation
|
|
931
|
+
return
|
|
932
|
+
}
|
|
933
|
+
const rid = this.confirmStepsRequestCounter++,
|
|
934
|
+
unconfirmedDiff = {
|
|
935
|
+
type: "diff",
|
|
936
|
+
v: this.mod.editor.docInfo.version,
|
|
937
|
+
rid
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
unconfirmedDiff["cid"] = this.mod.editor.client_id
|
|
941
|
+
|
|
942
|
+
if (stepsToSend) {
|
|
943
|
+
unconfirmedDiff["ds"] = stepsToSend.steps.map(s =>
|
|
944
|
+
s.toJSON()
|
|
945
|
+
)
|
|
946
|
+
// In case the title changed, we also add a title field to
|
|
947
|
+
// update the title field instantly - important for the
|
|
948
|
+
// document overview page.
|
|
949
|
+
let newTitle = ""
|
|
950
|
+
this.mod.editor.view.state.doc.firstChild.forEach(child => {
|
|
951
|
+
if (
|
|
952
|
+
!child.marks.find(
|
|
953
|
+
mark => mark.type.name === "deletion"
|
|
954
|
+
)
|
|
955
|
+
) {
|
|
956
|
+
newTitle += child.textContent
|
|
957
|
+
}
|
|
958
|
+
})
|
|
959
|
+
newTitle = newTitle.slice(0, 255)
|
|
960
|
+
let oldTitle = ""
|
|
961
|
+
this.mod.editor.docInfo.confirmedDoc.firstChild.forEach(
|
|
962
|
+
child => {
|
|
963
|
+
if (
|
|
964
|
+
!child.marks.find(
|
|
965
|
+
mark => mark.type.name === "deletion"
|
|
966
|
+
)
|
|
967
|
+
) {
|
|
968
|
+
oldTitle += child.textContent
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
)
|
|
972
|
+
oldTitle = oldTitle.slice(0, 255)
|
|
973
|
+
if (newTitle !== oldTitle) {
|
|
974
|
+
unconfirmedDiff["ti"] = newTitle
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (fnStepsToSend) {
|
|
979
|
+
// We add the client ID to every single step
|
|
980
|
+
unconfirmedDiff["fs"] = fnStepsToSend.steps.map(s =>
|
|
981
|
+
s.toJSON()
|
|
982
|
+
)
|
|
983
|
+
}
|
|
984
|
+
if (this.footnoteRender) {
|
|
985
|
+
unconfirmedDiff["footnoterender"] = true
|
|
986
|
+
this.footnoteRender = false
|
|
987
|
+
}
|
|
988
|
+
if (commentUpdates.length) {
|
|
989
|
+
unconfirmedDiff["cu"] = commentUpdates
|
|
990
|
+
}
|
|
991
|
+
if (bibliographyUpdates.length) {
|
|
992
|
+
unconfirmedDiff["bu"] = bibliographyUpdates
|
|
993
|
+
}
|
|
994
|
+
if (imageUpdates.length) {
|
|
995
|
+
unconfirmedDiff["iu"] = imageUpdates
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
this.unconfirmedDiffs[rid] = Object.assign(
|
|
999
|
+
{doc: this.mod.editor.view.state.doc},
|
|
1000
|
+
unconfirmedDiff
|
|
1001
|
+
)
|
|
1002
|
+
return unconfirmedDiff
|
|
1003
|
+
} else if (
|
|
1004
|
+
this.mod.editor.currentView?.state &&
|
|
1005
|
+
getSelectionUpdate(this.mod.editor.currentView.state)
|
|
1006
|
+
) {
|
|
1007
|
+
const currentView = this.mod.editor.currentView
|
|
1008
|
+
|
|
1009
|
+
if (this.lastSelectionUpdateState === currentView.state) {
|
|
1010
|
+
// Selection update has been sent for this state already. Skip
|
|
1011
|
+
return false
|
|
1012
|
+
}
|
|
1013
|
+
this.lastSelectionUpdateState = currentView.state
|
|
1014
|
+
// Create a new caret as the current user
|
|
1015
|
+
const selectionUpdate = getSelectionUpdate(currentView.state)
|
|
1016
|
+
return {
|
|
1017
|
+
type: "selection_change",
|
|
1018
|
+
id: this.mod.editor.user.id,
|
|
1019
|
+
v: this.mod.editor.docInfo.version,
|
|
1020
|
+
session_id: this.mod.editor.docInfo.session_id,
|
|
1021
|
+
anchor: selectionUpdate.anchor,
|
|
1022
|
+
head: selectionUpdate.head,
|
|
1023
|
+
// Whether the selection is in the footnote or the main editor
|
|
1024
|
+
editor:
|
|
1025
|
+
currentView === this.mod.editor.view
|
|
1026
|
+
? "main"
|
|
1027
|
+
: "footnotes"
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
return false
|
|
1031
|
+
}
|
|
1032
|
+
})
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Send an encrypted diff for an E2EE document.
|
|
1037
|
+
*
|
|
1038
|
+
* For E2EE documents, the diff payload (steps, comments, bibliography,
|
|
1039
|
+
* image updates) is encrypted as a single blob before sending. The
|
|
1040
|
+
* server relays the encrypted diff to other clients without being able
|
|
1041
|
+
* to read the content.
|
|
1042
|
+
*
|
|
1043
|
+
* The unencrypted diff is stored locally in `unconfirmedDiffs` for
|
|
1044
|
+
* confirmation/rejection handling. Only the wire format is encrypted.
|
|
1045
|
+
*
|
|
1046
|
+
* @private
|
|
1047
|
+
*/
|
|
1048
|
+
async _sendE2EEDiff() {
|
|
1049
|
+
// Check if there's anything to send
|
|
1050
|
+
if (
|
|
1051
|
+
this.awaitingDiffResponse ||
|
|
1052
|
+
this.mod.editor.waitingForDocument ||
|
|
1053
|
+
this.receiving
|
|
1054
|
+
) {
|
|
1055
|
+
// No diff to send, but check for selection update.
|
|
1056
|
+
// Guard: the editor view may not be initialized yet
|
|
1057
|
+
// (e.g. during loadDocument before ProseMirror is set up).
|
|
1058
|
+
if (this.mod.editor.view) {
|
|
1059
|
+
this._sendE2EESelectionChange()
|
|
1060
|
+
}
|
|
1061
|
+
return
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (
|
|
1065
|
+
!sendableSteps(this.mod.editor.view.state) &&
|
|
1066
|
+
!this.mod.editor.mod.comments.store.unsentEvents().length &&
|
|
1067
|
+
!this.mod.editor.mod.db.bibDB.unsentEvents().length &&
|
|
1068
|
+
!this.mod.editor.mod.db.imageDB.unsentEvents().length
|
|
1069
|
+
) {
|
|
1070
|
+
// No diff, check for selection update
|
|
1071
|
+
if (this.mod.editor.view) {
|
|
1072
|
+
this._sendE2EESelectionChange()
|
|
1073
|
+
}
|
|
1074
|
+
return
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
this.disableDiffSending()
|
|
1078
|
+
|
|
1079
|
+
const stepsToSend = sendableSteps(this.mod.editor.view.state),
|
|
1080
|
+
fnStepsToSend = sendableSteps(
|
|
1081
|
+
this.mod.editor.mod.footnotes.fnEditor.view.state
|
|
1082
|
+
),
|
|
1083
|
+
commentUpdates = this.mod.editor.mod.comments.store.unsentEvents(),
|
|
1084
|
+
bibliographyUpdates = this.mod.editor.mod.db.bibDB.unsentEvents(),
|
|
1085
|
+
imageUpdates = this.mod.editor.mod.db.imageDB.unsentEvents()
|
|
1086
|
+
|
|
1087
|
+
if (
|
|
1088
|
+
!stepsToSend &&
|
|
1089
|
+
!fnStepsToSend &&
|
|
1090
|
+
!commentUpdates.length &&
|
|
1091
|
+
!bibliographyUpdates.length &&
|
|
1092
|
+
!imageUpdates.length
|
|
1093
|
+
) {
|
|
1094
|
+
// no diff. abandon operation
|
|
1095
|
+
this.enableDiffSending()
|
|
1096
|
+
return
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const rid = this.confirmStepsRequestCounter++,
|
|
1100
|
+
unconfirmedDiff = {
|
|
1101
|
+
type: "diff",
|
|
1102
|
+
v: this.mod.editor.docInfo.version,
|
|
1103
|
+
rid
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
unconfirmedDiff["cid"] = this.mod.editor.client_id
|
|
1107
|
+
|
|
1108
|
+
// Collect the payload fields that need encryption
|
|
1109
|
+
const encryptedPayload = {}
|
|
1110
|
+
|
|
1111
|
+
if (stepsToSend) {
|
|
1112
|
+
encryptedPayload["ds"] = stepsToSend.steps.map(s => s.toJSON())
|
|
1113
|
+
// Track title changes for the document overview
|
|
1114
|
+
let newTitle = ""
|
|
1115
|
+
this.mod.editor.view.state.doc.firstChild.forEach(child => {
|
|
1116
|
+
if (!child.marks.find(mark => mark.type.name === "deletion")) {
|
|
1117
|
+
newTitle += child.textContent
|
|
1118
|
+
}
|
|
1119
|
+
})
|
|
1120
|
+
newTitle = newTitle.slice(0, 255)
|
|
1121
|
+
let oldTitle = ""
|
|
1122
|
+
this.mod.editor.docInfo.confirmedDoc.firstChild.forEach(child => {
|
|
1123
|
+
if (!child.marks.find(mark => mark.type.name === "deletion")) {
|
|
1124
|
+
oldTitle += child.textContent
|
|
1125
|
+
}
|
|
1126
|
+
})
|
|
1127
|
+
oldTitle = oldTitle.slice(0, 255)
|
|
1128
|
+
if (newTitle !== oldTitle) {
|
|
1129
|
+
// For E2EE documents, the title is encrypted too.
|
|
1130
|
+
// We don't send "ti" in plaintext; it goes in the
|
|
1131
|
+
// encrypted payload.
|
|
1132
|
+
encryptedPayload["ti"] = newTitle
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (fnStepsToSend) {
|
|
1137
|
+
encryptedPayload["fs"] = fnStepsToSend.steps.map(s => s.toJSON())
|
|
1138
|
+
}
|
|
1139
|
+
if (this.footnoteRender) {
|
|
1140
|
+
unconfirmedDiff["footnoterender"] = true
|
|
1141
|
+
this.footnoteRender = false
|
|
1142
|
+
}
|
|
1143
|
+
if (commentUpdates.length) {
|
|
1144
|
+
encryptedPayload["cu"] = commentUpdates
|
|
1145
|
+
}
|
|
1146
|
+
if (bibliographyUpdates.length) {
|
|
1147
|
+
encryptedPayload["bu"] = bibliographyUpdates
|
|
1148
|
+
}
|
|
1149
|
+
if (imageUpdates.length) {
|
|
1150
|
+
encryptedPayload["iu"] = imageUpdates
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Store the unencrypted diff locally for confirmation handling.
|
|
1154
|
+
// We keep the plaintext steps so confirmDiff can apply them.
|
|
1155
|
+
this.unconfirmedDiffs[rid] = Object.assign(
|
|
1156
|
+
{doc: this.mod.editor.view.state.doc},
|
|
1157
|
+
unconfirmedDiff,
|
|
1158
|
+
encryptedPayload
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
try {
|
|
1162
|
+
// Encrypt the payload fields as a single blob
|
|
1163
|
+
const key = this.mod.editor.e2ee.key
|
|
1164
|
+
const ep = await E2EEEncryptor.encryptObject(encryptedPayload, key)
|
|
1165
|
+
|
|
1166
|
+
// Build the wire-format diff: metadata in plaintext,
|
|
1167
|
+
// payload encrypted
|
|
1168
|
+
const wireDiff = {
|
|
1169
|
+
type: "diff",
|
|
1170
|
+
v: unconfirmedDiff.v,
|
|
1171
|
+
rid: unconfirmedDiff.rid,
|
|
1172
|
+
cid: unconfirmedDiff.cid,
|
|
1173
|
+
ep, // encrypted payload
|
|
1174
|
+
e2ee_salt: this.mod.editor.e2ee.encryptionSalt
|
|
1175
|
+
}
|
|
1176
|
+
if (unconfirmedDiff.footnoterender) {
|
|
1177
|
+
wireDiff.footnoterender = true
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Send the encrypted diff
|
|
1181
|
+
this.mod.editor.ws.send(() => wireDiff)
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
console.error("E2EE: Failed to encrypt diff", error)
|
|
1184
|
+
// Re-enable diff sending so we can try again
|
|
1185
|
+
this.enableDiffSending()
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Send a selection change for an E2EE document.
|
|
1191
|
+
* Selection changes are not encrypted (they only contain cursor position).
|
|
1192
|
+
*
|
|
1193
|
+
* @private
|
|
1194
|
+
*/
|
|
1195
|
+
_sendE2EESelectionChange() {
|
|
1196
|
+
if (
|
|
1197
|
+
!this.mod.editor.currentView ||
|
|
1198
|
+
!this.mod.editor.currentView.state
|
|
1199
|
+
) {
|
|
1200
|
+
return
|
|
1201
|
+
}
|
|
1202
|
+
if (!getSelectionUpdate(this.mod.editor.currentView.state)) {
|
|
1203
|
+
return
|
|
1204
|
+
}
|
|
1205
|
+
const currentView = this.mod.editor.currentView
|
|
1206
|
+
if (this.lastSelectionUpdateState === currentView.state) {
|
|
1207
|
+
return
|
|
1208
|
+
}
|
|
1209
|
+
this.lastSelectionUpdateState = currentView.state
|
|
1210
|
+
const selectionUpdate = getSelectionUpdate(currentView.state)
|
|
1211
|
+
this.mod.editor.ws.send(() => ({
|
|
1212
|
+
type: "selection_change",
|
|
1213
|
+
id: this.mod.editor.user.id,
|
|
1214
|
+
v: this.mod.editor.docInfo.version,
|
|
1215
|
+
session_id: this.mod.editor.docInfo.session_id,
|
|
1216
|
+
anchor: selectionUpdate.anchor,
|
|
1217
|
+
head: selectionUpdate.head,
|
|
1218
|
+
editor: currentView === this.mod.editor.view ? "main" : "footnotes"
|
|
1219
|
+
}))
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
receiveSelectionChange(data) {
|
|
1223
|
+
const participant = this.mod.participants.find(
|
|
1224
|
+
par => par.id === data.id
|
|
1225
|
+
)
|
|
1226
|
+
let tr, fnTr
|
|
1227
|
+
if (!participant) {
|
|
1228
|
+
// participant is still unknown to us. Ignore
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1231
|
+
if (data.editor === "footnotes") {
|
|
1232
|
+
fnTr = updateCollaboratorSelection(
|
|
1233
|
+
this.mod.editor.mod.footnotes.fnEditor.view.state,
|
|
1234
|
+
participant,
|
|
1235
|
+
data
|
|
1236
|
+
)
|
|
1237
|
+
tr = removeCollaboratorSelection(this.mod.editor.view.state, data)
|
|
1238
|
+
} else {
|
|
1239
|
+
tr = updateCollaboratorSelection(
|
|
1240
|
+
this.mod.editor.view.state,
|
|
1241
|
+
participant,
|
|
1242
|
+
data
|
|
1243
|
+
)
|
|
1244
|
+
fnTr = removeCollaboratorSelection(
|
|
1245
|
+
this.mod.editor.mod.footnotes.fnEditor.view.state,
|
|
1246
|
+
data
|
|
1247
|
+
)
|
|
1248
|
+
}
|
|
1249
|
+
if (tr) {
|
|
1250
|
+
this.mod.editor.view.dispatch(tr)
|
|
1251
|
+
}
|
|
1252
|
+
if (fnTr) {
|
|
1253
|
+
this.mod.editor.mod.footnotes.fnEditor.view.dispatch(fnTr)
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
receiveDiff(data, serverFix = false) {
|
|
1258
|
+
// Check if this is an encrypted diff for an E2EE document.
|
|
1259
|
+
// Encrypted diffs have an "ep" (encrypted payload) field instead
|
|
1260
|
+
// of plaintext ds/fs/cu/bu/iu fields. We decrypt the payload
|
|
1261
|
+
// first, then process the diff normally.
|
|
1262
|
+
if (data["ep"] && this.mod.editor.e2ee && this.mod.editor.e2ee.key) {
|
|
1263
|
+
this._receiveE2EEDiff(data, serverFix)
|
|
1264
|
+
return
|
|
1265
|
+
}
|
|
1266
|
+
this._processDiff(data, serverFix)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Process a diff (unencrypted or already decrypted).
|
|
1271
|
+
* @private
|
|
1272
|
+
*/
|
|
1273
|
+
_processDiff(data, serverFix = false) {
|
|
1274
|
+
this.mod.editor.docInfo.version++
|
|
1275
|
+
if (data["bu"]) {
|
|
1276
|
+
// bibliography updates
|
|
1277
|
+
this.mod.editor.mod.db.bibDB.receive(data["bu"])
|
|
1278
|
+
}
|
|
1279
|
+
if (data["iu"]) {
|
|
1280
|
+
// images updates
|
|
1281
|
+
this.mod.editor.mod.db.imageDB.receive(data["iu"])
|
|
1282
|
+
}
|
|
1283
|
+
if (data["cu"]) {
|
|
1284
|
+
// comment updates
|
|
1285
|
+
this.mod.editor.mod.comments.store.receive(data["cu"])
|
|
1286
|
+
}
|
|
1287
|
+
if (data["ds"]) {
|
|
1288
|
+
// document steps
|
|
1289
|
+
this.applyDiffs(data["ds"], data["cid"])
|
|
1290
|
+
}
|
|
1291
|
+
if (data["fs"]) {
|
|
1292
|
+
// footnote steps
|
|
1293
|
+
this.mod.editor.mod.footnotes.fnEditor.applyDiffs(
|
|
1294
|
+
data["fs"],
|
|
1295
|
+
data["cid"]
|
|
1296
|
+
)
|
|
1297
|
+
}
|
|
1298
|
+
if (data["footnoterender"]) {
|
|
1299
|
+
// re-render footnotes properly
|
|
1300
|
+
this.mod.editor.mod.footnotes.fnEditor.renderAllFootnotes()
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (serverFix) {
|
|
1304
|
+
// Diff is a fix created by server due to missing diffs.
|
|
1305
|
+
if ("reject_request_id" in data) {
|
|
1306
|
+
delete this.unconfirmedDiffs[data.reject_request_id]
|
|
1307
|
+
}
|
|
1308
|
+
this.cancelCurrentlyCheckingVersion()
|
|
1309
|
+
|
|
1310
|
+
// There may be unsent local changes. Send them now after .5 seconds,
|
|
1311
|
+
// in case collaborators want to send something first.
|
|
1312
|
+
this.enableDiffSending()
|
|
1313
|
+
window.setTimeout(() => this.sendToCollaborators(), 500)
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Receive and decrypt an E2EE diff from another client.
|
|
1319
|
+
*
|
|
1320
|
+
* For E2EE documents, diffs arrive with an "ep" (encrypted payload)
|
|
1321
|
+
* field containing the encrypted steps, comments, bibliography, and
|
|
1322
|
+
* image updates. This method decrypts the payload and then processes
|
|
1323
|
+
* the diff using the standard _processDiff path.
|
|
1324
|
+
*
|
|
1325
|
+
* @param {Object} data - The diff message with "ep" field
|
|
1326
|
+
* @param {boolean} serverFix - Whether this is a server-generated fix
|
|
1327
|
+
* @private
|
|
1328
|
+
*/
|
|
1329
|
+
async _receiveE2EEDiff(data, serverFix = false) {
|
|
1330
|
+
try {
|
|
1331
|
+
const key = this.mod.editor.e2ee.key
|
|
1332
|
+
const decryptedPayload = await E2EEEncryptor.decryptObject(
|
|
1333
|
+
data["ep"],
|
|
1334
|
+
key
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
// Merge the decrypted payload with the metadata fields
|
|
1338
|
+
// that were sent in plaintext (cid, rid, footnoterender)
|
|
1339
|
+
const mergedData = {
|
|
1340
|
+
cid: data["cid"],
|
|
1341
|
+
rid: data["rid"],
|
|
1342
|
+
footnoterender: data["footnoterender"],
|
|
1343
|
+
reject_request_id: data["reject_request_id"],
|
|
1344
|
+
...decryptedPayload
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Process the decrypted diff normally
|
|
1348
|
+
this._processDiff(mergedData, serverFix)
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
console.error("E2EE: Failed to decrypt incoming diff", error)
|
|
1351
|
+
// If decryption fails, we still need to increment the version
|
|
1352
|
+
// to stay in sync with the server, but we skip applying the
|
|
1353
|
+
// diff content.
|
|
1354
|
+
this.mod.editor.docInfo.version++
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
setConfirmedDoc(tr, stepsLength) {
|
|
1359
|
+
// Find the latest version of the doc without any unconfirmed local changes
|
|
1360
|
+
|
|
1361
|
+
const rebased = tr.getMeta("rebased"),
|
|
1362
|
+
docNumber = rebased + stepsLength
|
|
1363
|
+
|
|
1364
|
+
this.mod.editor.docInfo.confirmedDoc =
|
|
1365
|
+
docNumber === tr.docs.length ? tr.doc : tr.docs[docNumber]
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
confirmDiff(request_id) {
|
|
1369
|
+
const unconfirmedDiffs = this.unconfirmedDiffs[request_id]
|
|
1370
|
+
if (!unconfirmedDiffs) {
|
|
1371
|
+
return
|
|
1372
|
+
}
|
|
1373
|
+
this.mod.editor.docInfo.version++
|
|
1374
|
+
|
|
1375
|
+
const sentSteps = unconfirmedDiffs["ds"] // document steps
|
|
1376
|
+
if (sentSteps) {
|
|
1377
|
+
const ourIds = sentSteps.map(_step => this.mod.editor.client_id)
|
|
1378
|
+
const tr = receiveTransaction(
|
|
1379
|
+
this.mod.editor.view.state,
|
|
1380
|
+
sentSteps,
|
|
1381
|
+
ourIds
|
|
1382
|
+
)
|
|
1383
|
+
this.mod.editor.view.dispatch(tr)
|
|
1384
|
+
this.mod.editor.docInfo.confirmedDoc = unconfirmedDiffs["doc"]
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const sentFnSteps = unconfirmedDiffs["fs"] // footnote steps
|
|
1388
|
+
if (sentFnSteps) {
|
|
1389
|
+
const fnTr = receiveTransaction(
|
|
1390
|
+
this.mod.editor.mod.footnotes.fnEditor.view.state,
|
|
1391
|
+
sentFnSteps,
|
|
1392
|
+
sentFnSteps.map(_step => this.mod.editor.client_id)
|
|
1393
|
+
)
|
|
1394
|
+
this.mod.editor.mod.footnotes.fnEditor.view.dispatch(fnTr)
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const sentComments = unconfirmedDiffs["cu"] // comment updates
|
|
1398
|
+
if (sentComments) {
|
|
1399
|
+
this.mod.editor.mod.comments.store.eventsSent(sentComments)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const sentBibliographyUpdates = unconfirmedDiffs["bu"] // bibliography updates
|
|
1403
|
+
if (sentBibliographyUpdates) {
|
|
1404
|
+
this.mod.editor.mod.db.bibDB.eventsSent(sentBibliographyUpdates)
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const sentImageUpdates = unconfirmedDiffs["iu"] // image updates
|
|
1408
|
+
if (sentImageUpdates) {
|
|
1409
|
+
this.mod.editor.mod.db.imageDB.eventsSent(sentImageUpdates)
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
delete this.unconfirmedDiffs[request_id]
|
|
1413
|
+
this.enableDiffSending()
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
rejectDiff(request_id) {
|
|
1417
|
+
delete this.unconfirmedDiffs[request_id]
|
|
1418
|
+
this.enableDiffSending()
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
applyDiffs(diffs, cid) {
|
|
1422
|
+
this.receiving = true
|
|
1423
|
+
const steps = diffs.map(j => Step.fromJSON(this.mod.editor.schema, j))
|
|
1424
|
+
const clientIds = diffs.map(_ => cid)
|
|
1425
|
+
const tr = receiveTransaction(
|
|
1426
|
+
this.mod.editor.view.state,
|
|
1427
|
+
steps,
|
|
1428
|
+
clientIds
|
|
1429
|
+
)
|
|
1430
|
+
tr.setMeta("remote", true)
|
|
1431
|
+
this.mod.editor.view.dispatch(tr)
|
|
1432
|
+
this.setConfirmedDoc(tr, steps.length)
|
|
1433
|
+
this.receiving = false
|
|
1434
|
+
this.sendToCollaborators()
|
|
1435
|
+
}
|
|
1436
|
+
}
|