CVE-2023-29206: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: XWiki Commons are technical libraries common to several other top level XWiki projects. There was no check in the author of a JavaScript xobject or StyleSheet xobject added in a XWiki document, so until now it was possible for a user having only Edit Right to create such object and to craft a script allowing to perform some operations when executing by a user with appropriate rights. This has been patched in XWiki 14.9-rc-1 by only executing the script if the author of it has Script rights.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2023-04-15T16:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/pom.xml, xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractDocumentSkinExtensionPlugin.java, xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractSkinExtensionPlugin.java, xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/CssSkinExtensionPluginTest.java, xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/JsSkinExtensionPluginTest.java
Commit date: 2022-10-12 14:00:20 +0200
Commit message: XWIKI-9119: Refactoring of SkinExtensionPlugin

  * Provide some tests

Diff: diff --git a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/pom.xml b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/pom.xml
index 97d83e7187789..5f1225a32b5b7 100644
--- a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/pom.xml
@@ -29,11 +29,11 @@
   </parent>
   <artifactId>xwiki-platform-skin-skinx</artifactId>
   <name>XWiki Platform - Skin - Skin Extensions</name>
   <description>XWiki Platform - Skin - Skin Extensions</description>
   <properties>
-    <xwiki.jacoco.instructionRatio>0.18</xwiki.jacoco.instructionRatio>
+    <xwiki.jacoco.instructionRatio>0.25</xwiki.jacoco.instructionRatio>
   </properties>
   <dependencies>
     <!-- Build tools -->
     <!-- Needed for backward compatibility Aspects -->
     <dependency>
diff --git a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractDocumentSkinExtensionPlugin.java b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractDocumentSkinExtensionPlugin.java
index 44e8405edd85f..5456db6ea2d6a 100644
--- a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractDocumentSkinExtensionPlugin.java
+++ b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractDocumentSkinExtensionPlugin.java
@@ -27,18 +27,20 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.xwiki.bridge.event.DocumentCreatedEvent;
 import org.xwiki.bridge.event.DocumentDeletedEvent;
 import org.xwiki.bridge.event.DocumentUpdatedEvent;
 import org.xwiki.bridge.event.WikiDeletedEvent;
 import org.xwiki.model.EntityType;
 import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.DocumentReferenceResolver;
 import org.xwiki.model.reference.EntityReferenceResolver;
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.observation.EventListener;
 import org.xwiki.observation.ObservationManager;
 import org.xwiki.observation.event.Event;
@@ -83,10 +85,14 @@ public abstract class AbstractDocumentSkinExtensionPlugin extends AbstractSkinEx
     /**
      * Used to match events on "use" property.
      */
     private final List<Event> events = new ArrayList<>(3);
 
+    private AuthorizationManager authorizationManager;
+    private DocumentReferenceResolver<String> stringDocumentReferenceResolver;
+    private EntityReferenceResolver<String> currentEntityReferenceResolver;
+
     /**
      * XWiki plugin constructor.
      * 
      * @param name The name of the plugin, which can be used for retrieving the plugin API from velocity. Unused.
      * @param className The canonical classname of the plugin. Unused.
@@ -172,11 +178,11 @@ public void virtualInit(XWikiContext context)
      * @see AbstractSkinExtensionPlugin#getAlwaysUsedExtensions(XWikiContext)
      */
     @Override
     public Set<String> getAlwaysUsedExtensions(XWikiContext context)
     {
-        EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING);
+        EntityReferenceSerializer<String> serializer = getDefaultEntityReferenceSerializer();
         Set<DocumentReference> references = getAlwaysUsedExtensions();
         Set<String> names = new HashSet<>(references.size());
         for (DocumentReference reference : references) {
             names.add(serializer.serialize(reference));
         }
