v1.0.83: HTML-escape + JSON validation for map fields (onblur + pre-submit)

This commit is contained in:
2026-07-27 17:02:26 +04:00
parent 6afe4ef9a9
commit c37709eef0
2 changed files with 41 additions and 5 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ from routes.main import bp as main_bp
from routes.api import bp as api_bp from routes.api import bp as api_bp
from routes.api_test import bp as api_test_bp from routes.api_test import bp as api_test_bp
VERSION = "1.0.82" VERSION = "1.0.83"
app = Flask(__name__, template_folder="templates", static_folder="static") app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc") app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
+40 -4
View File
@@ -211,27 +211,39 @@ function showParams(opId,opName){
const params=data.params||data; const params=data.params||data;
const form=document.getElementById('params-form'); const form=document.getElementById('params-form');
const displayNameValue=isCreate?makeCreateDisplayName():''; const displayNameValue=isCreate?makeCreateDisplayName():'';
form.innerHTML=(opName==='create'?`<div class="param-row"><label class="req">displayName</label><span style="font-size:12px;color:var(--muted);white-space:nowrap;">${AUTOTEST_PREFIX}</span><input type="text" id="param-displayname" value="${displayNameValue}" autocomplete="off"></div>`:'') form.innerHTML=(opName==='create'?`<div class="param-row"><label class="req">displayName</label><span style="font-size:12px;color:var(--muted);white-space:nowrap;">${AUTOTEST_PREFIX}</span><input type="text" id="param-displayname" value="${_esc(displayNameValue)}" autocomplete="off"></div>`:'')
+ params.map(p=>{ + params.map(p=>{
const req=p.isRequired; const req=p.isRequired;
const hasDfl=p.defaultValue!==null&&p.defaultValue!==undefined&&p.defaultValue!==''; const hasDfl=p.defaultValue!==null&&p.defaultValue!==undefined&&p.defaultValue!=='';
const labelCls=req?(hasDfl?'req':'req-nodfl'):''; const labelCls=req?(hasDfl?'req':'req-nodfl'):'';
const dfl=hasDfl?p.defaultValue:(req?'':''); const dfl=hasDfl?p.defaultValue:(req?'':'');
const vl=p.valueList; const vl=p.valueList;
const isMap=(p.dataType||'').startsWith('map');
let input; let input;
if(vl&&Array.isArray(vl)){ if(vl&&Array.isArray(vl)){
input=`<select name="p_${p.svcOperationCfsParamId}" data-type="${p.dataType}">${vl.map(v=>`<option value="${v}" ${v==dfl?'selected':''}>${v}</option>`).join('')}</select>`; input=`<select name="p_${p.svcOperationCfsParamId}" data-type="${_esc(p.dataType)}">${vl.map(v=>`<option value="${_esc(v)}" ${v==dfl?'selected':''}>${_esc(v)}</option>`).join('')}</select>`;
}else if(p.dataType==='boolean'){ }else if(p.dataType==='boolean'){
input=`<select name="p_${p.svcOperationCfsParamId}" data-type="boolean"><option value="true" ${dfl==='true'||dfl===true?'selected':''}>true</option><option value="false" ${dfl==='false'||dfl===false?'selected':''}>false</option></select>`; input=`<select name="p_${p.svcOperationCfsParamId}" data-type="boolean"><option value="true" ${dfl==='true'||dfl===true?'selected':''}>true</option><option value="false" ${dfl==='false'||dfl===false?'selected':''}>false</option></select>`;
}else{ }else{
input=`<input type="text" name="p_${p.svcOperationCfsParamId}" value="${dfl}" placeholder="${req&&!hasDfl?'обязательно':''}">`; const dt=isMap?'map':'';
input=`<input type="text" name="p_${p.svcOperationCfsParamId}" value="${_esc(dfl)}" data-type="${dt}" placeholder="${req&&!hasDfl?'обязательно':''}"${isMap?' onblur="validateJson(this)"':''}>`;
if(isMap) input+=`<span class="json-err" style="display:none;color:var(--destructive);font-size:10px;margin-left:4px;"></span>`;
} }
return `<div class="param-row"><label class="${labelCls}">${p.name}</label>${input}</div>`; return `<div class="param-row"><label class="${labelCls}">${_esc(p.name)}</label>${input}</div>`;
}).join(''); }).join('');
const btn=document.getElementById('btn-test'); const btn=document.getElementById('btn-test');
btn.style.display='inline-flex'; btn.style.display='inline-flex';
btn.textContent='Запустить'; btn.textContent='Запустить';
btn.onclick=()=>{ btn.onclick=()=>{
// Проверить ВСЕ map-поля перед отправкой
let bad=[];
document.querySelectorAll('#params-form [data-type="map"]').forEach(el=>{
if(!validateJson(el,true)) bad.push(el);
});
if(bad.length>0){
document.getElementById('test-status').innerHTML=`<span class="badge">Ошибка</span> ${bad.length} полей с невалидным JSON — поправьте и нажмите Запустить снова`;
return;
}
const pp={}; const pp={};
document.querySelectorAll('#params-form [name^="p_"]').forEach(el=>{ document.querySelectorAll('#params-form [name^="p_"]').forEach(el=>{
let v=el.value; let v=el.value;
@@ -245,6 +257,30 @@ function showParams(opId,opName){
}); });
} }
// === Хелперы ===
function _esc(s){
/* HTML-экранирование: " → &quot; & → &amp; < → &lt; */
return String(s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;');
}
function validateJson(el,quiet){
/* Проверить что значение в поле — валидный JSON.
quiet=true — не менять внешний вид (для batch-проверки перед отправкой). */
const errEl=el.parentElement.querySelector('.json-err');
const v=el.value.trim();
if(!v) return true; // пусто — ок
try{ JSON.parse(v); }
catch(e){
if(!quiet&&errEl){ errEl.style.display='inline'; errEl.textContent='Ошибка JSON: '+e.message; }
el.style.borderColor='var(--destructive)';
return false;
}
if(!quiet&&errEl) errEl.style.display='none';
el.style.borderColor='';
return true;
}
function setFinishedState(statusText, statusClass, statusError, statusDuration, instanceName){ function setFinishedState(statusText, statusClass, statusError, statusDuration, instanceName){
const btn=document.getElementById('btn-test'); const btn=document.getElementById('btn-test');
btn.disabled=false; btn.disabled=false;