+
+ ${result.detailedMessageHTML}
+
+
+ `;
+
+ panelGroup.appendChild(panel);
+ });
+
+ this.analysisResultsContainer.appendChild(panelGroup);
+ }
+ }
+
+ return {
+ /**
+ * Load and display the required rights dialog
+ * @return {Promise} A promise that resolves when the dialog is shown
+ */
+ show: async function () {
+ const response = await fetch(restURL);
+ const data = await response.json();
+ // Create a bootstrap dialog to display the results
+ const currentRights = data.currentRights;
+ const availableRights = data.availableRights;
+ const dialog = new RequiredRightsDialog(currentRights);
+
+ let indexOfCurrentRight = 0;
+ const unsupportedRights = [];
+ for (const right of currentRights.rights) {
+ const rightIndex = availableRights.findIndex(r => r.right === right.right && r.scope === right.scope);
+ if (rightIndex !== -1) {
+ // Keep the one with the highest index, which should be the most powerful right.
+ if (rightIndex > indexOfCurrentRight) {
+ indexOfCurrentRight = rightIndex;
+ }
+ } else {
+ unsupportedRights.push(right);
+ }
+ }
+
+ const currentRight = availableRights[indexOfCurrentRight];
+
+ if (unsupportedRights.length > 0) {
+ const warningBox = document.createElement('div');
+ warningBox.className = 'box warningmessage';
+ const warningContent = document.createElement('div');
+ warningBox.appendChild(warningContent);
+ const warningParagraph = document.createElement('p');
+ warningContent.appendChild(warningParagraph);
+ warningParagraph.textContent = l10n['modal.unsupportedRights'];
+ const unsupportedRightsList = document.createElement('ul');
+ warningContent.appendChild(unsupportedRightsList);
+ unsupportedRights.forEach(right => {
+ const listItem = document.createElement('li');
+ listItem.textContent = l10n.get('modal.unsupportedRightItem', right.right, right.scope);
+ unsupportedRightsList.appendChild(listItem);
+ });
+ dialog.enforceSelectionElement.appendChild(warningBox);
+ }
+
+ // Create the "Don't enforce" option.
+ dialog.createEnforcementOption(
+ l10n['modal.noEnforceOption'],
+ '0',
+ !data.currentRights.enforce,
+ !availableRights[0].hasRight,
+ [
+ l10n['modal.noEnforceOption.hint1'],
+ l10n['modal.noEnforceOption.hint2']
+ ]
+ );
+
+ // Create the "Enforce" option.
+ dialog.createEnforcementOption(
+ l10n['modal.enforceOption'],
+ '1',
+ data.currentRights.enforce,
+ !availableRights[0].hasRight,
+ [
+ l10n['modal.enforceOption.hint1'],
+ l10n['modal.enforceOption.hint2']
+ ]
+ );
+
+ // Display a nice visualization that shows the rights "Edit", "Script", "Wiki Admin" and "Programming" with the current right highlighted if it isn't null.
+ availableRights.forEach(right => {
+ const checked = currentRight.right === right.right && currentRight.scope === right.scope;
+ let status = '';
+ if (right.right === '' && right.definitelyRequiredRight) {
+ // Check if there is any right that is maybe required.
+ if (availableRights.some(r => r.maybeRequiredRight)) {
+ status = 'maybeEnough';
+ } else {
+ status = 'enough';
+ }
+ } else if (right.definitelyRequiredRight) {
+ status = 'required';
+ } else if (right.maybeRequiredRight) {
+ status = 'maybeRequired';
+ }
+ dialog.addRight(right.displayName, right.right, right.scope, checked, !right.hasRight, status);
+ });
+
+ // Display the analysis results.
+ // First, resolve the entity reference.
+ const analysisResults = data.analysisResults.map(result => {
+ // The client-side resolver isn't fully compatible with the entity references generated in Java.
+ result.entityReference =
+ XWiki.Model.resolve(result.entityReference.replace(/^(object|class)_property/, '$1'));
+ return result;
+ });
+
+ function groupBy(result, propertyExtractor)
+ {
+ const grouped = {};
+
+ for (let i = 0; i < result.length; i++) {
+ const item = result[i];
+ const value = propertyExtractor(item);
+
+ if (Array.isArray(value)) {
+ let current = grouped;
+
+ for (let j = 0; j < value.length; j++) {
+ const key = value[j];
+ const isLastKey = j === value.length - 1;
+
+ if (!current[key]) {
+ current[key] = isLastKey ? [] : {};
+ }
+
+ current = current[key];
+ }
+
+ current.push(item);
+ } else {
+ if (!grouped[value]) {
+ grouped[value] = [];
+ }
+
+ grouped[value].push(item);
+ }
+ }
+
+ return grouped;
+ }
+
+ // Display the results where the entity type is DOCUMENT
+ const documentResults = analysisResults.filter(
+ result => result.entityReference.type === XWiki.EntityType.DOCUMENT);
+
+ // Group the results by locale.
+ const documentResultsByLocale = groupBy(documentResults, result => result.locale ?? '');
+ // Display the results in the dialog. Start with the empty string locale, if any.
+ if (documentResultsByLocale['']) {
+ dialog.addResultsHeading(3, l10n['modal.contentAndTitle']);
+ dialog.addResults(documentResultsByLocale['']);
+ delete documentResultsByLocale[''];
+ }
+ // Display the results for each locale.
+ for (const locale in documentResultsByLocale) {
+ dialog.addResultsHeading(3, l10n.get('modal.localizedContentAndTitle', locale));
+ dialog.addResults(documentResultsByLocale[locale]);
+ }
+ // Display results of type CLASS_PROPERTY, if any.
+ const classPropertyResults = analysisResults.filter(
+ result => result.entityReference.type === XWiki.EntityType.CLASS_PROPERTY);
+ if (classPropertyResults.length > 0) {
+ dialog.addResultsHeading(3, l10n['classProperties']);
+
+ // Group the results by property name
+ const classPropertyResultsByProperty = groupBy(classPropertyResults,
+ result => result.entityReference.name);
+
+ for (const propertyName in classPropertyResultsByProperty) {
+ dialog.addResultsHeading(4, l10n.get('modal.property', propertyName));
+ dialog.addResults(classPropertyResultsByProperty[propertyName]);
+ }
+ }
+ // Display objects and their properties. Group the results by XClass name and object index.
+ // Consider both results of type OBJECT and OBJECT_PROPERTY.
+ const objectResults = analysisResults.filter(
+ result => result.entityReference.type === XWiki.EntityType.OBJECT
+ || result.entityReference.type === XWiki.EntityType.OBJECT_PROPERTY);
+ if (objectResults.length > 0) {
+ // Group the results by XClass name and object index
+ const objectResultsByXClassAndObject = groupBy(objectResults, result => {
+ let xClass;
+ if (result.entityReference.type === XWiki.EntityType.OBJECT) {
+ xClass = result.entityReference.name;
+ } else {
+ xClass = result.entityReference.parent.name;
+ }
+ // xClass is of them form 'ClassName[objectIndex]'. Extract the class name and object index as int.
+ const match = xClass.match(/^(.*)\[(\d+)]$/);
+ return [match[1], parseInt(match[2])];
+ });
+
+ for (const xClassName in objectResultsByXClassAndObject) {
+ for (const objectIndex in objectResultsByXClassAndObject[xClassName]) {
+ dialog.addResultsHeading(3, l10n.get('modal.object', xClassName, objectIndex));
+
+ // First display results of type OBJECT
+ const objectResults = objectResultsByXClassAndObject[xClassName][objectIndex].filter(
+ result => result.entityReference.type === XWiki.EntityType.OBJECT);
+ dialog.addResults(objectResults);
+
+ // Then display results of type OBJECT_PROPERTY, grouped by property name.
+ const objectPropertyResults = objectResultsByXClassAndObject[xClassName][objectIndex].filter(
+ result => result.entityReference.type === XWiki.EntityType.OBJECT_PROPERTY);
+
+ const objectPropertyResultsByProperty = groupBy(objectPropertyResults,
+ result => result.entityReference.name);
+
+ for (const propertyName in objectPropertyResultsByProperty) {
+ dialog.addResultsHeading(4, l10n.get('modal.property', propertyName));
+ dialog.addResults(objectPropertyResultsByProperty[propertyName]);
+ }
+ }
+ }
+ }
+
+ // Hide the toggle for the details if there are no details.
+ if (!analysisResults.length) {
+ dialog.advancedToggleContainer.hidden = true;
+ }
+
+ document.body.appendChild(dialog.dialogElement);
+
+ // Remove the dialog from the DOM when it has been closed.
+ $(dialog.dialogElement).on('hidden.bs.modal', () => {
+ dialog.dialogElement.remove();
+ });
+
+ // Enable the tooltips
+ $(dialog.dialogElement).find('[data-toggle="tooltip"]').tooltip({'trigger': 'hover focus click'});
+
+ // Display the dialog
+ $(dialog.dialogElement).modal('show');
+ }
+ };
+});
+
+require(['jquery', 'xwiki-requiredrights-dialog'], function ($, dialog) {
+ const selector = 'button[data-xwiki-requiredrights-dialog="show"]';
+
+ function init(root)
+ {
+ $(root).find(selector).prop('disabled', false);
+ }
+
+ $(function () {
+ $(document).on('click', 'button[data-xwiki-requiredrights-dialog="show"]', function (event) {
+ event.preventDefault();
+ dialog.show();
+ });
+
+ init(document);
+
+ $(document).on('xwiki:dom:updated', (event, data) => {
+ data.elements.forEach(init);
+ });
+ });
+});
+
+require(['jquery', 'xwiki-l10n!xwiki-requiredrights-messages'], function ($, l10n) {
+ $(document).on('xwiki:document:requiredRightsUpdated.viewMode', function (event, data) {
+ const contentWrapper = $('#xwikicontent').not('[contenteditable]');
+ if (contentWrapper.length && XWiki.currentDocument.documentReference.equals(data.documentReference)) {
+ const notification = new XWiki.widgets.Notification(l10n['contentUpdate.inProgress'], 'inprogress');
+ return loadContent().then(output => {
+ // Update the displayed document title and content.
+ $('#document-title h1').html(output.renderedTitle);
+ contentWrapper.html(output.renderedContent);
+ // Let others know that the DOM has been updated, in order to enhance it.
+ $(document).trigger('xwiki:dom:updated', {'elements': contentWrapper.toArray()});
+ notification.replace(new XWiki.widgets.Notification(l10n['contentUpdate.done'], 'done'));
+ }).catch(() => {
+ notification.replace(new XWiki.widgets.Notification(l10n['contentUpdate.failed'], 'error'));
+ });
+ }
+
+ function loadContent()
+ {
+ const data = {
+ // Get only the document content and title (without the header, footer, panels, etc.)
+ xpage: 'get',
+ // The displayed document title can depend on the rights.
+ outputTitle: 'true'
+ };
+ return $.get(XWiki.currentDocument.getURL('view'), new URLSearchParams(data).toString())
+ .then(function (html) {
+ // Extract the rendered title and content.
+ const container = $('