@@ -212,11 +218,11 @@ public Set<DocumentReference> getAlwaysUsedExtensions()
                     .searchDocumentReferences(query, context)) {
                     try {
                         XWikiDocument doc = context.getWiki().getDocument(extension, context);
                         // Only add the extension as being "always used" if the page holding it has been saved with
                         // programming rights.
-                        if (Utils.getComponent(AuthorizationManager.class).hasAccess(Right.PROGRAM,
+                        if (getAuthorizationManager().hasAccess(Right.PROGRAM,
                             doc.getAuthorReference(), doc.getDocumentReference())) {
                             extensions.add(extension);
                         }
                     } catch (XWikiException e1) {
                         LOGGER.error("Error while adding skin extension [{}] as always used. It will be ignored.",
@@ -234,40 +240,87 @@ public Set<DocumentReference> getAlwaysUsedExtensions()
 
     @Override
     public boolean hasPageExtensions(XWikiContext context)
     {
         XWikiDocument doc = context.getDoc();
-        if (doc != null) {
-            List<BaseObject> objects = doc.getObjects(getExtensionClassName());
-            if (objects != null) {
-                for (BaseObject obj : objects) {
-                    if (obj == null) {
-                        continue;
-                    }
-                    if (obj.getStringValue(USE_FIELDNAME).equals("currentPage")) {
-                        return true;
-                    }
+        boolean result = false;
+        if (doc != null && this.hasCurrentPageExtensionObjects(doc)) {
+            if (getAuthorizationManager().hasAccess(Right.SCRIPT, doc.getAuthorReference(),
+                doc.getDocumentReference())) {
+                result = true;
+            } else {
+                displayScriptRightLog(doc.getDocumentReference());
+            }
+        }
+        return result;
+    }
+
+    private void displayScriptRightLog(Object documentReference)
+    {
+        LOGGER.warn("Extensions present in [{}] ignored because of lack of script right from the author.",
+            documentReference);
+    }
+
+    private boolean hasCurrentPageExtensionObjects(XWikiDocument doc)
+    {
+        List<BaseObject> objects = doc.getObjects(getExtensionClassName());
+        if (objects != null) {
+            for (BaseObject obj : objects) {
+                if (obj == null) {
+                    continue;
+                }
+                if (StringUtils.equals(obj.getStringValue(USE_FIELDNAME), "currentPage")) {
+                    return true;
                 }
             }
         }
         return false;
     }
 
     @Override
-    public void use(String resource, XWikiContext context)
+    public void use(String resource, Map<String, Object> parameters, XWikiContext context)
     {
         String canonicalResource = getCanonicalDocumentName(resource);
 
-        super.use(canonicalResource, context);
+        if (this.canResourceBeUsed(canonicalResource, context)) {
+            super.use(canonicalResource, parameters, context);
+        } else {
+            displayScriptRightLog(canonicalResource);
+        }
     }
 
-    @Override
-    public void use(String resource, Map<String, Object> parameters, XWikiContext context)
+    private DocumentReferenceResolver<String> getDocumentReferenceResolver()
     {
-        String canonicalResource = getCanonicalDocumentName(resource);
+        if (this.stringDocumentReferenceResolver == null) {
+            this.stringDocumentReferenceResolver = Utils.getComponent(DocumentReferenceResolver.TYPE_STRING);
+        }
+        return this.stringDocumentReferenceResolver;
+    }
 
-        super.use(canonicalResource, parameters, context);
+    private AuthorizationManager getAuthorizationManager()
+    {
+        if (this.authorizationManager == null) {
+            this.authorizationManager = Utils.getComponent(AuthorizationManager.class);
+        }
+        return this.authorizationManager;
+    }
+
+    private boolean canResourceBeUsed(String resource, XWikiContext context)
+    {
+        DocumentReferenceResolver<String> documentReferenceResolver = getDocumentReferenceResolver();
+        DocumentReference documentReference = documentReferenceResolver.resolve(resource);
+
+        try {
+            XWikiDocument document = context.getWiki().getDocument(documentReference, context);
+            DocumentReference authorReference = document.getAuthorReference();
+            return getAuthorizationManager().hasAccess(Right.SCRIPT, authorReference, documentReference);
+        } catch (XWikiException e) {
+            LOGGER.error("Error while loading [{}] for checking script right: [{}]", documentReference,
+                ExceptionUtils.getRootCauseMessage(e));
+            LOGGER.debug("Original error stack trace: ", e);
+            return false;
+        }
     }
 
     /**
      * {@inheritDoc}
      * <p>
@@ -330,11 +383,11 @@ private void onDocumentEvent(XWikiDocument document)
     {
         boolean remove = false;
         if (document.getObject(getExtensionClassName()) != null) {
             // new or already existing object
             if (document.getObject(getExtensionClassName(), USE_FIELDNAME, "always", false) != null) {
-                if (Utils.getComponent(AuthorizationManager.class).hasAccess(Right.PROGRAM,
+                if (getAuthorizationManager().hasAccess(Right.PROGRAM,
                     document.getAuthorReference(), document.getDocumentReference())) {
                     getAlwaysUsedExtensions().add(document.getDocumentReference());
 
                     return;
                 } else {
@@ -353,22 +406,28 @@ private void onDocumentEvent(XWikiDocument document)
         if (remove) {
             getAlwaysUsedExtensions().remove(document.getDocumentReference());
         }
     }
 
+    private EntityReferenceResolver<String> getCurrentEntityReferenceResolver()
+    {
+        if (this.currentEntityReferenceResolver == null) {
+            this.currentEntityReferenceResolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "current");
+        }
+        return this.currentEntityReferenceResolver;
+    }
+
     /**
      * Get the canonical serialization of a document name, in the {@code wiki:Space.Document} format.
      *
      * @param documentName the original document name to fix
      * @return fixed document name
      */
     private String getCanonicalDocumentName(String documentName)
     {
-        @SuppressWarnings("unchecked")
-        EntityReferenceResolver<String> resolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "current");
-        @SuppressWarnings("unchecked")
-        EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING);
+        EntityReferenceResolver<String> resolver = getCurrentEntityReferenceResolver();
+        EntityReferenceSerializer<String> serializer = getDefaultEntityReferenceSerializer();
         return serializer.serialize(resolver.resolve(documentName, EntityType.DOCUMENT));
     }
 
     /**
      * @param documentName the Skin Extension's document name
diff --git a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractSkinExtensionPlugin.java b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractSkinExtensionPlugin.java
index 86d8d93c8c5c4..ae3d23fa9637c 100644
--- a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractSkinExtensionPlugin.java
+++ b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractSkinExtensionPlugin.java
@@ -227,18 +227,11 @@ private void useResource(String resource, XWikiContext context)
      * @param context The current request context.
      * @see #use(String, Map, XWikiContext)
      */
     public void use(String resource, XWikiContext context)
     {
-        useResource(resource, context);
-
-        // In case a previous call added some parameters, remove them, since the last call for a resource always
-        // discards previous ones.
-        getParametersMap(context).remove(resource);
-
-        // Register the use of the resource in case the current execution is an asynchronous renderer
-        getSkinExtensionAsync().use(getName(), resource, null);
+        use(resource, null, context);
     }
 
     /**
      * Mark a skin extension document as used in the current result, together with some parameters. How the parameters
      * are used, depends on the type of resource being pulled. For example, JS and CSS extensions use the parameters in
@@ -254,12 +247,18 @@ public void use(String resource, XWikiContext context)
      */
     public void use(String resource, Map<String, Object> parameters, XWikiContext context)
     {
         useResource(resource, context);
 
-        // Associate parameters to the resource
-        getParametersMap(context).put(resource, parameters);
+        // In case a previous call added some parameters, remove them, since the last call for a resource always
+        // discards previous ones.
+        if (parameters == null) {
+            getParametersMap(context).remove(resource);
+        } else {
+            // Associate parameters to the resource
+            getParametersMap(context).put(resource, parameters);
+        }
 
         getSkinExtensionAsync().use(getName(), resource, parameters);
     }
 
     /**
diff --git a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/CssSkinExtensionPluginTest.java b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/CssSkinExtensionPluginTest.java
index 6a55c96a45ffc..4d90e3130624e 100644
--- a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/CssSkinExtensionPluginTest.java
+++ b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/CssSkinExtensionPluginTest.java
@@ -20,52 +20,71 @@
 package com.xpn.xwiki.plugin.skinx;
 
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
 
 import javax.inject.Named;
 import javax.inject.Provider;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.xwiki.model.EntityType;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.DocumentReferenceResolver;
+import org.xwiki.model.reference.EntityReferenceResolver;
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.model.reference.SpaceReference;
 import org.xwiki.model.reference.WikiReference;
 import org.xwiki.observation.ObservationManager;
 import org.xwiki.security.authorization.AuthorizationManager;
 import org.xwiki.security.authorization.ContextualAuthorizationManager;
 import org.xwiki.security.authorization.Right;
+import org.xwiki.skinx.internal.async.SkinExtensionAsync;
+import org.xwiki.test.LogLevel;
+import org.xwiki.test.junit5.LogCaptureExtension;
 import org.xwiki.test.junit5.mockito.MockComponent;
 
 import com.xpn.xwiki.XWiki;
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
 import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
 import com.xpn.xwiki.objects.classes.BaseClass;
 import com.xpn.xwiki.store.XWikiStoreInterface;
 import com.xpn.xwiki.test.MockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
 import com.xpn.xwiki.web.XWikiRequest;
 import com.xpn.xwiki.web.XWikiURLFactory;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 /**
  * Tests for {@link CssSkinExtensionPlugin}.
  *
  * @version $Id$
  * @since 13.10RC1
  */
 @OldcoreTest
-public class CssSkinExtensionPluginTest
+class CssSkinExtensionPluginTest
 {
     @InjectMockitoOldcore
     private MockitoOldcore mockitoOldcore;
 
     @MockComponent
@@ -81,22 +100,35 @@
     @MockComponent
     private Provider<DocumentReference> documentReferenceProvider;
 
     @MockComponent
     @Named("currentmixed")
-    private DocumentReferenceResolver<String> documentReferenceResolver;
+    private DocumentReferenceResolver<String> currentMixedDocumentReferenceResolver;
 
     @MockComponent
     @Named("current")
     private DocumentReferenceResolver<String> currentDocumentReferenceResolver;
 
+    @MockComponent
+    private DocumentReferenceResolver<String> documentReferenceResolver;
+
+    @MockComponent
+    @Named("current")
+    private EntityReferenceResolver<String> currentEntityReferenceResolver;
+
     @MockComponent
     private AuthorizationManager authorizationManager;
 
     @MockComponent
     private ContextualAuthorizationManager contextualAuthorizationManager;
 
+    @MockComponent
+    private SkinExtensionAsync skinExtensionAsync;
+
+    @RegisterExtension
+    LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
+
     private XWikiContext context;
     private BaseClass pluginClass;
     private CssSkinExtensionPlugin skinExtensionPlugin;
     private XWikiURLFactory urlFactory;
 
@@ -110,11 +142,11 @@ void setup() throws XWikiException
         this.urlFactory = mock(XWikiURLFactory.class);
         context.setURLFactory(urlFactory);
         XWikiRequest xWikiRequest = mock(XWikiRequest.class);
         context.setRequest(xWikiRequest);
         XWiki wiki = mockitoOldcore.getSpyXWiki();
-        when(this.documentReferenceResolver.resolve(CssSkinExtensionPlugin.SSX_CLASS_NAME)).thenReturn(
+        when(this.currentMixedDocumentReferenceResolver.resolve(CssSkinExtensionPlugin.SSX_CLASS_NAME)).thenReturn(
             new DocumentReference(CssSkinExtensionPlugin.SSX_CLASS_REFERENCE, new WikiReference("xwiki")));
         when(wiki.getDocument(CssSkinExtensionPlugin.SSX_CLASS_NAME, context)).thenReturn(classDoc);
         when(classDoc.getXClass()).thenReturn(this.pluginClass);
         this.skinExtensionPlugin = new CssSkinExtensionPlugin("plugin1", "unused", context);
         skinExtensionPlugin.init(context);
@@ -204,6 +236,152 @@ void endParsingAlwaysUsedExtensions() throws XWikiException, MalformedURLExcepti
             expectedStyle,
             CssSkinExtensionPlugin.class.getCanonicalName());
         String obtainedContent = this.skinExtensionPlugin.endParsing(content, this.context);
         assertEquals(expectedContent, obtainedContent);
     }
+
+    @Test
+    void use() throws XWikiException
+    {
+        String resource = "MySpace.MySSXPage";
+        Map<String, Object> parameters = Collections.singletonMap("fooParam", "barValue");
+
+        DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "MySSXPage");
+        when(this.currentEntityReferenceResolver.resolve(resource, EntityType.DOCUMENT)).thenReturn(documentReference);
+        when(this.entityReferenceSerializer.serialize(documentReference)).thenReturn(resource);
+
+        when(this.documentReferenceResolver.resolve(resource)).thenReturn(documentReference);
+        XWikiDocument document = mock(XWikiDocument.class);
+        when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference, context)).thenReturn(document);
+
+        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo");
+        when(document.getAuthorReference()).thenReturn(userReference);
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true);
+
+        this.skinExtensionPlugin.use(resource, parameters, context);
+        String className = CssSkinExtensionPlugin.class.getCanonicalName();
+        Set<String> resources = (Set<String>) context.get(className);
+
+        assertEquals(Collections.singleton(resource), resources);
+
+        Map<String, Map<String, Object>> parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        Map<String, Map<String, Object>> expectedParameters = new HashMap<>();
+        expectedParameters.put(resource, parameters);
+
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference);
+        verify(this.skinExtensionAsync).use("ssx", resource, parameters);
+
+        String resource2 = "MySpace.MyOtherSSXPage";
+        Map<String, Object> parameters2 = null;
+        DocumentReference documentReference2 = new DocumentReference("xwiki", "MySpace", "MyOtherSSXPage");
+
+        when(this.currentEntityReferenceResolver.resolve(resource2, EntityType.DOCUMENT))
+            .thenReturn(documentReference2);
+        when(this.entityReferenceSerializer.serialize(documentReference2)).thenReturn(resource2);
+
+        when(this.documentReferenceResolver.resolve(resource2)).thenReturn(documentReference2);
+        XWikiDocument document2 = mock(XWikiDocument.class);
+        when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference2, context)).thenReturn(document2);
+
+        DocumentReference userReference2 = new DocumentReference("xwiki", "XWiki", "Bar");
+        when(document2.getAuthorReference()).thenReturn(userReference2);
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(false);
+
+        this.skinExtensionPlugin.use(resource2, parameters2, context);
+        resources = (Set<String>) context.get(className);
+        assertEquals(Collections.singleton(resource), resources);
+        parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference2, documentReference2);
+        verify(this.skinExtensionAsync, never()).use("ssx", resource2, parameters2);
+
+        assertEquals(1, this.logCapture.size());
+        assertEquals("Extensions present in [MySpace.MyOtherSSXPage] ignored because of lack of script right "
+            + "from the author.", this.logCapture.getMessage(0));
+
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(true);
+        this.skinExtensionPlugin.use(resource2, parameters2, context);
+
+        Set<String> expectedSet = new HashSet<>();
+        expectedSet.add(resource);
+        expectedSet.add(resource2);
+
+        resources = (Set<String>) context.get(className);
+        assertEquals(expectedSet, resources);
+
+        parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.authorizationManager, times(2)).hasAccess(Right.SCRIPT, userReference2, documentReference2);
+        verify(this.skinExtensionAsync).use("ssx", resource2, null);
+
+        parameters2 = Collections.singletonMap("buzValue", 42);
+        this.skinExtensionPlugin.use(resource2, parameters2, context);
+
+        resources = (Set<String>) context.get(className);
+        assertEquals(expectedSet, resources);
+        parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        expectedParameters.put(resource2, parameters2);
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.skinExtensionAsync).use("ssx", resource2, parameters2);
+
+        this.skinExtensionPlugin.use(resource, null, context);
+        expectedParameters.remove(resource);
+        resources = (Set<String>) context.get(className);
+        assertEquals(expectedSet, resources);
+        parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        expectedParameters.put(resource2, parameters2);
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.skinExtensionAsync).use("ssx", resource, null);
+    }
+
+    @Test
+    void hasPageExtensions()
+    {
+        this.context.setDoc(null);
+        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
+
+        XWikiDocument currentDoc = mock(XWikiDocument.class, "currentDoc");
+        this.context.setDoc(currentDoc);
+
+        String className = CssSkinExtensionPlugin.SSX_CLASS_NAME;
+        when(currentDoc.getObjects(className)).thenReturn(null);
+        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
+        verify(currentDoc).getObjects(className);
+
+        BaseObject baseObject = mock(BaseObject.class, "specificObj");
+        when(baseObject.getStringValue("use")).thenReturn("wiki");
+        Vector<BaseObject> objectList = new Vector<>();
+        objectList.add(mock(BaseObject.class));
+        objectList.add(null);
+        objectList.add(mock(BaseObject.class));
+        objectList.add(baseObject);
+        objectList.add(null);
+        objectList.add(mock(BaseObject.class));
+
+        when(currentDoc.getObjects(className)).thenReturn(objectList);
+        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
+        verifyNoInteractions(this.authorizationManager);
+
+        when(baseObject.getStringValue("use")).thenReturn("currentPage");
+        DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "SomePage");
+        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo");
+        when(currentDoc.getDocumentReference()).thenReturn(documentReference);
+        when(currentDoc.getAuthorReference()).thenReturn(userReference);
+
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(false);
+        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
+        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference);
+
+        assertEquals(1, this.logCapture.size());
+        assertEquals("Extensions present in [xwiki:MySpace.SomePage] ignored because of lack of script right "
+            + "from the author.", this.logCapture.getMessage(0));
+
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true);
+        assertTrue(this.skinExtensionPlugin.hasPageExtensions(context));
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/JsSkinExtensionPluginTest.java b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/JsSkinExtensionPluginTest.java
new file mode 100644
index 0000000000000..1b0fd2df8dc29
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/JsSkinExtensionPluginTest.java
@@ -0,0 +1,280 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xpn.xwiki.plugin.skinx;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
+
+import javax.inject.Named;
+import javax.inject.Provider;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.xwiki.model.EntityType;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.DocumentReferenceResolver;
+import org.xwiki.model.reference.EntityReferenceResolver;
+import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.model.reference.WikiReference;
+import org.xwiki.observation.ObservationManager;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.skinx.internal.async.SkinExtensionAsync;
+import org.xwiki.test.LogLevel;
+import org.xwiki.test.junit5.LogCaptureExtension;
+import org.xwiki.test.junit5.mockito.MockComponent;
+
+import com.xpn.xwiki.XWiki;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xpn.xwiki.objects.classes.BaseClass;
+import com.xpn.xwiki.test.MockitoOldcore;
+import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
+import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
+import com.xpn.xwiki.web.XWikiRequest;
+import com.xpn.xwiki.web.XWikiURLFactory;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link JsSkinExtensionPlugin}.
+ *
+ * @version $Id$
+ * @since 14.9RC1
+ */
+@OldcoreTest
+class JsSkinExtensionPluginTest
+{
+    @InjectMockitoOldcore
+    private MockitoOldcore mockitoOldcore;
+
+    @MockComponent
+    private Provider<DocumentReference> documentReferenceProvider;
+
+    @MockComponent
+    private ObservationManager observationManager;
+
+    @MockComponent
+    private EntityReferenceSerializer<String> entityReferenceSerializer;
+
+    @MockComponent
+    @Named("currentmixed")
+    private DocumentReferenceResolver<String> currentMixedDocumentReferenceResolver;
+
+    @MockComponent
+    private DocumentReferenceResolver<String> documentReferenceResolver;
+
+    @MockComponent
+    @Named("current")
+    private EntityReferenceResolver<String> currentEntityReferenceResolver;
+
+    @MockComponent
+    private AuthorizationManager authorizationManager;
+
+    @MockComponent
+    private SkinExtensionAsync skinExtensionAsync;
+
+    @RegisterExtension
+    LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
+
+    private XWikiContext context;
+    private BaseClass pluginClass;
+    private JsSkinExtensionPlugin skinExtensionPlugin;
+    private XWikiURLFactory urlFactory;
+
+    @BeforeEach
+    void setup() throws XWikiException
+    {
+        XWikiDocument classDoc = mock(XWikiDocument.class);
+        this.pluginClass = mock(BaseClass.class);
+
+        this.context = mockitoOldcore.getXWikiContext();
+        this.urlFactory = mock(XWikiURLFactory.class);
+        context.setURLFactory(urlFactory);
+        XWikiRequest xWikiRequest = mock(XWikiRequest.class);
+        context.setRequest(xWikiRequest);
+        XWiki wiki = mockitoOldcore.getSpyXWiki();
+        when(this.currentMixedDocumentReferenceResolver.resolve(JsSkinExtensionPlugin.JSX_CLASS_NAME)).thenReturn(
+            new DocumentReference(JsSkinExtensionPlugin.JSX_CLASS_REFERENCE, new WikiReference("xwiki")));
+        when(wiki.getDocument(JsSkinExtensionPlugin.JSX_CLASS_NAME, context)).thenReturn(classDoc);
+        when(classDoc.getXClass()).thenReturn(this.pluginClass);
+        this.skinExtensionPlugin = new JsSkinExtensionPlugin("plugin1", "unused", context);
+        skinExtensionPlugin.init(context);
+    }
+
+    @Test
+    void use() throws XWikiException
+    {
+        String resource = "MySpace.MyJSXPage";
+        Map<String, Object> parameters = Collections.singletonMap("fooParam", "barValue");
+
+        DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "MyJSXPage");
+        when(this.currentEntityReferenceResolver.resolve(resource, EntityType.DOCUMENT)).thenReturn(documentReference);
+        when(this.entityReferenceSerializer.serialize(documentReference)).thenReturn(resource);
+
+        when(this.documentReferenceResolver.resolve(resource)).thenReturn(documentReference);
+        XWikiDocument document = mock(XWikiDocument.class);
+        when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference, context)).thenReturn(document);
+
+        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo");
+        when(document.getAuthorReference()).thenReturn(userReference);
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true);
+
+        this.skinExtensionPlugin.use(resource, parameters, context);
+        String className = JsSkinExtensionPlugin.class.getCanonicalName();
+        Set<String> resources = (Set<String>) context.get(className);
+
+        assertEquals(Collections.singleton(resource), resources);
+
+        Map<String, Map<String, Object>> parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        Map<String, Map<String, Object>> expectedParameters = new HashMap<>();
+        expectedParameters.put(resource, parameters);
+
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference);
+        verify(this.skinExtensionAsync).use("jsx", resource, parameters);
+
+        String resource2 = "MySpace.MyOtherJSXPage";
+        Map<String, Object> parameters2 = null;
+        DocumentReference documentReference2 = new DocumentReference("xwiki", "MySpace", "MyOtherJSXPage");
+
+        when(this.currentEntityReferenceResolver.resolve(resource2, EntityType.DOCUMENT))
+            .thenReturn(documentReference2);
+        when(this.entityReferenceSerializer.serialize(documentReference2)).thenReturn(resource2);
+
+        when(this.documentReferenceResolver.resolve(resource2)).thenReturn(documentReference2);
+        XWikiDocument document2 = mock(XWikiDocument.class);
+        when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference2, context)).thenReturn(document2);
+
+        DocumentReference userReference2 = new DocumentReference("xwiki", "XWiki", "Bar");
+        when(document2.getAuthorReference()).thenReturn(userReference2);
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(false);
+
+        this.skinExtensionPlugin.use(resource2, parameters2, context);
+        resources = (Set<String>) context.get(className);
+        assertEquals(Collections.singleton(resource), resources);
+        parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference2, documentReference2);
+        verify(this.skinExtensionAsync, never()).use("jsx", resource2, parameters2);
+
+        assertEquals(1, this.logCapture.size());
+        assertEquals("Extensions present in [MySpace.MyOtherJSXPage] ignored because of lack of script right "
+            + "from the author.", this.logCapture.getMessage(0));
+
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(true);
+        this.skinExtensionPlugin.use(resource2, parameters2, context);
+
+        Set<String> expectedSet = new HashSet<>();
+        expectedSet.add(resource);
+        expectedSet.add(resource2);
+
+        resources = (Set<String>) context.get(className);
+        assertEquals(expectedSet, resources);
+
+        parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.authorizationManager, times(2)).hasAccess(Right.SCRIPT, userReference2, documentReference2);
+        verify(this.skinExtensionAsync).use("jsx", resource2, null);
+
+        parameters2 = Collections.singletonMap("buzValue", 42);
+        this.skinExtensionPlugin.use(resource2, parameters2, context);
+
+        resources = (Set<String>) context.get(className);
+        assertEquals(expectedSet, resources);
+        parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        expectedParameters.put(resource2, parameters2);
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.skinExtensionAsync).use("jsx", resource2, parameters2);
+
+        this.skinExtensionPlugin.use(resource, null, context);
+        expectedParameters.remove(resource);
+        resources = (Set<String>) context.get(className);
+        assertEquals(expectedSet, resources);
+        parametersMap =
+            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
+        expectedParameters.put(resource2, parameters2);
+        assertEquals(expectedParameters, parametersMap);
+        verify(this.skinExtensionAsync).use("jsx", resource, null);
+    }
+
+    @Test
+    void hasPageExtensions()
+    {
+        this.context.setDoc(null);
+        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
+
+        XWikiDocument currentDoc = mock(XWikiDocument.class, "currentDoc");
+        this.context.setDoc(currentDoc);
+
+        String className = JsSkinExtensionPlugin.JSX_CLASS_NAME;
+        when(currentDoc.getObjects(className)).thenReturn(null);
+        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
+        verify(currentDoc).getObjects(className);
+
+        BaseObject baseObject = mock(BaseObject.class, "specificObj");
+        when(baseObject.getStringValue("use")).thenReturn("wiki");
+        Vector<BaseObject> objectList = new Vector<>();
+        objectList.add(mock(BaseObject.class));
+        objectList.add(null);
+        objectList.add(mock(BaseObject.class));
+        objectList.add(baseObject);
+        objectList.add(null);
+        objectList.add(mock(BaseObject.class));
+
+        when(currentDoc.getObjects(className)).thenReturn(objectList);
+        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
+        verifyNoInteractions(this.authorizationManager);
+
+        when(baseObject.getStringValue("use")).thenReturn("currentPage");
+        DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "SomePage");
+        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo");
+        when(currentDoc.getDocumentReference()).thenReturn(documentReference);
+        when(currentDoc.getAuthorReference()).thenReturn(userReference);
+
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(false);
+        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
+        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference);
+
+        assertEquals(1, this.logCapture.size());
+        assertEquals("Extensions present in [xwiki:MySpace.SomePage] ignored because of lack of script right "
+            + "from the author.", this.logCapture.getMessage(0));
+
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true);
+        assertTrue(this.skinExtensionPlugin.hasPageExtensions(context));
+    }
+}
\ No newline at end of file

