diff --git a/tools/cldr-apps/js/src/esm/cldrVote.mjs b/tools/cldr-apps/js/src/esm/cldrVote.mjs index 6a22cddb854..8644f58dc19 100644 --- a/tools/cldr-apps/js/src/esm/cldrVote.mjs +++ b/tools/cldr-apps/js/src/esm/cldrVote.mjs @@ -13,7 +13,7 @@ import * as cldrSurvey from "./cldrSurvey.mjs"; import * as cldrTable from "./cldrTable.mjs"; import * as cldrText from "./cldrText.mjs"; -const CLDR_VOTE_DEBUG = false; +const CLDR_VOTE_DEBUG = true; /** * The special "vote level" selected by the user, or zero for default. @@ -26,10 +26,11 @@ let voteLevelChanged = 0; /** * Wire up the button to perform a submit * - * @param button - * @param tr - * @param theRow - * @param vHash + * @param {Element} button the GUI button + * @param {Element} tr the table row + * @param {Object} theRow object describing the table row + * @param {String} vHash hash of the value of the candidate item (cf. DataPage.getValueHash on back end), + * or empty string for newly submitted value */ function wireUpButton(button, tr, theRow, vHash) { let vHashStr = vHash; @@ -84,12 +85,10 @@ function wireUpButton(button, tr, theRow, vHash) { * @param {Object} newValue the newly submitted value, or undefined if it's a vote for an already existing value (buttonb) * @param {Element} button the GUI button * - * TODO: shorten this function, using (non-nested) subroutines - * * Called from cldrTable.addValueVote (for new value submission) * as well as locally in cldrVote (vote for existing value or abstain) */ -function handleWiredClick(tr, theRow, vHash, newValue, button) { +async function handleWiredClick(tr, theRow, vHash, newValue, button) { if (!tr || !theRow || tr.wait) { return; } @@ -117,138 +116,134 @@ function handleWiredClick(tr, theRow, vHash, newValue, button) { } tr.myProposal = null; // mark any pending proposal as invalid. } - - var myUnDefer = function () { - tr.wait = false; - }; tr.wait = true; cldrInfo.reset(); theRow.proposedResults = null; - if (CLDR_VOTE_DEBUG) { - console.log( - "Vote for " + tr.rowHash + " v='" + vHash + "', value='" + value + "'" - ); + logVote(tr.rowHash, vHash, value); } const ourContent = { value: valToShow, voteLevelChanged: voteLevelChanged, }; const ourUrl = getSubmitUrl(theRow.xpstrid); - - var originalTrClassName = tr.className; + const init = cldrAjax.makePostData(ourContent); + const originalTrClassName = tr.className; tr.className = "tr_checking1"; - - var loadHandler = function (json) { + oneMorePendingVote(); + try { + const response = await cldrAjax.doFetch(ourUrl, init); /* * Restore tr.className, so it stops being 'tr_checking1' immediately on receiving - * any response. It may change again below, such as to 'tr_err' or 'tr_checking2'. + * any response. It may change again below to 'tr_err' or 'tr_checking2'. */ tr.className = originalTrClassName; + if (response.ok) { + const json = await response.json(); + handleVoteOk(json, tr, theRow, button, valToShow); + } else { + const message = "Server response: " + response.statusText; + handleVoteErr(tr, message, button); + } + } catch (e) { + const message = e.name + " - " + e.message; + handleVoteErr(tr, message, button); + } finally { + tr.wait = false; oneLessPendingVote(); - try { - if (json.err && json.err.length > 0) { - tr.className = "tr_err"; - tr.innerHTML = - "" + - cldrStatus.stopIcon() + - " Could not check value. Try reloading the page.
" + - json.err + - ""; - myUnDefer(); - cldrRetry.handleDisconnect("Error submitting a vote", json); - } else { - if (json.didVote) { - // if submitted.. - tr.className = "tr_checking2"; - cldrTable.refreshSingleRow( - tr, - theRow, - function (theRow) { - // submit went through. Now show the pop. - button.className = "ichoice-o"; - button.checked = false; - cldrSurvey.hideLoader(); - if (json.testResults && (json.testWarnings || json.testErrors)) { - // tried to submit, have errs or warnings. - showProposedItem( - tr.inputTd, - tr, - theRow, - valToShow, - json.testResults - ); - } - myUnDefer(); - }, - function (err) { - myUnDefer(); - cldrRetry.handleDisconnect(err, json); - } - ); // end refresh-loaded-fcn - // end: async - } else { - // Did not submit. Show errors, etc - if ( - (json.statusAction && json.statusAction != "ALLOW") || - (json.testResults && (json.testWarnings || json.testErrors)) - ) { - showProposedItem( - tr.inputTd, - tr, - theRow, - valToShow, - json.testResults, - json - ); - } // else no errors, not submitted. Nothing to do. - button.className = "ichoice-o"; - button.checked = false; - cldrSurvey.hideLoader(); - myUnDefer(); - } + } +} + +/** + * Handle an OK response. Here "OK" doesn't necessarily mean that the vote succeeded; + * it means that we got json, which might have json.err, etc.; json.didVote means + * the vote succeeded. + * + * @param {Object} json the server response + * @param {Element} tr the table row + * @param {Object} theRow object describing the table row + * @param {Element} button the GUI button + * @param {String} valToShow the value the user is voting for + */ +function handleVoteOk(json, tr, theRow, button, valToShow) { + if (json.err && json.err.length > 0) { + handleVoteErr(tr, json.err, button); + } else if (json.didVote) { + handleVoteSubmitted(json, tr, theRow, button, valToShow); + } else { + handleVoteNotSubmitted(json, tr, theRow, button, valToShow); + } +} + +function handleVoteSubmitted(json, tr, theRow, button, valToShow) { + tr.className = "tr_checking2"; + cldrTable.refreshSingleRow( + tr, + theRow, + function (theRow) { + // submit went through. Now show the pop. + button.className = "ichoice-o"; + button.checked = false; + cldrSurvey.hideLoader(); + if (json.testResults && (json.testWarnings || json.testErrors)) { + // tried to submit, have errs or warnings. + showProposedItem(tr.inputTd, tr, theRow, valToShow, json.testResults); } - } catch (e) { - tr.className = "tr_err"; - tr.innerHTML = - cldrStatus.stopIcon() + - " Could not check value. Try reloading the page.
" + - e.message; - console.log("Error in ajax post [handleWiredClick] ", e.message); - myUnDefer(); - cldrRetry.handleDisconnect("handleWiredClick:" + e.message, json); + }, + function (err) { + cldrRetry.handleDisconnect(err, json); } - }; - var errorHandler = function (err) { - /* - * Restore tr.className, so it stops being 'tr_checking1' immediately on receiving - * any response. It may change again below, such as to 'tr_err'. - */ - tr.className = originalTrClassName; - oneLessPendingVote(); - // The err parameter is a string composed by cldrAjax.makeErrorMessage (legacy code), - // not user-friendly, still possibly useful for debugging if this actually occurs. - console.log("Error: " + err); - // To close the input pop-up window with minimal changes to this legacy code, call - // cldrLoad.reloadV when the user closes the notification. Postpone a better solution - // until cldrVote is modernized. - cldrNotify.errorWithCallback( - "Error submitting a vote", - err, - cldrLoad.reloadV - ); - myUnDefer(); - }; - const xhrArgs = { - url: ourUrl, - handleAs: "json", - content: ourContent, - load: loadHandler, - error: errorHandler, - timeout: cldrAjax.mediumTimeout(), - }; - oneMorePendingVote(); - cldrAjax.sendXhr(xhrArgs); + ); +} + +function handleVoteNotSubmitted(json, tr, theRow, button, valToShow) { + if ( + (json.statusAction && json.statusAction != "ALLOW") || + (json.testResults && (json.testWarnings || json.testErrors)) + ) { + showProposedItem(tr.inputTd, tr, theRow, valToShow, json.testResults, json); + } + button.className = "ichoice-o"; + button.checked = false; + cldrSurvey.hideLoader(); +} + +function handleVoteErr(tr, message, button) { + tr.className = "tr_err"; + tr.innerHTML = + "" + + cldrStatus.stopIcon() + + " Could not check value. Try reloading the page.
" + + makeSafe(message) + + ""; + cldrRetry.handleDisconnect("Error submitting a vote"); + button.className = "ichoice-o"; + button.checked = false; + cldrSurvey.hideLoader(); +} + +/** + * Avoid warning, "Directly writing error messages to a webpage without sanitization allows for a cross-site + * scripting vulnerability if parts of the error message can be influenced by a user." + * + * @param {String} s the raw string + * @returns the sanitized string + */ +function makeSafe(s) { + return s.replace(/[<>&]/g, ''); +} + +function logVote(rowHash, vHash, value) { + console.log( + "Vote for " + + rowHash + + " v='" + + vHash + + "', value='" + + value + + "' time = " + + Date.now() + ); } function getSubmitUrl(xpstrid) { @@ -375,7 +370,7 @@ function showProposedItem(inTd, tr, theRow, value, tests, json) { if (!ourItem) { var h3 = document.createElement("h3"); - var span = appendItem(h3, value, "value"); + appendItem(h3, value, "value"); h3.className = "span"; div3.appendChild(h3); } @@ -518,7 +513,12 @@ function oneMorePendingVote() { ++pendingVoteCount; lastTimeChanged = Date.now(); if (CLDR_VOTE_DEBUG) { - console.log("oneMorePendingVote: ++pendingVoteCount = " + pendingVoteCount); + console.log( + "oneMorePendingVote: ++pendingVoteCount = " + + pendingVoteCount + + " time = " + + lastTimeChanged + ); } } @@ -533,7 +533,12 @@ function oneLessPendingVote() { } lastTimeChanged = Date.now(); if (CLDR_VOTE_DEBUG) { - console.log("oneLessPendingVote: --pendingVoteCount = " + pendingVoteCount); + console.log( + "oneLessPendingVote: --pendingVoteCount = " + + pendingVoteCount + + " time = " + + lastTimeChanged + ); } if (pendingVoteCount == 0) { cldrSurvey.expediteStatusUpdate(); @@ -553,18 +558,24 @@ function oneLessPendingVote() { function isBusy() { if (pendingVoteCount > 0) { if (CLDR_VOTE_DEBUG) { - console.log("cldrVote busy: pendingVoteCount = " + pendingVoteCount); + console.log( + "cldrVote busy: pendingVoteCount = " + + pendingVoteCount + + " time = " + + Date.now() + ); } return true; } - if (Date.now() - lastTimeChanged < 3000) { + const time = Date.now(); + if (time - lastTimeChanged < 3000) { if (CLDR_VOTE_DEBUG) { - console.log("cldrVote busy: less than 3 seconds elapsed"); + console.log("cldrVote busy: less than 3 seconds elapsed; time = " + time); } return true; } if (CLDR_VOTE_DEBUG) { - console.log("cldrVote not busy"); + console.log("cldrVote not busy; time = " + time); } return false; }