xref: /plugin/dokucrypt3/script.js (revision 97c734d516051c5b55fc0c340717b771d97a295d)
1/* DOKUWIKI:include_once crypto_high-level.js */
2
3
4/**
5 * Handles necessary actions before submitting the 'wikitext' edit form.
6 */
7var currentSubmitter = null;
8
9function editFormOnSubmit(e) {
10  if (e && e.submitter) {
11    let curSub = e.submitter;
12	if (curSub.name.length==0) return false;
13	if (curSub.name.includes("cancel")) return true;
14
15	// only store the submitter if we got this far
16	currentSubmitter = curSub;
17  }
18
19  if (async_getKey_active) return false;
20
21  if(hasUnencryptedSecrets()) {
22    askForEncryptPasswordWithVerification();
23    return false;
24  } else {
25    // there is no unencrypted content, so we can prepare submitting the form, now
26
27    // Move the original wiki_text element out of the form like we used to do in decryptEditSetup().
28    // To prevent accidental submission of unencrypted text.
29    var wikitext=document.getElementById('wiki__text');
30    var editform=document.getElementById('dw__editform');
31    editform.parentNode.insertBefore(wikitext,editform);
32
33    // Now, get the wikitext content to our hidden field
34    var hiddentext=document.getElementById('wiki__text_submit');
35    hiddentext.value=wikitext.value;
36
37    return true;
38  }
39}
40
41/**
42 * Setup the edit form: add the decrypt button and necessary functionality.
43 */
44function decryptEditSetup(msg) {
45    //alert('setting up');
46    var editform=null, wikitext=null, hiddentext=null, preview=null;
47    if(!(editform=document.getElementById('dw__editform'))) {
48      // alert("no form dw__editform\n");
49      return(true);
50    }
51    if(!(wikitext=document.getElementById('wiki__text'))) {
52     // alert("no wiki__text");
53     return(false);
54    }
55    // if there is no preview button, then assume this is a
56    // "Recover draft" page, dont do anything.
57    if(!(preview=document.getElementById('edbtn__preview'))) {
58      return(false);
59    }
60
61    if(!(save=document.getElementById('edbtn__save'))) {
62      return(false);
63    }
64
65    // Create a hidden element with id 'wiki__text_submit' and
66    // name wikitext (same as the wiki__text).
67
68    if(!(hiddentext=document.createElement('input'))) {
69     return(false);
70    }
71
72    hiddentext.setAttribute('id', 'wiki__text_submit');
73    hiddentext.setAttribute('name', 'wikitext');
74    hiddentext.setAttribute('type','hidden');
75    editform.insertBefore(hiddentext,null);
76
77    if(!(decryptButton=document.createElement('input'))) {
78     return(false);
79    }
80    decryptButton.setAttribute('id', 'decryptButton');
81    decryptButton.setAttribute('name', 'decryptButton');
82    decryptButton.setAttribute('type','Button');
83    decryptButton.setAttribute('value','DecryptSecret');
84    decryptButton.onclick=decryptButtonOnClick;
85    decryptButton.setAttribute('class','button');
86    decryptButton.setAttribute('className','button'); // required for IE
87    preview.parentNode.insertBefore(decryptButton,preview);
88
89    editform.onsubmit = function() {return editFormOnSubmit(event);};
90
91    // The following is taken from lib/scripts/locktimer.js (state of 2018-06-08) to make drafts work.
92    // We override the locktimer refresh function to abort saving of drafts with unencrypted content.
93    dw_locktimer.refresh = function(){
94
95        var now = new Date(),
96                params = 'call=lock&id=' + dw_locktimer.pageid + '&';
97
98            // refresh every half minute only
99            if(now.getTime() - dw_locktimer.lasttime.getTime() <= 30*1000) {
100                return;
101            }
102
103            // POST everything necessary for draft saving
104            if(dw_locktimer.draft && jQuery('#dw__editform').find('textarea[name=wikitext]').length > 0){
105
106                // *** BEGIN dokucrypt modified code
107                // Do not allow saving of a draft, if this page needs some content to be encrypted on save.
108                // Basically abort saving of drafts if this page has some content that needs encrypting.
109                if (hasUnencryptedSecrets()) { return(false); }
110                // *** END dokucrypt modified code
111
112                params += jQuery('#dw__editform').find(dw_locktimer.fieldsToSaveAsDraft.join(', ')).serialize();
113            }
114
115            jQuery.post(
116                DOKU_BASE + 'lib/exe/ajax.php',
117                params,
118                dw_locktimer.refreshed,
119                'json'
120            );
121            dw_locktimer.lasttime = now;
122    };
123}
124
125/**
126 * Checks of there are <SECRET> blocks in the wikitext by trying to encrypt a given text
127 * with <SECRET>s contained. Works and returns synchronously.
128 *
129 * @param  string   x   The text to be encrypted (usually that's the content of the
130 *                      textfield containing the wiki pages text source).
131 *
132 * @return boolean      true, if there are <SECRET> blocks contained. Otherwise false.
133 */
134function hasUnencryptedSecrets() {
135  var wikitext=null, hiddentext=null;
136  if(!(wikitext=document.getElementById('wiki__text'))) {
137    alert("failed to get wiki__text");
138    return(false);
139  }
140  if (wikitext.value.includes("<" + tag_pt))
141    return true;
142  else
143    return false;
144}
145
146/**
147 * Adds an input dialog to the edit page to ask the user for the encryption password. Works with callbacks and therefore represents an asynchronous workflow.
148 */
149function askForEncryptPasswordWithVerification() {
150  var wikitext = document.getElementById('wiki__text');
151  var hiddentext=document.getElementById('wiki__text_submit');
152
153  lock = "default";
154
155  // callback manages what to do and where to insert the decrypted text to
156  // call pw_prompt and let the callback call the next pw_prompt for input verification (repeat passwort)
157  do_verification = function(key) {
158
159    do_encryption = function(key2) {
160      if (key != key2) {
161        alert("Passwords do not match!");
162        return;
163      }
164
165      // important: cache the key first, then try to do the encryption!
166      setKeyForLock(lock,key);
167
168      var encrypted_text = encryptMixedText(wikitext.value);
169      if (encrypted_text) {
170        wikitext.value=encrypted_text;
171        hiddentext.value=encrypted_text;
172
173		// retry submit
174		currentSubmitter.click();
175      } else {
176        setKeyForLock(lock,null);
177        alert("The text could not be encrypted!");
178		currentSubmitter = null;
179      }
180    };
181
182    pw_prompt({
183      lm:"Repeat passphrase key for lock " + lock,
184      elem:wikitext,
185      submit_callback:do_encryption
186    });
187
188  };
189
190  pw_prompt({
191    lm:"Enter passphrase key for lock " + lock,
192    elem:wikitext,
193    submit_callback:do_verification
194  });
195}
196
197function askForDecryptPassword() {
198  var wikitext = document.getElementById('wiki__text');
199  var hiddentext=document.getElementById('wiki__text_submit');
200
201  lock = "default";
202
203  // callback manages what to do and where to insert the decrypted text to
204  do_decryption = function(key) {
205    // important: cache the key first, then try to do the decryption!
206    setKeyForLock(lock,key);
207
208    var decrypted_text = decryptMixedText(wikitext.value);
209    if (decrypted_text) {
210      wikitext.value=decrypted_text;
211      hiddentext.value=decrypted_text;
212    } else {
213      setKeyForLock(lock,null);
214      alert("The text could not be decrypted!");
215    }
216  };
217
218  pw_prompt({
219    lm:"Enter passphrase key for lock " + lock,
220    elem:wikitext,
221    submit_callback:do_decryption
222  });
223
224}
225
226/**
227 * Handles the actions after clicking the Decrypt button in the edit form. Tries to
228 * decrypt any <ENCRYPTED> blocks.
229 */
230function decryptButtonOnClick() {
231  askForDecryptPassword();
232  return(true);
233}
234
235function toggleElemVisibility(elemid) {
236   elem=document.getElementById(elemid);
237   if(elem.style.visibility=="visible") {
238      elem.style.visibility="hidden";
239      elem.style.position="absolute";
240   } else {
241      elem.style.visibility="visible";
242      elem.style.position="relative";
243   }
244}
245
246/*
247  this is called from <A HREF=> links to decrypt the inline html
248*/
249function toggleCryptDiv(elemid,lock,ctext) {
250   var elem=null, atab=null, ptext="";
251   var ctStr="Decrypt Encrypted Text", ptStr="Hide Plaintext";
252   elem=document.getElementById(elemid);
253   atag=document.getElementById(elemid + "_atag");
254   if(elem===null || atag===null) {
255      alert("failed to find element id " + elemid);
256   }
257   if(atag.innerHTML==ptStr) {
258      // encrypt text (set back to ctext, and forget key)
259      elem.innerHTML=ctext;
260      atag.innerHTML=ctStr;
261      elem.style.visibility="hidden";
262      elem.style.position="absolute";
263      setKeyForLock(lock,undefined);
264   } else if (atag.innerHTML==ctStr) {
265      // decrypt text
266
267      // callback manages what to do and where to insert the decrypted text to
268      do_decryption = function(given_key) {
269		//try the decryption
270        if(!(ptext=decryptTextString(ctext,given_key))) {
271          alert("failed to decrypt with provided key");
272          return;
273        }
274
275        elem.textContent=ptext;
276        atag.innerHTML=ptStr;
277        // make it visible
278        elem.style.visibility="visible";
279        elem.style.position="relative";
280
281        //store the key that was used
282        setKeyForLock(lock,given_key);
283
284        if (JSINFO["plugin_dokucrypt2_CONFIG_copytoclipboard"] == 1) {
285          //put it into the clipboard
286          copyToClipboard(ptext).then(() => {
287            if (JSINFO['plugin_dokucrypt2_CONFIG_hidepasswordoncopytoclipboard']) {
288              elem.textContent = "{" + JSINFO['plugin_dokucrypt2_TEXT_copied_to_clipboard'] + "}";
289            } else {
290              elem.textContent += " {" + JSINFO['plugin_dokucrypt2_TEXT_copied_to_clipboard'] + "}";
291            };
292            console.log('Encrypted value has been copied to the clipboard.');
293          }).catch(() => {
294            console.log('Encrypted value could not be copied to the clipboard.');
295          });
296        }
297      };
298
299      // now test if there is a key cached for the given lock - if no key can be determined, show password prompt
300      var key = getKeyForLock(lock);
301      if(key===false || key===undefined || key === null || !decryptTextString(ctext,key)) {
302		pw_prompt({
303          lm:"Enter passphrase for lock " + lock,
304          lock:lock,
305          elem:elem,
306          submit_callback:do_decryption
307        });
308
309      } else {
310        do_decryption(key);
311      }
312   } else { alert("Broken"); return; }
313}
314
315//copy to clipboard from: https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined
316
317function copyToClipboard(textToCopy) {
318    // navigator clipboard api needs a secure context (https)
319    if (navigator.clipboard && window.isSecureContext) {
320        // navigator clipboard api method'
321        return navigator.clipboard.writeText(textToCopy);
322    } else {
323        // text area method
324        let textArea = document.createElement("textarea");
325        textArea.value = textToCopy;
326        // make the textarea out of viewport
327        textArea.style.position = "fixed";
328        textArea.style.left = "-999999px";
329        textArea.style.top = "-999999px";
330        document.body.appendChild(textArea);
331        textArea.focus();
332        textArea.select();
333        return new Promise((res, rej) => {
334            // here the magic happens
335            document.execCommand('copy') ? res() : rej();
336            textArea.remove();
337        });
338    }
339}
340
341// protected password prompt adapted from: https://stackoverflow.com/a/28461750/19144619
342var promptElem = null;
343var label = null;
344var input = null;
345var submit_button = null;
346var cancel_button = null;
347var submit_event = null;
348var cancel_event = null;
349var enter_event = null;
350var async_getKey_active = false; // tracks whether the user is currently being asked for a key
351
352window.pw_prompt = function(options) {
353    var lm = options.lm || "Password:",
354        bm = options.bm || "Submit",
355        cm = options.cm || "Cancel",
356        elem = options.elem || document.body,
357		submit_callback = options.submit_callback;
358
359    if(!submit_callback) { // callback manages what to do and where to insert the decrypted text to
360        alert("No callback function for submitting provided! Please provide one - it should handle the actions after submitting the pw_prompt.")
361    };
362
363    if (promptElem == null) {
364        promptElem = document.createElement("div");
365        promptElem.className = "dokucrypt2pw_prompt";
366
367        label = document.createElement("label");
368        label.textContent = lm;
369        label.for = "pw_prompt_input";
370        promptElem.appendChild(label);
371
372        input = document.createElement("input");
373        input.id = "pw_prompt_input";
374        input.type = "password";
375        promptElem.appendChild(input);
376
377        submit_button = document.createElement("button");
378        promptElem.appendChild(submit_button);
379
380        cancel_button = document.createElement("button");
381        promptElem.appendChild(cancel_button);
382    } else {
383        //remove event listeners
384        submit_button.removeEventListener("click", submit_event);
385        cancel_button.removeEventListener("click", cancel_event);
386    }
387
388    submit_event = function() {
389        if (promptElem.parentNode)
390            promptElem.parentNode.removeChild(promptElem);
391        async_getKey_active = false;
392        submit_callback(input.value);
393    };
394    cancel_event = function() {
395        if (promptElem.parentNode)
396            promptElem.parentNode.removeChild(promptElem);
397        async_getKey_active = false;
398    };
399
400    label.textContent = lm;
401    input.value = "";
402    submit_button.textContent = bm;
403    submit_button.addEventListener("click", submit_event, false);
404    cancel_button.textContent = cm;
405    cancel_button.addEventListener("click", cancel_event, false);
406
407    if(elem.nextSibling){
408        elem.parentNode.insertBefore(promptElem,elem.nextSibling);
409    } else {
410        elem.parentNode.appendChild(promptElem);
411    }
412
413    async_getKey_active = true;
414    input.focus();
415};
416