Vulnerable code:     <xwiki.jacoco.instructionRatio>0.18</xwiki.jacoco.instructionRatio>
        EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING);
                        if (Utils.getComponent(AuthorizationManager.class).hasAccess(Right.PROGRAM,
        if (doc != null) {
            List<BaseObject> objects = doc.getObjects(getExtensionClassName());
            if (objects != null) {
                for (BaseObject obj : objects) {
                    if (obj == null) {
                        continue;
                    }
                    if (obj.getStringValue(USE_FIELDNAME).equals("currentPage")) {
                        return true;
                    }
    public void use(String resource, XWikiContext context)
        super.use(canonicalResource, context);
    @Override
    public void use(String resource, Map<String, Object> parameters, XWikiContext context)
        String canonicalResource = getCanonicalDocumentName(resource);
        super.use(canonicalResource, parameters, context);
                if (Utils.getComponent(AuthorizationManager.class).hasAccess(Right.PROGRAM,
        @SuppressWarnings("unchecked")
        EntityReferenceResolver<String> resolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "current");
        @SuppressWarnings("unchecked")
        EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING);
        useResource(resource, context);

        // In case a previous call added some parameters, remove them, since the last call for a resource always
        // discards previous ones.
        getParametersMap(context).remove(resource);

        // Register the use of the resource in case the current execution is an asynchronous renderer
        getSkinExtensionAsync().use(getName(), resource, null);
        // Associate parameters to the resource
        getParametersMap(context).put(resource, parameters);
public class CssSkinExtensionPluginTest
    private DocumentReferenceResolver<String> documentReferenceResolver;
        when(this.documentReferenceResolver.resolve(CssSkinExtensionPlugin.SSX_CLASS_NAME)).thenReturn(
Fixed code:     <xwiki.jacoco.instructionRatio>0.25</xwiki.jacoco.instructionRatio>
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.xwiki.model.reference.DocumentReferenceResolver;
    private AuthorizationManager authorizationManager;
    private DocumentReferenceResolver<String> stringDocumentReferenceResolver;
    private EntityReferenceResolver<String> currentEntityReferenceResolver;

        EntityReferenceSerializer<String> serializer = getDefaultEntityReferenceSerializer();
                        if (getAuthorizationManager().hasAccess(Right.PROGRAM,
        boolean result = false;
        if (doc != null && this.hasCurrentPageExtensionObjects(doc)) {
            if (getAuthorizationManager().hasAccess(Right.SCRIPT, doc.getAuthorReference(),
                doc.getDocumentReference())) {
                result = true;
            } else {
                displayScriptRightLog(doc.getDocumentReference());
            }
        }
        return result;
    }

    private void displayScriptRightLog(Object documentReference)
    {
        LOGGER.warn("Extensions present in [{}] ignored because of lack of script right from the author.",
            documentReference);
    }

    private boolean hasCurrentPageExtensionObjects(XWikiDocument doc)
    {
        List<BaseObject> objects = doc.getObjects(getExtensionClassName());
        if (objects != null) {
            for (BaseObject obj : objects) {
                if (obj == null) {
                    continue;
                }
                if (StringUtils.equals(obj.getStringValue(USE_FIELDNAME), "currentPage")) {
                    return true;
    public void use(String resource, Map<String, Object> parameters, XWikiContext context)
        if (this.canResourceBeUsed(canonicalResource, context)) {
            super.use(canonicalResource, parameters, context);
        } else {
            displayScriptRightLog(canonicalResource);
        }
    private DocumentReferenceResolver<String> getDocumentReferenceResolver()
        if (this.stringDocumentReferenceResolver == null) {
            this.stringDocumentReferenceResolver = Utils.getComponent(DocumentReferenceResolver.TYPE_STRING);
        }
        return this.stringDocumentReferenceResolver;
    }
    private AuthorizationManager getAuthorizationManager()
    {
        if (this.authorizationManager == null) {
            this.authorizationManager = Utils.getComponent(AuthorizationManager.class);
        }
        return this.authorizationManager;
    }

    private boolean canResourceBeUsed(String resource, XWikiContext context)
    {
        DocumentReferenceResolver<String> documentReferenceResolver = getDocumentReferenceResolver();
        DocumentReference documentReference = documentReferenceResolver.resolve(resource);

        try {
            XWikiDocument document = context.getWiki().getDocument(documentReference, context);
            DocumentReference authorReference = document.getAuthorReference();
            return getAuthorizationManager().hasAccess(Right.SCRIPT, authorReference, documentReference);
        } catch (XWikiException e) {
            LOGGER.error("Error while loading [{}] for checking script right: [{}]", documentReference,
                ExceptionUtils.getRootCauseMessage(e));
            LOGGER.debug("Original error stack trace: ", e);
            return false;
        }
                if (getAuthorizationManager().hasAccess(Right.PROGRAM,
    private EntityReferenceResolver<String> getCurrentEntityReferenceResolver()
    {
        if (this.currentEntityReferenceResolver == null) {
            this.currentEntityReferenceResolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "current");
        }
        return this.currentEntityReferenceResolver;
    }

        EntityReferenceResolver<String> resolver = getCurrentEntityReferenceResolver();
        EntityReferenceSerializer<String> serializer = getDefaultEntityReferenceSerializer();
        use(resource, null, context);
        // In case a previous call added some parameters, remove them, since the last call for a resource always
        // discards previous ones.
        if (parameters == null) {
            getParametersMap(context).remove(resource);
        } else {
            // Associate parameters to the resource
            getParametersMap(context).put(resource, parameters);
        }
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.EntityReferenceResolver;
import org.xwiki.skinx.internal.async.SkinExtensionAsync;
import org.xwiki.test.LogLevel;
import org.xwiki.test.junit5.LogCaptureExtension;
import com.xpn.xwiki.objects.BaseObject;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
class CssSkinExtensionPluginTest
    private DocumentReferenceResolver<String> currentMixedDocumentReferenceResolver;
    @MockComponent
    private DocumentReferenceResolver<String> documentReferenceResolver;

    @MockComponent
    @Named("current")
    private EntityReferenceResolver<String> currentEntityReferenceResolver;

    @MockComponent
    private SkinExtensionAsync skinExtensionAsync;

    @RegisterExtension
    LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);

        when(this.currentMixedDocumentReferenceResolver.resolve(CssSkinExtensionPlugin.SSX_CLASS_NAME)).thenReturn(

    @Test
    void use() throws XWikiException
    {
        String resource = "MySpace.MySSXPage";
        Map<String, Object> parameters = Collections.singletonMap("fooParam", "barValue");

        DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "MySSXPage");
        when(this.currentEntityReferenceResolver.resolve(resource, EntityType.DOCUMENT)).thenReturn(documentReference);
        when(this.entityReferenceSerializer.serialize(documentReference)).thenReturn(resource);

        when(this.documentReferenceResolver.resolve(resource)).thenReturn(documentReference);
        XWikiDocument document = mock(XWikiDocument.class);
        when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference, context)).thenReturn(document);

        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo");
        when(document.getAuthorReference()).thenReturn(userReference);
        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true);

        this.skinExtensionPlugin.use(resource, parameters, context);
        String className = CssSkinExtensionPlugin.class.getCanonicalName();
        Set<String> resources = (Set<String>) context.get(className);

        assertEquals(Collections.singleton(resource), resources);

        Map<String, Map<String, Object>> parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        Map<String, Map<String, Object>> expectedParameters = new HashMap<>();
        expectedParameters.put(resource, parameters);

        assertEquals(expectedParameters, parametersMap);
        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference);
        verify(this.skinExtensionAsync).use("ssx", resource, parameters);

        String resource2 = "MySpace.MyOtherSSXPage";
        Map<String, Object> parameters2 = null;
        DocumentReference documentReference2 = new DocumentReference("xwiki", "MySpace", "MyOtherSSXPage");

        when(this.currentEntityReferenceResolver.resolve(resource2, EntityType.DOCUMENT))
            .thenReturn(documentReference2);
        when(this.entityReferenceSerializer.serialize(documentReference2)).thenReturn(resource2);

        when(this.documentReferenceResolver.resolve(resource2)).thenReturn(documentReference2);
        XWikiDocument document2 = mock(XWikiDocument.class);
        when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference2, context)).thenReturn(document2);

        DocumentReference userReference2 = new DocumentReference("xwiki", "XWiki", "Bar");
        when(document2.getAuthorReference()).thenReturn(userReference2);
        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(false);

        this.skinExtensionPlugin.use(resource2, parameters2, context);
        resources = (Set<String>) context.get(className);
        assertEquals(Collections.singleton(resource), resources);
        parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        assertEquals(expectedParameters, parametersMap);
        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference2, documentReference2);
        verify(this.skinExtensionAsync, never()).use("ssx", resource2, parameters2);

        assertEquals(1, this.logCapture.size());
        assertEquals("Extensions present in [MySpace.MyOtherSSXPage] ignored because of lack of script right "
            + "from the author.", this.logCapture.getMessage(0));

        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(true);
        this.skinExtensionPlugin.use(resource2, parameters2, context);

        Set<String> expectedSet = new HashSet<>();
        expectedSet.add(resource);
        expectedSet.add(resource2);

        resources = (Set<String>) context.get(className);
        assertEquals(expectedSet, resources);

        parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        assertEquals(expectedParameters, parametersMap);
        verify(this.authorizationManager, times(2)).hasAccess(Right.SCRIPT, userReference2, documentReference2);
        verify(this.skinExtensionAsync).use("ssx", resource2, null);

        parameters2 = Collections.singletonMap("buzValue", 42);
        this.skinExtensionPlugin.use(resource2, parameters2, context);

        resources = (Set<String>) context.get(className);
        assertEquals(expectedSet, resources);
        parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        expectedParameters.put(resource2, parameters2);
        assertEquals(expectedParameters, parametersMap);
        verify(this.skinExtensionAsync).use("ssx", resource2, parameters2);

        this.skinExtensionPlugin.use(resource, null, context);
        expectedParameters.remove(resource);
        resources = (Set<String>) context.get(className);
        assertEquals(expectedSet, resources);
        parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        expectedParameters.put(resource2, parameters2);
        assertEquals(expectedParameters, parametersMap);
        verify(this.skinExtensionAsync).use("ssx", resource, null);
    }

    @Test
    void hasPageExtensions()
    {
        this.context.setDoc(null);
        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));

        XWikiDocument currentDoc = mock(XWikiDocument.class, "currentDoc");
        this.context.setDoc(currentDoc);

        String className = CssSkinExtensionPlugin.SSX_CLASS_NAME;
        when(currentDoc.getObjects(className)).thenReturn(null);
        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
        verify(currentDoc).getObjects(className);

        BaseObject baseObject = mock(BaseObject.class, "specificObj");
        when(baseObject.getStringValue("use")).thenReturn("wiki");
        Vector<BaseObject> objectList = new Vector<>();
        objectList.add(mock(BaseObject.class));
        objectList.add(null);
        objectList.add(mock(BaseObject.class));
        objectList.add(baseObject);
        objectList.add(null);
        objectList.add(mock(BaseObject.class));

        when(currentDoc.getObjects(className)).thenReturn(objectList);
        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
        verifyNoInteractions(this.authorizationManager);

        when(baseObject.getStringValue("use")).thenReturn("currentPage");
        DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "SomePage");
        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo");
        when(currentDoc.getDocumentReference()).thenReturn(documentReference);
        when(currentDoc.getAuthorReference()).thenReturn(userReference);

        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(false);
        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference);

        assertEquals(1, this.logCapture.size());
        assertEquals("Extensions present in [xwiki:MySpace.SomePage] ignored because of lack of script right "
            + "from the author.", this.logCapture.getMessage(0));

        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true);
        assertTrue(this.skinExtensionPlugin.hasPageExtensions(context));
    }
