fix(forms): never submit an invalid form; unlock password-change dialog
Playwright Tests / test (push) Canceled after 0s

serializeObject() returns null for an invalid form, but most callers sent
the result anyway. Add $.fn.rejectInvalid (stop the spinner, let the
browser report the offending field) and use it at every call site.

Bugs fixed:
- Password change: on a mismatch the spinner kept running, and the custom
  validity on "npw" was never cleared, so the browser blocked every later
  submit even after the passwords matched. It is now cleared as soon as
  either password field is edited.
- sendForm: options.complete referenced an undefined jqXHR (ReferenceError)
  and an invalid form was posted with partial data.
- login.send: an invalid form threw a TypeError on qs.loginaccount.
- postXT({ form }): an invalid form was posted with empty data.
- Form dialogs without required fields had no "Bitte überprüfen Sie Ihre
  Eingaben" note, so a rejected submit showed nothing.

Hardening: null guards for the user, user-group, re-login, SMS password,
admin specialties and report-parameter submits.

Add e2e_tests/form-validation.cjs covering each path against the real
scripts; it fails on the previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stefan
2026-10-06 18:11:05 +02:00
co-authored by Claude Opus 5.5
parent 4145ab699c
commit 4443bba63c
20 changed files with 231 additions and 31 deletions
+11 -2
View File
@@ -130,9 +130,18 @@ $fis.resetPass = function (id, fds) {
title: $t.rsp || '', title: $t.rsp || '',
submit: function (e) { submit: function (e) {
var c = $(this).ldng(1); var c = $(this).ldng(1);
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, c.serializeObject(true, { typedvalues: true })); var values = c.serializeObject(true, { typedvalues: true });
if (values === null) { c.rejectInvalid(); return; }
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
if ((qs.npw || '') !== (qs.npwc || '')) { if ((qs.npw || '') !== (qs.npwc || '')) {
c.find('input[name="npw"]:first')[0].setCustomValidity($t.pnm); /* The browser blocks every later submit while npw carries a custom error, so it
must be cleared as soon as either password field is edited again. */
var npw = c.find('input[name="npw"]:first');
npw[0].setCustomValidity($t.pnm);
c.find('input[name="npw"], input[name="npwc"]').off('input.pnm').on('input.pnm', function () {
npw[0].setCustomValidity('');
});
c.rejectInvalid();
} else { } else {
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('account/changepassword'), data: qs, success: function (response) { url: $ocms.url('account/changepassword'), data: qs, success: function (response) {
+3 -2
View File
@@ -48,9 +48,10 @@ let $rep = {
}; };
let scl = function (ev) { let scl = function (ev) {
//console.debug(ev); //console.debug(ev);
let btn = $(this), rx = ev.data; let btn = $(this), rx = ev.data, params = btn.serializeObject();
if (params === null) { return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('rep/gct/' + rx.name), data: btn.serializeObject(), success: (response) => { url: $ocms.url('rep/gct/' + rx.name), data: params, success: (response) => {
btn.addClass('selected').siblings().removeClass('selected'); btn.addClass('selected').siblings().removeClass('selected');
$('#listframe').rC('fix').aC('hd'); $('#listframe').rC('fix').aC('hd');
repfrm.html(response); repfrm.html(response);
@@ -55,6 +55,7 @@ let $spc = {
let f = c.form[0]; let f = c.form[0];
if (typeof f.reportValidity === 'function' && !f.reportValidity()) { return; } if (typeof f.reportValidity === 'function' && !f.reportValidity()) { return; }
let values = c.form.serializeObject(true, { typedvalues: false }); let values = c.form.serializeObject(true, { typedvalues: false });
if (values === null) { c.form.rejectInvalid(); return; }
if (typeof options.confirm === 'function' && !confirm(options.confirm(values))) { return; } if (typeof options.confirm === 'function' && !confirm(options.confirm(values))) { return; }
c.result.hide(); c.result.hide();
btn.prop('disabled', true).text($spt.working); btn.prop('disabled', true).text($spt.working);
+3
View File
@@ -106,6 +106,7 @@ let $usr = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usr/create'), data: qs, success: function (response) { url: $ocms.url('usr/create'), data: qs, success: function (response) {
alert(response.emailed === true ? $uct.create_sent : $uct.create_noemail); alert(response.emailed === true ? $uct.create_sent : $uct.create_noemail);
@@ -141,6 +142,7 @@ let $usr = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usr/pwdreset'), data: qs, success: function (response) { url: $ocms.url('usr/pwdreset'), data: qs, success: function (response) {
alert(response.emailed === true ? $uct.pwdreset_sent : $uct.pwdreset_noemail); alert(response.emailed === true ? $uct.pwdreset_sent : $uct.pwdreset_noemail);
@@ -162,6 +164,7 @@ let $usr = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usr/email'), data: qs, success: function () { url: $ocms.url('usr/email'), data: qs, success: function () {
c.trigger('modal_close'); c.trigger('modal_close');
+2
View File
@@ -58,6 +58,7 @@ let $ugp = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usrgrp/create'), data: qs, success: function () { url: $ocms.url('usrgrp/create'), data: qs, success: function () {
c.trigger('modal_close'); c.trigger('modal_close');
@@ -79,6 +80,7 @@ let $ugp = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usrgrp/rename'), data: qs, success: function () { url: $ocms.url('usrgrp/rename'), data: qs, success: function () {
c.trigger('modal_close'); c.trigger('modal_close');
+14 -4
View File
@@ -488,6 +488,7 @@ $ocms.postXT = function (options) {
} }
if (options.form instanceof jQuery) { if (options.form instanceof jQuery) {
options.data = options.form.serializeObject(); options.data = options.form.serializeObject();
if (options.data === null) { options.form.rejectInvalid(); return; }
options.contentType = 'form-data'; options.contentType = 'form-data';
} else if (options.lzw instanceof jQuery) { } else if (options.lzw instanceof jQuery) {
options.data.lzw = $.ccLZW(options.lzw.serializeAnything(true)).join(','); options.data.lzw = $.ccLZW(options.lzw.serializeAnything(true)).join(',');
@@ -720,6 +721,13 @@ $ocms.vbl_send = function (ev) {
c.toggleClass('invalid', !valid); c.toggleClass('invalid', !valid);
return valid ? result : null; return valid ? result : null;
}; };
/* For a form whose serializeObject() returned null: stop its loading indicator and let the
browser point at the offending field, instead of submitting partial data. */
$.fn.rejectInvalid = function () {
var c = $(this).ldng(0), f = c.closest('form')[0] || c.find('form')[0];
if (f && typeof f.reportValidity === 'function') { f.reportValidity(); }
return c;
};
$.fn.sendForm = function (url, success, options) { $.fn.sendForm = function (url, success, options) {
var form = $(this); var form = $(this);
options = options || {}; options = options || {};
@@ -734,11 +742,13 @@ $ocms.vbl_send = function (ev) {
if (typeof options.error === 'function') { options.error.call(this, jqXHR); } else { if (typeof options.error === 'function') { options.error.call(this, jqXHR); } else {
$ocms.failure.call(this, jqXHR); $ocms.failure.call(this, jqXHR);
} }
}, complete: function () { }, complete: function (jqXHR) {
form.ldng(0); form.ldng(0);
if (typeof options.complete === 'function') { options.complete.call(this, jqXHR); } if (typeof options.complete === 'function') { options.complete.call(this, jqXHR); }
} }
}; };
var formdata = form.serializeObject();
if (formdata === null) { form.rejectInvalid(); return; }
var fle = form.find('input[type="file"]'); var fle = form.find('input[type="file"]');
postoptions.data = new FormData(); postoptions.data = new FormData();
if (fle.length > 0) { if (fle.length > 0) {
@@ -747,8 +757,7 @@ $ocms.vbl_send = function (ev) {
postoptions.data.append('file_lastmodified', $ocms.isodt(value.lastModifiedDate)); postoptions.data.append('file_lastmodified', $ocms.isodt(value.lastModifiedDate));
}); });
} }
var formdata = form.serializeObject(); $.each(formdata, function (di, dx) {
$.each(formdata || {}, function (di, dx) {
postoptions.data.append(di, dx); postoptions.data.append(di, dx);
}); });
form.ldng(); form.ldng();
@@ -781,7 +790,7 @@ $ocms.login = {
return false; return false;
} else { } else {
var qs = f.serializeObject(); var qs = f.serializeObject();
qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || ''); if (qs === null) { f.rejectInvalid(); return false; }
qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || ''); qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || '');
if (ne(qs.loginaccount) === '' && bool($ocms.auth.accountrequired, true) === true ) { if (ne(qs.loginaccount) === '' && bool($ocms.auth.accountrequired, true) === true ) {
alert($t.l16); alert($t.l16);
@@ -840,6 +849,7 @@ $ocms.login = {
d.find('form').submit(function (e) { d.find('form').submit(function (e) {
e.preventDefault(); e.preventDefault();
var qs = $(this).serializeObject(true), s2 = code === null, u = s2 ?'spwc':'spw'; var qs = $(this).serializeObject(true), s2 = code === null, u = s2 ?'spwc':'spw';
if (qs === null) { $(this).rejectInvalid(); return false; }
$ocms.postXT.call(this, { $ocms.postXT.call(this, {
url: $ocms.url(u), data: qs, complete: function () { url: $ocms.url(u), data: qs, complete: function () {
if (s2) { if (s2) {
+6 -2
View File
@@ -360,6 +360,8 @@
}; };
if (c.find(':input[required]').length > 0) { if (c.find(':input[required]').length > 0) {
$$.dc('note_required', ft).append($$.sc('ind_required', '*')).append($$.s($t.t1 || 'Eingabe erforderlich')); $$.dc('note_required', ft).append($$.sc('ind_required', '*')).append($$.s($t.t1 || 'Eingabe erforderlich'));
}
if (isform === true) { /* shown via .invalid whenever serializeObject rejects the form, required fields or not */
$$.dc('note_invalid', ft).append($$.s($t.t2 || 'Bitte überprüfen Sie Ihre Eingaben im Formular.')); $$.dc('note_invalid', ft).append($$.s($t.t2 || 'Bitte überprüfen Sie Ihre Eingaben im Formular.'));
} }
if (f('cancel') === true) { if (f('cancel') === true) {
@@ -614,7 +616,7 @@
cfm = function (e) { cfm = function (e) {
var c = $(this).ldng(1); var c = $(this).ldng(1);
var values = c.serializeObject(bool(options.checkvalidity, true), { typedvalues: bool(options.typedvalues, false) }); var values = c.serializeObject(bool(options.checkvalidity, true), { typedvalues: bool(options.typedvalues, false) });
if (values === null) { c.ldng(0); return; } if (values === null) { c.rejectInvalid(); return; }
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values); var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
if ((options.url || '') !== ''){ if ((options.url || '') !== ''){
$ocms.postXT({ $ocms.postXT({
@@ -659,7 +661,9 @@
let gf1 = $$.dc('frm').append($ocms.mform(flds).addClass('stacked')); let gf1 = $$.dc('frm').append($ocms.mform(flds).addClass('stacked'));
let f_submit = function (e) { let f_submit = function (e) {
var c = $(this).ldng(1); var c = $(this).ldng(1);
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, c.serializeObject()); var values = c.serializeObject();
if (values === null) { c.rejectInvalid(); return; }
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
$ocms.postXT({ $ocms.postXT({
url: '/vt/login', data: qs, success: function (response) { url: '/vt/login', data: qs, success: function (response) {
if (((response || {}).login || '') !== '') { if (((response || {}).login || '') !== '') {
+31 -8
View File
@@ -725,6 +725,7 @@ $ocms.postXT = function (options) {
} }
if (options.form instanceof jQuery) { if (options.form instanceof jQuery) {
options.data = options.form.serializeObject(); options.data = options.form.serializeObject();
if (options.data === null) { options.form.rejectInvalid(); return; }
options.contentType = 'form-data'; options.contentType = 'form-data';
} else if (options.lzw instanceof jQuery) { } else if (options.lzw instanceof jQuery) {
options.data.lzw = $.ccLZW(options.lzw.serializeAnything(true)).join(','); options.data.lzw = $.ccLZW(options.lzw.serializeAnything(true)).join(',');
@@ -957,6 +958,13 @@ $ocms.vbl_send = function (ev) {
c.toggleClass('invalid', !valid); c.toggleClass('invalid', !valid);
return valid ? result : null; return valid ? result : null;
}; };
/* For a form whose serializeObject() returned null: stop its loading indicator and let the
browser point at the offending field, instead of submitting partial data. */
$.fn.rejectInvalid = function () {
var c = $(this).ldng(0), f = c.closest('form')[0] || c.find('form')[0];
if (f && typeof f.reportValidity === 'function') { f.reportValidity(); }
return c;
};
$.fn.sendForm = function (url, success, options) { $.fn.sendForm = function (url, success, options) {
var form = $(this); var form = $(this);
options = options || {}; options = options || {};
@@ -971,11 +979,13 @@ $ocms.vbl_send = function (ev) {
if (typeof options.error === 'function') { options.error.call(this, jqXHR); } else { if (typeof options.error === 'function') { options.error.call(this, jqXHR); } else {
$ocms.failure.call(this, jqXHR); $ocms.failure.call(this, jqXHR);
} }
}, complete: function () { }, complete: function (jqXHR) {
form.ldng(0); form.ldng(0);
if (typeof options.complete === 'function') { options.complete.call(this, jqXHR); } if (typeof options.complete === 'function') { options.complete.call(this, jqXHR); }
} }
}; };
var formdata = form.serializeObject();
if (formdata === null) { form.rejectInvalid(); return; }
var fle = form.find('input[type="file"]'); var fle = form.find('input[type="file"]');
postoptions.data = new FormData(); postoptions.data = new FormData();
if (fle.length > 0) { if (fle.length > 0) {
@@ -984,8 +994,7 @@ $ocms.vbl_send = function (ev) {
postoptions.data.append('file_lastmodified', $ocms.isodt(value.lastModifiedDate)); postoptions.data.append('file_lastmodified', $ocms.isodt(value.lastModifiedDate));
}); });
} }
var formdata = form.serializeObject(); $.each(formdata, function (di, dx) {
$.each(formdata || {}, function (di, dx) {
postoptions.data.append(di, dx); postoptions.data.append(di, dx);
}); });
form.ldng(); form.ldng();
@@ -1018,7 +1027,7 @@ $ocms.login = {
return false; return false;
} else { } else {
var qs = f.serializeObject(); var qs = f.serializeObject();
qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || ''); if (qs === null) { f.rejectInvalid(); return false; }
qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || ''); qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || '');
if (ne(qs.loginaccount) === '' && bool($ocms.auth.accountrequired, true) === true ) { if (ne(qs.loginaccount) === '' && bool($ocms.auth.accountrequired, true) === true ) {
alert($t.l16); alert($t.l16);
@@ -1077,6 +1086,7 @@ $ocms.login = {
d.find('form').submit(function (e) { d.find('form').submit(function (e) {
e.preventDefault(); e.preventDefault();
var qs = $(this).serializeObject(true), s2 = code === null, u = s2 ?'spwc':'spw'; var qs = $(this).serializeObject(true), s2 = code === null, u = s2 ?'spwc':'spw';
if (qs === null) { $(this).rejectInvalid(); return false; }
$ocms.postXT.call(this, { $ocms.postXT.call(this, {
url: $ocms.url(u), data: qs, complete: function () { url: $ocms.url(u), data: qs, complete: function () {
if (s2) { if (s2) {
@@ -2139,6 +2149,8 @@ class NumArray extends Array {
}; };
if (c.find(':input[required]').length > 0) { if (c.find(':input[required]').length > 0) {
$$.dc('note_required', ft).append($$.sc('ind_required', '*')).append($$.s($t.t1 || 'Eingabe erforderlich')); $$.dc('note_required', ft).append($$.sc('ind_required', '*')).append($$.s($t.t1 || 'Eingabe erforderlich'));
}
if (isform === true) { /* shown via .invalid whenever serializeObject rejects the form, required fields or not */
$$.dc('note_invalid', ft).append($$.s($t.t2 || 'Bitte überprüfen Sie Ihre Eingaben im Formular.')); $$.dc('note_invalid', ft).append($$.s($t.t2 || 'Bitte überprüfen Sie Ihre Eingaben im Formular.'));
} }
if (f('cancel') === true) { if (f('cancel') === true) {
@@ -2393,7 +2405,7 @@ class NumArray extends Array {
cfm = function (e) { cfm = function (e) {
var c = $(this).ldng(1); var c = $(this).ldng(1);
var values = c.serializeObject(bool(options.checkvalidity, true), { typedvalues: bool(options.typedvalues, false) }); var values = c.serializeObject(bool(options.checkvalidity, true), { typedvalues: bool(options.typedvalues, false) });
if (values === null) { c.ldng(0); return; } if (values === null) { c.rejectInvalid(); return; }
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values); var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
if ((options.url || '') !== ''){ if ((options.url || '') !== ''){
$ocms.postXT({ $ocms.postXT({
@@ -2438,7 +2450,9 @@ class NumArray extends Array {
let gf1 = $$.dc('frm').append($ocms.mform(flds).addClass('stacked')); let gf1 = $$.dc('frm').append($ocms.mform(flds).addClass('stacked'));
let f_submit = function (e) { let f_submit = function (e) {
var c = $(this).ldng(1); var c = $(this).ldng(1);
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, c.serializeObject()); var values = c.serializeObject();
if (values === null) { c.rejectInvalid(); return; }
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
$ocms.postXT({ $ocms.postXT({
url: '/vt/login', data: qs, success: function (response) { url: '/vt/login', data: qs, success: function (response) {
if (((response || {}).login || '') !== '') { if (((response || {}).login || '') !== '') {
@@ -3000,9 +3014,18 @@ $fis.resetPass = function (id, fds) {
title: $t.rsp || '', title: $t.rsp || '',
submit: function (e) { submit: function (e) {
var c = $(this).ldng(1); var c = $(this).ldng(1);
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, c.serializeObject(true, { typedvalues: true })); var values = c.serializeObject(true, { typedvalues: true });
if (values === null) { c.rejectInvalid(); return; }
var qs = $.extend({ loginaccount: $ocms.auth.account || '' }, values);
if ((qs.npw || '') !== (qs.npwc || '')) { if ((qs.npw || '') !== (qs.npwc || '')) {
c.find('input[name="npw"]:first')[0].setCustomValidity($t.pnm); /* The browser blocks every later submit while npw carries a custom error, so it
must be cleared as soon as either password field is edited again. */
var npw = c.find('input[name="npw"]:first');
npw[0].setCustomValidity($t.pnm);
c.find('input[name="npw"], input[name="npwc"]').off('input.pnm').on('input.pnm', function () {
npw[0].setCustomValidity('');
});
c.rejectInvalid();
} else { } else {
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('account/changepassword'), data: qs, success: function (response) { url: $ocms.url('account/changepassword'), data: qs, success: function (response) {
+2 -2
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -53,9 +53,10 @@ let $rep = {
}; };
let scl = function (ev) { let scl = function (ev) {
//console.debug(ev); //console.debug(ev);
let btn = $(this), rx = ev.data; let btn = $(this), rx = ev.data, params = btn.serializeObject();
if (params === null) { return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('rep/gct/' + rx.name), data: btn.serializeObject(), success: (response) => { url: $ocms.url('rep/gct/' + rx.name), data: params, success: (response) => {
btn.addClass('selected').siblings().removeClass('selected'); btn.addClass('selected').siblings().removeClass('selected');
$('#listframe').rC('fix').aC('hd'); $('#listframe').rC('fix').aC('hd');
repfrm.html(response); repfrm.html(response);
+1 -1
View File
@@ -1 +1 @@
let $ct={mdl:"Berichte"},$ccol={};function getMonday(e){var t=(e=new Date(e)).getDay(),a=e.getDate()-t+(0==t?-6:1);return new Date(e.setDate(a))}let $rep={init2:function(e,t){e=e||"inv",t=t||{},$ocms.getScript([],(function(){$rep.init3(e,t)}))},init3:async function(e,t){let a=$("#contentframe").empty(),l=($$.dc("cfrm hd").appendTo(a),$$.dc("init_frm",a));$("#topbar").ocmsmenu([]);$("#activemodule").text($ct.mdl);let c=$$.dc("repfrm",l),i=(new Date,[(async()=>{if(await $fis.getAuth("fds_reports")>0){l.IN((function(){}));let e=$("#listframe").empty().aC("fix").rC("hd"),t=function(e){let t=$(this);if((e.params||[]).length>0){let a=$$.dc("ipf",t).click((function(e){e.stopPropagation()}));$.each(e.params,((e,t)=>{let l,c=$$.dc("ig",a).append($$.lbl(t.name));""===(t.valuelist||"")&&(l=$$.i({name:t.name,placeholder:t.placeholder}).appendTo(c),""!==(t.default||"")&&l.val(t.default))}))}},a=function(e){let t=$(this),a=e.data;$ocms.postXT({url:$ocms.url("rep/gct/"+a.name),data:t.serializeObject(),success:e=>{t.addClass("selected").siblings().removeClass("selected"),$("#listframe").rC("fix").aC("hd"),c.html(e)},datatype:"html"})};$ocms.postXT({url:$ocms.url("rep/catalog"),data:{},success:l=>{$.each(l.reports,((l,c)=>{let i=$$.dc("repitm",e).text(c.label).click(c,a);t.call(i,c)}))}})}})(),new Promise(((e,t)=>{}))]);await Promise.all(i)}},$$rep={init2:$rep.init2,auth:{}};export default $$rep; let $ct={mdl:"Berichte"},$ccol={};function getMonday(e){var t=(e=new Date(e)).getDay(),a=e.getDate()-t+(0==t?-6:1);return new Date(e.setDate(a))}let $rep={init2:function(e,t){e=e||"inv",t=t||{},$ocms.getScript([],(function(){$rep.init3(e,t)}))},init3:async function(e,t){let a=$("#contentframe").empty(),l=($$.dc("cfrm hd").appendTo(a),$$.dc("init_frm",a));$("#topbar").ocmsmenu([]);$("#activemodule").text($ct.mdl);let c=$$.dc("repfrm",l),i=(new Date,[(async()=>{if(await $fis.getAuth("fds_reports")>0){l.IN((function(){}));let e=$("#listframe").empty().aC("fix").rC("hd"),t=function(e){let t=$(this);if((e.params||[]).length>0){let a=$$.dc("ipf",t).click((function(e){e.stopPropagation()}));$.each(e.params,((e,t)=>{let l,c=$$.dc("ig",a).append($$.lbl(t.name));""===(t.valuelist||"")&&(l=$$.i({name:t.name,placeholder:t.placeholder}).appendTo(c),""!==(t.default||"")&&l.val(t.default))}))}},a=function(e){let t=$(this),a=e.data,l=t.serializeObject();null!==l&&$ocms.postXT({url:$ocms.url("rep/gct/"+a.name),data:l,success:e=>{t.addClass("selected").siblings().removeClass("selected"),$("#listframe").rC("fix").aC("hd"),c.html(e)},datatype:"html"})};$ocms.postXT({url:$ocms.url("rep/catalog"),data:{},success:l=>{$.each(l.reports,((l,c)=>{let i=$$.dc("repitm",e).text(c.label).click(c,a);t.call(i,c)}))}})}})(),new Promise(((e,t)=>{}))]);await Promise.all(i)}},$$rep={init2:$rep.init2,auth:{}};export default $$rep;
+1
View File
@@ -82,6 +82,7 @@ let $spc = {
let f = c.form[0]; let f = c.form[0];
if (typeof f.reportValidity === 'function' && !f.reportValidity()) { return; } if (typeof f.reportValidity === 'function' && !f.reportValidity()) { return; }
let values = c.form.serializeObject(true, { typedvalues: false }); let values = c.form.serializeObject(true, { typedvalues: false });
if (values === null) { c.form.rejectInvalid(); return; }
if (typeof options.confirm === 'function' && !confirm(options.confirm(values))) { return; } if (typeof options.confirm === 'function' && !confirm(options.confirm(values))) { return; }
c.result.hide(); c.result.hide();
btn.prop('disabled', true).text($spt.working); btn.prop('disabled', true).text($spt.working);
+1 -1
View File
@@ -1 +1 @@
let $spt={mdl:"Spezialfunktionen",intro:"Versteckte Wartungsfunktionen für Administratoren. Jede Aktion wird protokolliert (fds__admin_activity / Anwendungslog).",run:"Ausführen",working:"Wird ausgeführt…",failed:"Die Aktion ist fehlgeschlagen.",ifc_title:"Rechnungsdatei zurücksetzen (Soft-Delete)",ifc_desc:"Sichert die gespeicherte PDF-Datei einer finalisierten Rechnung (DB-Tabelle fds__invoice_file_backups + Kopie im Blob-Archiv) und entfernt sie aus der Rechnung. Beim nächsten Aufruf des Dokuments wird die PDF neu generiert (ADR 0025/0027).",ifc_id:"Interne Rechnungs-ID",ifc_id_ph:"z.B. W7VAL",ifc_id_note:"Die interne ID (Spalte Id), nicht die Rechnungsnummer R2026-….",ifc_btn:"Datei sichern und entfernen",ifc_confirm:'Die Rechnungsdatei von "{0}" wird gesichert und aus der Rechnung entfernt. Fortfahren?',ifc_ok:"Erledigt: Die Datei wurde gesichert und entfernt. Das Dokument wird beim nächsten Aufruf neu generiert.",ifc_ok_blob:"Blob-Backup: {0}",ifc_ok_noblob:"Kein Blob-Backup angelegt (Blob-Archiv deaktiviert, kein Blob vorhanden oder Kopie fehlgeschlagen — siehe Log).",ifc_reason:{invoice_not_found:"Rechnung nicht gefunden.",not_final:"Die Rechnung ist nicht finalisiert — nur finalisierte Rechnungen können zurückgesetzt werden (Entwürfe würden sonst dauerhaft als Entwurf gespeichert).",external:"Externe (MFR-)Dokumente können nicht neu generiert werden.",no_file:"Es ist keine Datei gespeichert — nichts zu tun.",db_error:"Datenbankfehler — Details im Anwendungslog (CodeReference AdminBackupClearInvoiceFile)."}},$spc={init2:function(){$ocms.getScript([],(function(){$spc.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($spt.mdl),$("#topbar").ocmsmenu([]);let e=$$.dc("spcfrm",$("#contentframe"));$$.dc("spc_intro",e).text($spt.intro);let t=$$.dc("spc_grid",e);$.each($spc.cards,(function(e,n){n(t)}))},card:function(e,t,n){let i=$$.dc("spc_card",e),r=$$.dc("spc_card_hd",i);$$.s(t).appendTo(r);let c=$$.dc("spc_card_bd",i);return n&&$$.dc("spc_card_desc",c).text(n),{card:i,body:c,form:$("<form/>",{class:"spc_form",novalidate:"novalidate"}).appendTo(c),result:$$.dc("spc_result",c).hide()}},field:function(e,t,n,i){i=i||{};let r=$$.dc("spc_field",e),c="spc_"+t;$$.lbl(n,{for:c}).appendTo(r);let o=$$.i({id:c,name:t,type:i.type||"text",placeholder:i.placeholder||""}).appendTo(r);return i.required&&o.attr("required","required"),i.pattern&&o.attr("pattern",i.pattern),i.note&&$$.dc("spc_field_note",r).text(i.note),o},showResult:function(e,t,n){e.empty().attr("class","spc_result "+(t?"spc_result_ok":"spc_result_err")),$.each([].concat(n||[]),(function(t,n){n&&$$.dc("spc_result_line",e).text(n)})),e.show()},submit:function(e,t){let n=$$.bbtn(t.button,"spc_btn spc_btn_primary").attr("type","submit").appendTo($$.dc("spc_actions",e.form));e.form.on("submit",(function(i){i.preventDefault();let r=e.form[0];if("function"==typeof r.reportValidity&&!r.reportValidity())return;let c=e.form.serializeObject(!0,{typedvalues:!1});("function"!=typeof t.confirm||confirm(t.confirm(c)))&&(e.result.hide(),n.prop("disabled",!0).text($spt.working),$ocms.postXT($.extend(t.run(c),{success:function(n){let i=t.report(n||{});$spc.showResult(e.result,i.ok,i.lines)},error:function(t){$spc.showResult(e.result,!1,[$spt.failed,t&&t.internalText||""])},complete:function(){n.prop("disabled",!1).text(t.button)}})))}))},cards:[function(e){let t=$spc.card(e,$spt.ifc_title,$spt.ifc_desc);$spc.field(t.form,"id",$spt.ifc_id,{required:!0,placeholder:$spt.ifc_id_ph,note:$spt.ifc_id_note,pattern:"[A-Za-z0-9]{1,10}"}),$spc.submit(t,{button:$spt.ifc_btn,confirm:function(e){return $spt.ifc_confirm.replace("{0}",e.id)},run:function(e){return{url:$ocms.url("admin/backupclearinvoicefile/"+encodeURIComponent((e.id||"").trim())),method:"post",data:{}}},report:function(e){let t=e.backup||{};return!0===t.success?{ok:!0,lines:[$spt.ifc_ok,t.blobBackupUri?$spt.ifc_ok_blob.replace("{0}",t.blobBackupUri):$spt.ifc_ok_noblob]}:{ok:!1,lines:[$spt.ifc_reason[t.reason]||$spt.failed+" ("+(t.reason||"?")+")"]}}})}]},$$spc={init2:$spc.init2,auth:{}};export default $$spc; let $spt={mdl:"Spezialfunktionen",intro:"Versteckte Wartungsfunktionen für Administratoren. Jede Aktion wird protokolliert (fds__admin_activity / Anwendungslog).",run:"Ausführen",working:"Wird ausgeführt…",failed:"Die Aktion ist fehlgeschlagen.",ifc_title:"Rechnungsdatei zurücksetzen (Soft-Delete)",ifc_desc:"Sichert die gespeicherte PDF-Datei einer finalisierten Rechnung (DB-Tabelle fds__invoice_file_backups + Kopie im Blob-Archiv) und entfernt sie aus der Rechnung. Beim nächsten Aufruf des Dokuments wird die PDF neu generiert (ADR 0025/0027).",ifc_id:"Interne Rechnungs-ID",ifc_id_ph:"z.B. W7VAL",ifc_id_note:"Die interne ID (Spalte Id), nicht die Rechnungsnummer R2026-….",ifc_btn:"Datei sichern und entfernen",ifc_confirm:'Die Rechnungsdatei von "{0}" wird gesichert und aus der Rechnung entfernt. Fortfahren?',ifc_ok:"Erledigt: Die Datei wurde gesichert und entfernt. Das Dokument wird beim nächsten Aufruf neu generiert.",ifc_ok_blob:"Blob-Backup: {0}",ifc_ok_noblob:"Kein Blob-Backup angelegt (Blob-Archiv deaktiviert, kein Blob vorhanden oder Kopie fehlgeschlagen — siehe Log).",ifc_reason:{invoice_not_found:"Rechnung nicht gefunden.",not_final:"Die Rechnung ist nicht finalisiert — nur finalisierte Rechnungen können zurückgesetzt werden (Entwürfe würden sonst dauerhaft als Entwurf gespeichert).",external:"Externe (MFR-)Dokumente können nicht neu generiert werden.",no_file:"Es ist keine Datei gespeichert — nichts zu tun.",db_error:"Datenbankfehler — Details im Anwendungslog (CodeReference AdminBackupClearInvoiceFile)."}},$spc={init2:function(){$ocms.getScript([],(function(){$spc.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($spt.mdl),$("#topbar").ocmsmenu([]);let e=$$.dc("spcfrm",$("#contentframe"));$$.dc("spc_intro",e).text($spt.intro);let t=$$.dc("spc_grid",e);$.each($spc.cards,(function(e,n){n(t)}))},card:function(e,t,n){let i=$$.dc("spc_card",e),r=$$.dc("spc_card_hd",i);$$.s(t).appendTo(r);let c=$$.dc("spc_card_bd",i);return n&&$$.dc("spc_card_desc",c).text(n),{card:i,body:c,form:$("<form/>",{class:"spc_form",novalidate:"novalidate"}).appendTo(c),result:$$.dc("spc_result",c).hide()}},field:function(e,t,n,i){i=i||{};let r=$$.dc("spc_field",e),c="spc_"+t;$$.lbl(n,{for:c}).appendTo(r);let o=$$.i({id:c,name:t,type:i.type||"text",placeholder:i.placeholder||""}).appendTo(r);return i.required&&o.attr("required","required"),i.pattern&&o.attr("pattern",i.pattern),i.note&&$$.dc("spc_field_note",r).text(i.note),o},showResult:function(e,t,n){e.empty().attr("class","spc_result "+(t?"spc_result_ok":"spc_result_err")),$.each([].concat(n||[]),(function(t,n){n&&$$.dc("spc_result_line",e).text(n)})),e.show()},submit:function(e,t){let n=$$.bbtn(t.button,"spc_btn spc_btn_primary").attr("type","submit").appendTo($$.dc("spc_actions",e.form));e.form.on("submit",(function(i){i.preventDefault();let r=e.form[0];if("function"==typeof r.reportValidity&&!r.reportValidity())return;let c=e.form.serializeObject(!0,{typedvalues:!1});null!==c?("function"!=typeof t.confirm||confirm(t.confirm(c)))&&(e.result.hide(),n.prop("disabled",!0).text($spt.working),$ocms.postXT($.extend(t.run(c),{success:function(n){let i=t.report(n||{});$spc.showResult(e.result,i.ok,i.lines)},error:function(t){$spc.showResult(e.result,!1,[$spt.failed,t&&t.internalText||""])},complete:function(){n.prop("disabled",!1).text(t.button)}}))):e.form.rejectInvalid()}))},cards:[function(e){let t=$spc.card(e,$spt.ifc_title,$spt.ifc_desc);$spc.field(t.form,"id",$spt.ifc_id,{required:!0,placeholder:$spt.ifc_id_ph,note:$spt.ifc_id_note,pattern:"[A-Za-z0-9]{1,10}"}),$spc.submit(t,{button:$spt.ifc_btn,confirm:function(e){return $spt.ifc_confirm.replace("{0}",e.id)},run:function(e){return{url:$ocms.url("admin/backupclearinvoicefile/"+encodeURIComponent((e.id||"").trim())),method:"post",data:{}}},report:function(e){let t=e.backup||{};return!0===t.success?{ok:!0,lines:[$spt.ifc_ok,t.blobBackupUri?$spt.ifc_ok_blob.replace("{0}",t.blobBackupUri):$spt.ifc_ok_noblob]}:{ok:!1,lines:[$spt.ifc_reason[t.reason]||$spt.failed+" ("+(t.reason||"?")+")"]}}})}]},$$spc={init2:$spc.init2,auth:{}};export default $$spc;
+3
View File
@@ -152,6 +152,7 @@ let $usr = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usr/create'), data: qs, success: function (response) { url: $ocms.url('usr/create'), data: qs, success: function (response) {
alert(response.emailed === true ? $uct.create_sent : $uct.create_noemail); alert(response.emailed === true ? $uct.create_sent : $uct.create_noemail);
@@ -187,6 +188,7 @@ let $usr = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usr/pwdreset'), data: qs, success: function (response) { url: $ocms.url('usr/pwdreset'), data: qs, success: function (response) {
alert(response.emailed === true ? $uct.pwdreset_sent : $uct.pwdreset_noemail); alert(response.emailed === true ? $uct.pwdreset_sent : $uct.pwdreset_noemail);
@@ -208,6 +210,7 @@ let $usr = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usr/email'), data: qs, success: function () { url: $ocms.url('usr/email'), data: qs, success: function () {
c.trigger('modal_close'); c.trigger('modal_close');
File diff suppressed because one or more lines are too long
+2
View File
@@ -78,6 +78,7 @@ let $ugp = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usrgrp/create'), data: qs, success: function () { url: $ocms.url('usrgrp/create'), data: qs, success: function () {
c.trigger('modal_close'); c.trigger('modal_close');
@@ -99,6 +100,7 @@ let $ugp = {
submit: function (e) { submit: function (e) {
let c = $(this).ldng(1); let c = $(this).ldng(1);
let qs = c.serializeObject(true, { typedvalues: true }); let qs = c.serializeObject(true, { typedvalues: true });
if (qs === null) { c.rejectInvalid(); return; }
$ocms.postXT({ $ocms.postXT({
url: $ocms.url('usrgrp/rename'), data: qs, success: function () { url: $ocms.url('usrgrp/rename'), data: qs, success: function () {
c.trigger('modal_close'); c.trigger('modal_close');
+1 -1
View File
@@ -1 +1 @@
let $ugt={mdl:"Gruppenverwaltung",rel:"Neu Laden",col_name:"Name",loadfail:"Die Gruppenliste konnte nicht geladen werden.",empty:"Es sind keine Benutzergruppen vorhanden.",newgroup:"Neue Gruppe",newgroup_btn:"Anlegen",name_field:"Gruppenname",rename:"Gruppe umbenennen",rename_btn:"Speichern",delete:"Gruppe löschen",delete_confirm:'Soll die Gruppe "{0}" wirklich gelöscht werden? Die Mitgliedschaft aller Benutzer in dieser Gruppe geht dabei verloren.',err:{save_failed:"Die Gruppe konnte nicht gespeichert werden.",delete_failed:"Die Gruppe konnte nicht gelöscht werden."},err_generic:"Die Änderung konnte nicht gespeichert werden."},$ugp={init2:function(){$ocms.getScript([],(function(){$ugp.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($ugt.mdl),$ugp.eM(),$ugp.renderLst()},eM:function(e){let n=[{lbl:$ugt.newgroup_btn,fnc:$ugp.createDlg}];return!0===bool(e,!1)&&n.push({lbl:$ugt.rel,fnc:$ugp.renderLst}),$("#topbar").ocmsmenu(n)},iMn:function(e){return $("#topbar").ocmsmenu([{lbl:$ugt.rename,fnc:function(){$ugp.renameDlg(e)}},{lbl:$ugt.delete,fnc:function(){$ugp.deleteGroup(e)}}])},renderLst:function(){let e=$fis.frm_list(!0,!0).ldng(1),n=$$.dc("ugpfrm",e);$ugp.eM(!0),$ocms.postXT({url:$ocms.url("usrgrp/list"),success:function(e){n.empty();let t=$$.dc("ugp_grid",n),r=$$.dc("ugp_row ugp_row_hd",t);$$.dc("ugp_col_name",r).text($ugt.col_name),0!==(e.groups||[]).length?$.each(e.groups||[],(function(e,n){let r=$$.dc("ugp_row",t,{id:"ugp"+n.usergroup_id});$$.dc("ugp_col_name",r).text(n.name||""),r.click((function(){r.toggleClass("selected").siblings().rC("selected"),!0===r.is(".selected")?$ugp.iMn(n):$ugp.eM(!0)}))})):$$.dc("ugp_empty",t).text($ugt.empty)},error:function(){n.empty(),$$.dc("ugp_err",n).text($ugt.loadfail)},complete:function(){e.ldng(0)}})},createDlg:function(){$ocms.dlgform([{name:"name",label:$ugt.name_field,type:"string",required:!0}],{title:$ugt.newgroup,button:$ugt.newgroup_btn,submit:function(e){let n=$(this).ldng(1),t=n.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usrgrp/create"),data:t,success:function(){n.trigger("modal_close")},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){n.ldng(0),$ugp.renderLst()}})}})},renameDlg:function(e){$ocms.dlgform([{name:"usergroup_id",type:"hidden",value:e.usergroup_id},{name:"name",label:$ugt.name_field,type:"string",required:!0,value:e.name}],{title:$ugt.rename,button:$ugt.rename_btn,submit:function(e){let n=$(this).ldng(1),t=n.serializeObject(!0,{typedvalues:!0});$ocms.postXT({url:$ocms.url("usrgrp/rename"),data:t,success:function(){n.trigger("modal_close")},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){n.ldng(0),$ugp.renderLst()}})}})},deleteGroup:function(e){confirm(string($ugt.delete_confirm,[e.name||""]))&&$ocms.postXT({url:$ocms.url("usrgrp/delete"),data:{usergroup_id:e.usergroup_id},success:function(){$ugp.eM(!0)},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){$ugp.renderLst()}})}},$$ugp={init2:$ugp.init2,auth:{}};export default $$ugp; let $ugt={mdl:"Gruppenverwaltung",rel:"Neu Laden",col_name:"Name",loadfail:"Die Gruppenliste konnte nicht geladen werden.",empty:"Es sind keine Benutzergruppen vorhanden.",newgroup:"Neue Gruppe",newgroup_btn:"Anlegen",name_field:"Gruppenname",rename:"Gruppe umbenennen",rename_btn:"Speichern",delete:"Gruppe löschen",delete_confirm:'Soll die Gruppe "{0}" wirklich gelöscht werden? Die Mitgliedschaft aller Benutzer in dieser Gruppe geht dabei verloren.',err:{save_failed:"Die Gruppe konnte nicht gespeichert werden.",delete_failed:"Die Gruppe konnte nicht gelöscht werden."},err_generic:"Die Änderung konnte nicht gespeichert werden."},$ugp={init2:function(){$ocms.getScript([],(function(){$ugp.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($ugt.mdl),$ugp.eM(),$ugp.renderLst()},eM:function(e){let n=[{lbl:$ugt.newgroup_btn,fnc:$ugp.createDlg}];return!0===bool(e,!1)&&n.push({lbl:$ugt.rel,fnc:$ugp.renderLst}),$("#topbar").ocmsmenu(n)},iMn:function(e){return $("#topbar").ocmsmenu([{lbl:$ugt.rename,fnc:function(){$ugp.renameDlg(e)}},{lbl:$ugt.delete,fnc:function(){$ugp.deleteGroup(e)}}])},renderLst:function(){let e=$fis.frm_list(!0,!0).ldng(1),n=$$.dc("ugpfrm",e);$ugp.eM(!0),$ocms.postXT({url:$ocms.url("usrgrp/list"),success:function(e){n.empty();let t=$$.dc("ugp_grid",n),r=$$.dc("ugp_row ugp_row_hd",t);$$.dc("ugp_col_name",r).text($ugt.col_name),0!==(e.groups||[]).length?$.each(e.groups||[],(function(e,n){let r=$$.dc("ugp_row",t,{id:"ugp"+n.usergroup_id});$$.dc("ugp_col_name",r).text(n.name||""),r.click((function(){r.toggleClass("selected").siblings().rC("selected"),!0===r.is(".selected")?$ugp.iMn(n):$ugp.eM(!0)}))})):$$.dc("ugp_empty",t).text($ugt.empty)},error:function(){n.empty(),$$.dc("ugp_err",n).text($ugt.loadfail)},complete:function(){e.ldng(0)}})},createDlg:function(){$ocms.dlgform([{name:"name",label:$ugt.name_field,type:"string",required:!0}],{title:$ugt.newgroup,button:$ugt.newgroup_btn,submit:function(e){let n=$(this).ldng(1),t=n.serializeObject(!0,{typedvalues:!0});null!==t?$ocms.postXT({url:$ocms.url("usrgrp/create"),data:t,success:function(){n.trigger("modal_close")},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){n.ldng(0),$ugp.renderLst()}}):n.rejectInvalid()}})},renameDlg:function(e){$ocms.dlgform([{name:"usergroup_id",type:"hidden",value:e.usergroup_id},{name:"name",label:$ugt.name_field,type:"string",required:!0,value:e.name}],{title:$ugt.rename,button:$ugt.rename_btn,submit:function(e){let n=$(this).ldng(1),t=n.serializeObject(!0,{typedvalues:!0});null!==t?$ocms.postXT({url:$ocms.url("usrgrp/rename"),data:t,success:function(){n.trigger("modal_close")},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){n.ldng(0),$ugp.renderLst()}}):n.rejectInvalid()}})},deleteGroup:function(e){confirm(string($ugt.delete_confirm,[e.name||""]))&&$ocms.postXT({url:$ocms.url("usrgrp/delete"),data:{usergroup_id:e.usergroup_id},success:function(){$ugp.eM(!0)},error:function(e){let n=((e||{}).responseJSON||{}).error;alert($ugt.err[n]||$ugt.err_generic)},complete:function(){$ugp.renderLst()}})}},$$ugp={init2:$ugp.init2,auth:{}};export default $$ugp;
+14 -4
View File
@@ -542,6 +542,7 @@ $ocms.postXT = function (options) {
} }
if (options.form instanceof jQuery) { if (options.form instanceof jQuery) {
options.data = options.form.serializeObject(); options.data = options.form.serializeObject();
if (options.data === null) { options.form.rejectInvalid(); return; }
options.contentType = 'form-data'; options.contentType = 'form-data';
} else if (options.lzw instanceof jQuery) { } else if (options.lzw instanceof jQuery) {
options.data.lzw = $.ccLZW(options.lzw.serializeAnything(true)).join(','); options.data.lzw = $.ccLZW(options.lzw.serializeAnything(true)).join(',');
@@ -774,6 +775,13 @@ $ocms.vbl_send = function (ev) {
c.toggleClass('invalid', !valid); c.toggleClass('invalid', !valid);
return valid ? result : null; return valid ? result : null;
}; };
/* For a form whose serializeObject() returned null: stop its loading indicator and let the
browser point at the offending field, instead of submitting partial data. */
$.fn.rejectInvalid = function () {
var c = $(this).ldng(0), f = c.closest('form')[0] || c.find('form')[0];
if (f && typeof f.reportValidity === 'function') { f.reportValidity(); }
return c;
};
$.fn.sendForm = function (url, success, options) { $.fn.sendForm = function (url, success, options) {
var form = $(this); var form = $(this);
options = options || {}; options = options || {};
@@ -788,11 +796,13 @@ $ocms.vbl_send = function (ev) {
if (typeof options.error === 'function') { options.error.call(this, jqXHR); } else { if (typeof options.error === 'function') { options.error.call(this, jqXHR); } else {
$ocms.failure.call(this, jqXHR); $ocms.failure.call(this, jqXHR);
} }
}, complete: function () { }, complete: function (jqXHR) {
form.ldng(0); form.ldng(0);
if (typeof options.complete === 'function') { options.complete.call(this, jqXHR); } if (typeof options.complete === 'function') { options.complete.call(this, jqXHR); }
} }
}; };
var formdata = form.serializeObject();
if (formdata === null) { form.rejectInvalid(); return; }
var fle = form.find('input[type="file"]'); var fle = form.find('input[type="file"]');
postoptions.data = new FormData(); postoptions.data = new FormData();
if (fle.length > 0) { if (fle.length > 0) {
@@ -801,8 +811,7 @@ $ocms.vbl_send = function (ev) {
postoptions.data.append('file_lastmodified', $ocms.isodt(value.lastModifiedDate)); postoptions.data.append('file_lastmodified', $ocms.isodt(value.lastModifiedDate));
}); });
} }
var formdata = form.serializeObject(); $.each(formdata, function (di, dx) {
$.each(formdata || {}, function (di, dx) {
postoptions.data.append(di, dx); postoptions.data.append(di, dx);
}); });
form.ldng(); form.ldng();
@@ -835,7 +844,7 @@ $ocms.login = {
return false; return false;
} else { } else {
var qs = f.serializeObject(); var qs = f.serializeObject();
qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || ''); if (qs === null) { f.rejectInvalid(); return false; }
qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || ''); qs.loginaccount = ne(qs.loginaccount, $ocms.auth.account || $ocms.auth.requestedaccount || '');
if (ne(qs.loginaccount) === '' && bool($ocms.auth.accountrequired, true) === true ) { if (ne(qs.loginaccount) === '' && bool($ocms.auth.accountrequired, true) === true ) {
alert($t.l16); alert($t.l16);
@@ -894,6 +903,7 @@ $ocms.login = {
d.find('form').submit(function (e) { d.find('form').submit(function (e) {
e.preventDefault(); e.preventDefault();
var qs = $(this).serializeObject(true), s2 = code === null, u = s2 ?'spwc':'spw'; var qs = $(this).serializeObject(true), s2 = code === null, u = s2 ?'spwc':'spw';
if (qs === null) { $(this).rejectInvalid(); return false; }
$ocms.postXT.call(this, { $ocms.postXT.call(this, {
url: $ocms.url(u), data: qs, complete: function () { url: $ocms.url(u), data: qs, complete: function () {
if (s2) { if (s2) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+130
View File
@@ -0,0 +1,130 @@
// Run with: node --test e2e_tests/form-validation.cjs
// Every caller of serializeObject() must treat a null result (invalid form) as "do not submit":
// stop the loading indicator, keep the form open and let the browser show the offending field.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { chromium } = require('playwright');
const root = path.resolve(__dirname, '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('invalid forms are rejected instead of submitted with partial data', async () => {
const browser = await chromium.launch({ headless: true,
channel: process.env.PLAYWRIGHT_CHANNEL || (process.platform === 'win32' ? 'msedge' : undefined) });
try {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.setContent('<html><body></body></html>');
await page.addScriptTag({ content: 'var $fis = {};' });
for (const file of [
'Fuchs/wwwroot/lib/jquery/jquery.js',
'Fuchs/js/intranet/oci_texts_basic_de.js',
'Fuchs/js/intranet/oci_basic.js',
'Fuchs/js/intranet/oci_mainbase.js',
'Fuchs/js/intranet/oci_main.js',
'Fuchs/js/intranet/fis_texts_gui_de.js',
'Fuchs/js/intranet/fis_main.js'
]) {
await page.addScriptTag({ content: read(file) });
}
const r = await page.evaluate(() => {
const out = {};
const posts = [];
const realPostXT = $ocms.postXT;
$ocms.postXT = function (o) { posts.push(o); if (o && typeof o.complete === 'function') { o.complete.call(this, { status: 200 }); } };
const ajax = [];
const realAjax = $.ajax;
$.ajax = (o) => { ajax.push(o); return $.Deferred().promise(); };
/* A required hidden field is never checked by the browser, so serializeObject is the only guard. */
const invalidForm = () => $('<form><input type="hidden" name="h" required value=""><input name="t" value="x"></form>').appendTo('body');
// rejectInvalid: stops the spinner and asks the browser to report the field
let reported = 0;
const f1 = invalidForm().ldng(1);
f1[0].reportValidity = () => { reported++; return false; };
f1.rejectInvalid();
out.rejectStopsLoading = !f1.hasClass('loading');
out.rejectReported = reported;
// postXT({ form }) sends nothing for an invalid form
realPostXT.call(document, { url: '/x', form: invalidForm() });
out.postXTAjaxCalls = ajax.length;
$.ajax = realAjax;
// sendForm: no post for an invalid form; a valid one posts and calls options.complete without throwing
posts.length = 0;
invalidForm().sendForm('/x');
out.sendFormInvalidPosts = posts.length;
let completed = null;
$('<form><input name="t" value="x"></form>').appendTo('body').sendForm('/x', null, { complete: (x) => { completed = x; } });
out.sendFormValidPosts = posts.length;
out.sendFormCompleteArg = completed;
// login.send: no TypeError on an invalid form, nothing posted
posts.length = 0;
const lf = invalidForm();
out.loginSendResult = $ocms.login.send.call(lf[0], { preventDefault() {} });
out.loginSendPosts = posts.length;
// dlg: the "check your input" note exists on every form dialog, even without required fields
const d = $ocms.dlg($$.dc('x').append('<input name="a">'), { form: true, confirm: () => {} });
out.noteInvalidWithoutRequired = d.ft.find('.note_invalid').length;
d.c.trigger('modal_close');
// dlgform (success mode): invalid form keeps the dialog open and never calls success
let succeeded = false;
const d2 = $ocms.dlgform([{ name: 'n', type: 'hidden', required: true, value: '' }], { success: () => { succeeded = true; } });
const form2 = $('body > .modal:last form');
form2.trigger('submit');
out.dlgformSucceeded = succeeded;
out.dlgformStillOpen = $('body > .modal').length;
out.dlgformLoading = form2.hasClass('loading');
out.dlgformInvalidNoteShown = form2.hasClass('invalid') && form2.find('.note_invalid').length;
form2.trigger('modal_close');
// password change: mismatch must not leave the spinner on or lock the form for good
window.confirm = () => true;
posts.length = 0;
$fis.resetPass();
const pf = $('body > .modal:last form');
pf.find('[name=opw]').val('old-password');
pf.find('[name=npw]').val('new-password-1');
pf.find('[name=npwc]').val('new-password-2');
pf.find('[name=code]').val('123456');
pf.trigger('submit');
const npw = pf.find('[name=npw]')[0];
out.mismatchPosts = posts.filter((p) => /changepassword/.test(p.url)).length;
out.mismatchLoading = pf.hasClass('loading');
out.mismatchMessage = npw.validationMessage;
pf.find('[name=npwc]').val('new-password-1').trigger('input');
out.afterEditValid = npw.checkValidity();
pf.trigger('submit');
out.matchPosts = posts.filter((p) => /changepassword/.test(p.url)).length;
return out;
});
assert.deepEqual(errors, []);
assert.equal(r.rejectStopsLoading, true);
assert.equal(r.rejectReported, 1);
assert.equal(r.postXTAjaxCalls, 0, 'postXT must not send an invalid form');
assert.equal(r.sendFormInvalidPosts, 0, 'sendForm must not send an invalid form');
assert.equal(r.sendFormValidPosts, 1);
assert.deepEqual(r.sendFormCompleteArg, { status: 200 }, 'sendForm passes the jqXHR to options.complete');
assert.equal(r.loginSendResult, false);
assert.equal(r.loginSendPosts, 0);
assert.equal(r.noteInvalidWithoutRequired, 1);
assert.equal(r.dlgformSucceeded, false);
assert.equal(r.dlgformStillOpen, 1);
assert.equal(r.dlgformLoading, false);
assert.ok(r.dlgformInvalidNoteShown);
assert.equal(r.mismatchPosts, 0);
assert.equal(r.mismatchLoading, false, 'spinner must stop on a password mismatch');
assert.equal(r.mismatchMessage, 'Die Passwörter stimmen nicht überein');
assert.equal(r.afterEditValid, true, 'editing a password field must clear the mismatch error');
assert.equal(r.matchPosts, 1);
} finally {
await browser.close();
}
});