Änderungen von Dokument ClassEditSheet

Zuletzt geändert von Holger Engels am 2026/09/22 10:06

Von Version 2.1
bearbeitet von Holger Engels
am 2026/09/22 10:06
Änderungskommentar: Install extension [org.xwiki.platform:xwiki-platform-appwithinminutes-ui/18.7.0]
Auf Version 1.1
bearbeitet von Holger Engels
am 2025/08/05 10:26
Änderungskommentar: Install extension [org.xwiki.platform:xwiki-platform-appwithinminutes-ui/17.6.0]

Zusammenfassung

Details

Seiteneigenschaften
Dokument-Autor
... ... @@ -1,1 +1,1 @@
1 -XWiki.holgerengels
1 +XWiki.hengels
Inhalt
... ... @@ -329,9 +329,6 @@
329 329   #set ($xclass = $doc.getxWikiClass().getXWikiClass())
330 330   #set ($discard = $xclass.addField($field.name, $field))
331 331   #set ($discard = $field.setObject($xclass))
332 - ## Set the class document author to the current user so that customDisplay code is rendered with the current user's
333 - ## rights, not the class document author's rights.
334 - #set ($discard = $doc.document.authors.setEffectiveMetadataAuthor($services.user.currentUserReference))
335 335   #displayField($doc.getxWikiClass().get($field.name) $formFieldDoc)
336 336   #else
337 337   Unsupported form field.
... ... @@ -384,9 +384,6 @@
384 384   ##
385 385   ## Display the field.
386 386   #set ($field = $doc.getxWikiClass().get($fieldName))
387 - ## Set the class document author to the current user so that customDisplay code is rendered with the current user's
388 - ## rights, not the class document author's rights.
389 - #set ($discard = $doc.document.authors.setEffectiveMetadataAuthor($services.user.currentUserReference))
390 390   #displayPropertyEditInput($field, "${doc.fullName}_0_", $object)
391 391  #end
392 392  
... ... @@ -395,9 +395,6 @@
395 395   *#
396 396  #macro (displayEditForm)
397 397   #set ($discard = $xwiki.jsx.use('AppWithinMinutes.ClassEditSheet'))
398 - ## Reusable helper used to recover gracefully when a preview or field-add request is rejected because the CSRF
399 - ## token expired (e.g. after a server restart).
400 - #set ($discard = $xwiki.jsx.use('AppWithinMinutes.Code.CSRFTokenRefresh'))
401 401   #set ($discard = $xwiki.ssx.use('AppWithinMinutes.ClassEditSheet'))
402 402   #set ($discard = $xwiki.ssx.use('AppWithinMinutes.ClassSheetGenerator'))
403 403   #if ("$!request.wizard" == 'true')
... ... @@ -416,21 +416,10 @@
416 416   * Displays either the edit class form or a new form field. The later is used when adding a new form field via AJAX.
417 417   *#
418 418  #macro (doEdit)
419 - #if ("$!request.field" != '' || "$!request.preview" == 'true')
420 - ## CSRF protection.
421 - #if (!$services.csrf.isTokenValid($request.form_token))
422 - $response.setStatus(403)
423 - ## Return a fresh token (using the same JSON shape as XWikiAction#csrfTokenCheck) so that the client can recover
424 - ## from an expired token (e.g. after a server restart) by refreshing it, after an explicit user confirmation,
425 - ## and retrying the request. Returning the token in the response is safe because the same-origin policy prevents
426 - ## a CSRF attacker from reading it.
427 - $response.setContentType('application/json')
428 - $jsontool.serialize({'errorType': 'CSRF', 'newToken': $services.csrf.getToken()})
429 - #elseif ("$!request.field" != '')
430 - #displayNewField()
431 - #else
432 - #previewField()
433 - #end
410 + #if ("$!request.field" != '')
411 + #displayNewField()
412 + #elseif ("$!request.preview" == 'true')
413 + #previewField()
434 434   #else
435 435   ## Make sure that only the sheet content is rendered when the class is saved using AJAX.
436 436   <div class="hidden">
... ... @@ -438,9 +438,6 @@
438 438   #if ($request.wizard == 'true')
439 439   ## Preserve the wizard mode.
440 440   <input type="hidden" name="wizard" value="true" />
441 - ## Provide the URL of the next wizard step as xredirect value (used by the AJAX Save & View implementation).
442 - #set ($appWizardNextStepURL = "#getNextStepURL()")
443 - <input type="hidden" id="xredirect" name="xredirect" value="$escapetool.xml($appWizardNextStepURL)" />
444 444   #end
445 445   ## Compute the application title to be used as the wizard step title.
446 446   #getAppTitle
... ... @@ -678,26 +678,6 @@
678 678  #end
679 679  
680 680  #**
681 - * Prints the URL of the next wizard step.
682 - *#
683 -#macro (getNextStepURL)
684 - #set ($className = $stringtool.removeEnd($doc.fullName, 'Class'))
685 - #set ($templateProviderReference = $services.model.resolveDocument("${className}TemplateProvider"))
686 - #set ($queryString = {
687 - 'wizard': true,
688 - 'sheet': 'AppWithinMinutes.TemplateProviderEditSheet'
689 - })
690 - #if (!$xwiki.exists($templateProviderReference))
691 - #set ($discard = $queryString.putAll({
692 - 'form_token': $services.csrf.getToken(),
693 - 'template': 'XWiki.TemplateProviderTemplate',
694 - 'parent': $doc.fullName
695 - }))
696 - #end
697 -$xwiki.getURL($templateProviderReference, 'edit', $escapetool.url($queryString))##
698 -#end
699 -
700 -#**
701 701   * Updates and saves the class definition, the class sheet and the class template.
702 702   *#
703 703  #macro (doSave)
... ... @@ -708,19 +708,24 @@
708 708   #updateAndSaveSheet
709 709   #updateAndSaveTranslations
710 710   #if ($action == 'save')
711 - #if ($request.getHeader('X-Requested-With') == 'XMLHttpRequest')
712 - ## When saving through AJAX, don't redirect and don't render an error page. Let the client-side handle both.
713 - #if ($errorMessage)
714 - $response.setStatus(400)
715 - #rawResponse($errorMessage, 'text/plain')
716 - #else
717 - $response.setStatus(204)
718 - #end
719 - #elseif ($errorMessage)
668 + #if ($errorMessage)
720 720   #error($errorMessage)
721 721   #elseif ("$!request.wizard" == 'true')
722 722   ## Redirect to next wizard step.
723 - $response.sendRedirect("#getNextStepURL()")
672 + #set ($className = $stringtool.removeEnd($doc.fullName, 'Class'))
673 + #set ($templateProviderReference = $services.model.resolveDocument("${className}TemplateProvider"))
674 + #set ($queryString = {
675 + 'wizard': true,
676 + 'sheet': 'AppWithinMinutes.TemplateProviderEditSheet'
677 + })
678 + #if (!$xwiki.exists($templateProviderReference))
679 + #set ($discard = $queryString.putAll({
680 + 'form_token': $services.csrf.getToken(),
681 + 'template': 'XWiki.TemplateProviderTemplate',
682 + 'parent': $doc.fullName
683 + }))
684 + #end
685 + $response.sendRedirect($xwiki.getURL($templateProviderReference, 'edit', $escapetool.url($queryString)))
724 724   #else
725 725   ## Redirect to view mode.
726 726   $response.sendRedirect($doc.getURL())
... ... @@ -727,8 +727,7 @@
727 727   #end
728 728   #else
729 729   #if ($errorMessage)
730 - $response.setStatus(400)
731 - #rawResponse($errorMessage, 'text/plain')
692 + $response.sendError(400, $errorMessage)
732 732   #else
733 733   $response.setStatus(204)
734 734   #end
XWiki.JavaScriptExtension[2]
Code
... ... @@ -10,42 +10,15 @@
10 10   enhance : function(data) {
11 11   this.container.insert({top: this._createToolBox()});
12 12   if (data) {
13 - this._loadField(data);
13 + this.container.addClassName('loading');
14 + new Ajax.Request(data, {
15 + method: 'get',
16 + onSuccess: this._display.bind(this)
17 + });
14 14   } else {
15 15   this._display(null);
16 16   }
17 17   },
18 - _loadField : function (url) {
19 - // The field URL embeds the CSRF token that was valid when the page was rendered. Always refresh it from the
20 - // current form_token input (the single source of truth, updated when the token is refreshed after an expired
21 - // token recovery) so that, once the token has been refreshed, adding further fields does not prompt again.
22 - const formToken = this.container.up('form')?.down('input[name="form_token"]');
23 - if (formToken) {
24 - const fieldURL = new URL(url, window.location.href);
25 - fieldURL.searchParams.set('form_token', formToken.value);
26 - url = fieldURL.toString();
27 - }
28 - this.container.addClassName('loading');
29 - const self = this;
30 - new Ajax.Request(url, {
31 - method: 'get',
32 - onSuccess: this._display.bind(this),
33 - // A 403 means the CSRF token expired (e.g. the server was restarted while the user was editing). Offer to
34 - // refresh the token and retry, instead of leaving the loading spinner spinning forever.
35 - on403: function (response) {
36 - this.container.removeClassName('loading');
37 - require(['xwiki-appwithinminutes-csrf'], function (csrf) {
38 - csrf.recoverFromExpiredToken(response, function () {
39 - // The token has been refreshed in the form: retry, _loadField will pick up the fresh token.
40 - self._loadField(url);
41 - });
42 - });
43 - }.bind(this),
44 - onFailure: function () {
45 - this.container.removeClassName('loading');
46 - }.bind(this)
47 - });
48 - },
49 49   getContainer : function() {
50 50   return this.container;
51 51   },
... ... @@ -165,12 +165,6 @@
165 165   // Add parameters required by the AJAX preview request.
166 166   this.previewData.set('preview', true);
167 167   this.previewData.set('xpage', 'plain');
168 - // Find and add the mandatory CSRF token.
169 - const editForm = this.container.up('form');
170 - const formToken = editForm && editForm.down('input[name="form_token"]');
171 - if (formToken) {
172 - this.previewData.set('form_token', formToken.value);
173 - }
174 174   },
175 175   _maybePreview : function() {
176 176   if(!this.previewData) {
... ... @@ -202,7 +202,6 @@
202 202   var preview = this.getViewer().down('dd');
203 203   if (preview) {
204 204   preview.addClassName('loading');
205 - const self = this;
206 206   // Firefox 3.6 doesn't resolve the empty string to the current page URL so we have to explicitly specify it.
207 207   new Ajax.Request(window.location.href, {
208 208   parameters: data.toQueryString(),
... ... @@ -212,20 +212,6 @@
212 212   this.previewData = data;
213 213   document.fire('xwiki:class:previewField', {field: this});
214 214   document.fire('xwiki:dom:updated', {elements: [preview]});
215 - }.bind(this),
216 - // A 403 means the CSRF token expired (e.g. the server was restarted while the user was editing). Offer to
217 - // refresh the token and retry the preview, instead of leaving the loading spinner spinning forever.
218 - on403: function (response) {
219 - preview.removeClassName('loading');
220 - require(['xwiki-appwithinminutes-csrf'], function (csrf) {
221 - csrf.recoverFromExpiredToken(response, function (token) {
222 - data.set('form_token', token);
223 - self._preview(data);
224 - });
225 - });
226 - }.bind(this),
227 - onFailure: function () {
228 - preview.removeClassName('loading');
229 229   }.bind(this)
230 230   });
231 231   }
... ... @@ -351,7 +351,7 @@
351 351   prettyNameInput.ariaLabel = '$escapetool.javascript($services.localization.render('platform.appwithinminutes.classEditorFieldPrettyNameToolTip'))';
352 352   // Replace the preview label with the pretty name input.
353 353   var previewLabel = field.getViewer().down('label');
354 - // We unset the for attribute, so that this label will not be set implicitly OR explicitly anymore.
306 + // We unset the for attribute, so that this label will not be set implicitely OR explicitely anymore.
355 355   previewLabel.writeAttribute('for', '');
356 356   previewLabel.removeChild(previewLabel.lastChild);
357 357   previewLabel.insert(prettyNameInput);
... ... @@ -580,14 +580,21 @@
580 580   // Remove the preview button.
581 581   form.down("input[name='action_preview']").remove();
582 582  
583 - // Let actionButtons.js submit a custom value for the submit buttons to avoid triggering the standard save action
584 - // on the server side.
585 - form.down("input[name='action_save']").dataset.submitValue = 'xaction_save';
586 - form.down("input[name='action_saveandcontinue']").dataset.submitValue = 'xaction_saveandcontinue';
535 + // Rename the save buttons.
536 + form.down("input[name='action_save']").name = 'xaction_save';
537 + form.down("input[name='action_saveandcontinue']").name = 'xaction_saveandcontinue';
587 587  
588 - // actionButtons.js detects this as a custom form due to the submit URL that is not the standard one.
589 - // Force the standard behavior by setting the customForm attribute to false.
590 - form.dataset.customForm = 'false';
539 + // Make sure the 'Save & Continue' button is submitted when clicked.
540 + document.observe('xwiki:class:save', function(event) {
541 + event = event.memo.originalEvent;
542 + if (event.memo['continue']) {
543 + var submitButton = event.memo.originalEvent.element();
544 + submitButton.insert({after: new Element('input', {type: 'hidden', 'name': submitButton.name})});
545 + (function () {
546 + submitButton.next().remove();
547 + }).defer();
548 + }
549 + }.bindAsEventListener(window));
591 591  
592 592   // Be prepared to enhance some of the field properties.
593 593   new XWiki.FormFieldNameManager();