/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package com.xpn.xwiki.plugin.skinx;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.inject.Named;
import javax.inject.Provider;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.model.reference.WikiReference;
import org.xwiki.observation.ObservationManager;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.skinx.internal.async.SkinExtensionAsync;
import org.xwiki.test.LogLevel;
import org.xwiki.test.junit5.LogCaptureExtension;
import org.xwiki.test.junit5.mockito.MockComponent;

import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.classes.BaseClass;
import com.xpn.xwiki.test.MockitoOldcore;
import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
import com.xpn.xwiki.web.XWikiRequest;
import com.xpn.xwiki.web.XWikiURLFactory;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

/**
 * Tests for {@link JsSkinExtensionPlugin}.
 *
 * @version $Id$
 * @since 14.9RC1
 */
@OldcoreTest
class JsSkinExtensionPluginTest
{
    @InjectMockitoOldcore
    private MockitoOldcore mockitoOldcore;

    @MockComponent
    private Provider<DocumentReference> documentReferenceProvider;

    @MockComponent
    private ObservationManager observationManager;

    @MockComponent
    private EntityReferenceSerializer<String> entityReferenceSerializer;

    @MockComponent
    @Named("currentmixed")
    private DocumentReferenceResolver<String> currentMixedDocumentReferenceResolver;

    @MockComponent
    private DocumentReferenceResolver<String> documentReferenceResolver;

    @MockComponent
    @Named("current")
    private EntityReferenceResolver<String> currentEntityReferenceResolver;

    @MockComponent
    private AuthorizationManager authorizationManager;

    @MockComponent
    private SkinExtensionAsync skinExtensionAsync;

    @RegisterExtension
    LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);

    private XWikiContext context;
    private BaseClass pluginClass;
    private JsSkinExtensionPlugin skinExtensionPlugin;
    private XWikiURLFactory urlFactory;

    @BeforeEach
    void setup() throws XWikiException
    {
        XWikiDocument classDoc = mock(XWikiDocument.class);
        this.pluginClass = mock(BaseClass.class);

        this.context = mockitoOldcore.getXWikiContext();
        this.urlFactory = mock(XWikiURLFactory.class);
        context.setURLFactory(urlFactory);
        XWikiRequest xWikiRequest = mock(XWikiRequest.class);
        context.setRequest(xWikiRequest);
        XWiki wiki = mockitoOldcore.getSpyXWiki();
        when(this.currentMixedDocumentReferenceResolver.resolve(JsSkinExtensionPlugin.JSX_CLASS_NAME)).thenReturn(
            new DocumentReference(JsSkinExtensionPlugin.JSX_CLASS_REFERENCE, new WikiReference("xwiki")));
        when(wiki.getDocument(JsSkinExtensionPlugin.JSX_CLASS_NAME, context)).thenReturn(classDoc);
        when(classDoc.getXClass()).thenReturn(this.pluginClass);
        this.skinExtensionPlugin = new JsSkinExtensionPlugin("plugin1", "unused", context);
        skinExtensionPlugin.init(context);
    }

    @Test
    void use() throws XWikiException
    {
        String resource = "MySpace.MyJSXPage";
        Map<String, Object> parameters = Collections.singletonMap("fooParam", "barValue");

        DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "MyJSXPage");
        when(this.currentEntityReferenceResolver.resolve(resource, EntityType.DOCUMENT)).thenReturn(documentReference);
        when(this.entityReferenceSerializer.serialize(documentReference)).thenReturn(resource);

        when(this.documentReferenceResolver.resolve(resource)).thenReturn(documentReference);
        XWikiDocument document = mock(XWikiDocument.class);
        when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference, context)).thenReturn(document);

        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo");
        when(document.getAuthorReference()).thenReturn(userReference);
        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true);

        this.skinExtensionPlugin.use(resource, parameters, context);
        String className = JsSkinExtensionPlugin.class.getCanonicalName();
        Set<String> resources = (Set<String>) context.get(className);

        assertEquals(Collections.singleton(resource), resources);

        Map<String, Map<String, Object>> parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        Map<String, Map<String, Object>> expectedParameters = new HashMap<>();
        expectedParameters.put(resource, parameters);

        assertEquals(expectedParameters, parametersMap);
        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference);
        verify(this.skinExtensionAsync).use("jsx", resource, parameters);

        String resource2 = "MySpace.MyOtherJSXPage";
        Map<String, Object> parameters2 = null;
        DocumentReference documentReference2 = new DocumentReference("xwiki", "MySpace", "MyOtherJSXPage");

        when(this.currentEntityReferenceResolver.resolve(resource2, EntityType.DOCUMENT))
            .thenReturn(documentReference2);
        when(this.entityReferenceSerializer.serialize(documentReference2)).thenReturn(resource2);

        when(this.documentReferenceResolver.resolve(resource2)).thenReturn(documentReference2);
        XWikiDocument document2 = mock(XWikiDocument.class);
        when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference2, context)).thenReturn(document2);

        DocumentReference userReference2 = new DocumentReference("xwiki", "XWiki", "Bar");
        when(document2.getAuthorReference()).thenReturn(userReference2);
        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(false);

        this.skinExtensionPlugin.use(resource2, parameters2, context);
        resources = (Set<String>) context.get(className);
        assertEquals(Collections.singleton(resource), resources);
        parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        assertEquals(expectedParameters, parametersMap);
        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference2, documentReference2);
        verify(this.skinExtensionAsync, never()).use("jsx", resource2, parameters2);

        assertEquals(1, this.logCapture.size());
        assertEquals("Extensions present in [MySpace.MyOtherJSXPage] ignored because of lack of script right "
            + "from the author.", this.logCapture.getMessage(0));

        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(true);
        this.skinExtensionPlugin.use(resource2, parameters2, context);

        Set<String> expectedSet = new HashSet<>();
        expectedSet.add(resource);
        expectedSet.add(resource2);

        resources = (Set<String>) context.get(className);
        assertEquals(expectedSet, resources);

        parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        assertEquals(expectedParameters, parametersMap);
        verify(this.authorizationManager, times(2)).hasAccess(Right.SCRIPT, userReference2, documentReference2);
        verify(this.skinExtensionAsync).use("jsx", resource2, null);

        parameters2 = Collections.singletonMap("buzValue", 42);
        this.skinExtensionPlugin.use(resource2, parameters2, context);

        resources = (Set<String>) context.get(className);
        assertEquals(expectedSet, resources);
        parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        expectedParameters.put(resource2, parameters2);
        assertEquals(expectedParameters, parametersMap);
        verify(this.skinExtensionAsync).use("jsx", resource2, parameters2);

        this.skinExtensionPlugin.use(resource, null, context);
        expectedParameters.remove(resource);
        resources = (Set<String>) context.get(className);
        assertEquals(expectedSet, resources);
        parametersMap =
            (Map<String, Map<String, Object>>) context.get(className + "_parameters");
        expectedParameters.put(resource2, parameters2);
        assertEquals(expectedParameters, parametersMap);
        verify(this.skinExtensionAsync).use("jsx", resource, null);
    }

    @Test
    void hasPageExtensions()
    {
        this.context.setDoc(null);
        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));

        XWikiDocument currentDoc = mock(XWikiDocument.class, "currentDoc");
        this.context.setDoc(currentDoc);

        String className = JsSkinExtensionPlugin.JSX_CLASS_NAME;
        when(currentDoc.getObjects(className)).thenReturn(null);
        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
        verify(currentDoc).getObjects(className);

        BaseObject baseObject = mock(BaseObject.class, "specificObj");
        when(baseObject.getStringValue("use")).thenReturn("wiki");
        Vector<BaseObject> objectList = new Vector<>();
        objectList.add(mock(BaseObject.class));
        objectList.add(null);
        objectList.add(mock(BaseObject.class));
        objectList.add(baseObject);
        objectList.add(null);
        objectList.add(mock(BaseObject.class));

        when(currentDoc.getObjects(className)).thenReturn(objectList);
        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
        verifyNoInteractions(this.authorizationManager);

        when(baseObject.getStringValue("use")).thenReturn("currentPage");
        DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "SomePage");
        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo");
        when(currentDoc.getDocumentReference()).thenReturn(documentReference);
        when(currentDoc.getAuthorReference()).thenReturn(userReference);

        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(false);
        assertFalse(this.skinExtensionPlugin.hasPageExtensions(context));
        verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference);

        assertEquals(1, this.logCapture.size());
        assertEquals("Extensions present in [xwiki:MySpace.SomePage] ignored because of lack of script right "
            + "from the author.", this.logCapture.getMessage(0));

        when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true);
        assertTrue(this.skinExtensionPlugin.hasPageExtensions(context));
    }
}