CVE-2023-37912: Insufficient Information (NVD-CWE-noinfo)
value: XWiki Rendering is a generic Rendering system that converts textual input in a given syntax into another syntax. Prior to version 14.10.6 of `org.xwiki.platform:xwiki-core-rendering-macro-footnotes` and `org.xwiki.platform:xwiki-rendering-macro-footnotes` and prior to version 15.1-rc-1 of `org.xwiki.platform:xwiki-rendering-macro-footnotes`, the footnote macro executed its content in a potentially different context than the one in which it was defined. In particular in combination with the include macro, this allows privilege escalation from a simple user account in XWiki to programming rights and thus remote code execution, impacting the confidentiality, integrity and availability of the whole XWiki installation. This vulnerability has been patched in XWiki 14.10.6 and 15.1-rc-1. There is no workaround apart from upgrading to a fixed version of the footnote macro.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 8.8
Published: 2023-10-25T18:17Z
Repository: https://github.com/xwiki/xwiki-rendering
Language: Java
Files changed: xwiki-rendering-macros/xwiki-rendering-macro-footnotes/pom.xml, xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/FootnoteMacro.java, xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/PutFootnotesMacro.java, xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/java/org/xwiki/rendering/macro/footnote/ExecutedContentMacro.java, xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/META-INF/components.txt, xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote3.test, xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote6.test
Commit date: 2023-02-16 13:46:00 +0100
Commit message: XRENDERING-688: Improve footnote rendering (#246)

* Parse and execute the footnote's content directly instead of from the
  putFootnotes macro.
* Use the id generator to generate unique ids.
* Adapt the existing test to expect nested footnote support.
* Add new tests to test some more corner cases.
* Wrap the content of the footnote in a composite block to make it
  easier to handle.
* Remove Checkstyle suppressions.
Diff: diff --git a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/pom.xml b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/pom.xml
index a44e855ab..cf3e0ab0e 100644
--- a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/pom.xml
+++ b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/pom.xml
@@ -32,12 +32,10 @@
   <description>Adds a note of text placed (by default) at the bottom of the page</description>
   <properties>
     <xwiki.jacoco.instructionRatio>0.92</xwiki.jacoco.instructionRatio>
     <!-- Name to display by the Extension Manager -->
     <xwiki.extension.name>Footnote Macro</xwiki.extension.name>
-    <!-- {{putFootnotes}} is a complex macro that needs to instantiate several different XDOM classes -->
-    <checkstyle.suppressions.location>${basedir}/src/main/checkstyle/checkstyle-suppressions.xml</checkstyle.suppressions.location>
   </properties>
 
   <!-- Test dependencies -->
   <dependencies>
     <dependency>
diff --git a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/checkstyle/checkstyle-suppressions.xml b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/checkstyle/checkstyle-suppressions.xml
deleted file mode 100644
index 2e37132d4..000000000
--- a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/checkstyle/checkstyle-suppressions.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
- * 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.
--->
-
-<!DOCTYPE suppressions PUBLIC
-     "-//Puppy Crawl//DTD Suppressions 1.0//EN"
-     "http://www.puppycrawl.com/dtds/suppressions_1_0.dtd">
-
-<suppressions>
-  <!-- {{putFootnotes}} is a complex macro that needs to instantiate several different XDOM classes -->
-  <suppress checks="ClassDataAbstractionCoupling" files="PutFootnotesMacro.java" />
-</suppressions>
diff --git a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/FootnoteMacro.java b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/FootnoteMacro.java
index 90a1b49dd..3a58b737a 100644
--- a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/FootnoteMacro.java
+++ b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/FootnoteMacro.java
@@ -21,20 +21,22 @@
 
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
+import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.xwiki.component.annotation.Component;
 import org.xwiki.rendering.block.Block;
 import org.xwiki.rendering.block.MacroBlock;
 import org.xwiki.rendering.block.match.BlockMatcher;
 import org.xwiki.rendering.block.match.MacroBlockMatcher;
 import org.xwiki.rendering.block.match.MacroMarkerBlockMatcher;
 import org.xwiki.rendering.macro.AbstractMacro;
+import org.xwiki.rendering.macro.MacroContentParser;
 import org.xwiki.rendering.macro.MacroExecutionException;
 import org.xwiki.rendering.macro.descriptor.DefaultContentDescriptor;
 import org.xwiki.rendering.macro.footnote.FootnoteMacroParameters;
 import org.xwiki.rendering.transformation.MacroTransformationContext;
 
@@ -79,10 +81,16 @@ public class FootnoteMacro extends AbstractMacro<FootnoteMacroParameters>
      * Matches MacroMarkerBlocks having a macro id of {@link PutFootnotesMacro#MACRO_NAME}.
      */
     private static final BlockMatcher PUTFOOTNOTE_MARKER_MATCHER =
         new MacroMarkerBlockMatcher(PutFootnotesMacro.MACRO_NAME);
 
+    /**
+     * Used to parse the content of the footnote.
+     */
+    @Inject
+    private MacroContentParser contentParser;
+
     /**
      * Create and initialize the descriptor of the macro.
      */
     public FootnoteMacro()
     {
@@ -114,8 +122,9 @@ public List<Block> execute(FootnoteMacroParameters parameters, String content, M
             if (context.getCurrentMacroBlock().getFirstBlock(PUTFOOTNOTE_MARKER_MATCHER, Block.Axes.ANCESTOR) == null) {
                 Block putFootnotesMacro = new MacroBlock(PutFootnotesMacro.MACRO_NAME, Collections.emptyMap(), false);
                 root.addChild(putFootnotesMacro);
             }
         }
-        return Collections.emptyList();
+
+        return this.contentParser.parse(content, context, true, true).getChildren();
     }
 }
diff --git a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/PutFootnotesMacro.java b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/PutFootnotesMacro.java
index 841f0b4f3..7d9ca5264 100644
--- a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/PutFootnotesMacro.java
+++ b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/main/java/org/xwiki/rendering/internal/macro/footnote/PutFootnotesMacro.java
@@ -18,36 +18,41 @@
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package org.xwiki.rendering.internal.macro.footnote;
 
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.ListIterator;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.rendering.block.Block;
+import org.xwiki.rendering.block.CompositeBlock;
 import org.xwiki.rendering.block.FormatBlock;
 import org.xwiki.rendering.block.LinkBlock;
 import org.xwiki.rendering.block.ListItemBlock;
 import org.xwiki.rendering.block.MacroMarkerBlock;
 import org.xwiki.rendering.block.NumberedListBlock;
 import org.xwiki.rendering.block.SpaceBlock;
 import org.xwiki.rendering.block.WordBlock;
-import org.xwiki.rendering.block.match.ClassBlockMatcher;
+import org.xwiki.rendering.block.match.MacroMarkerBlockMatcher;
 import org.xwiki.rendering.listener.Format;
 import org.xwiki.rendering.listener.reference.DocumentResourceReference;
+import org.xwiki.rendering.listener.reference.ResourceReference;
 import org.xwiki.rendering.macro.AbstractMacro;
-import org.xwiki.rendering.macro.MacroContentParser;
-import org.xwiki.rendering.macro.MacroExecutionException;
 import org.xwiki.rendering.macro.footnote.FootnoteMacroParameters;
 import org.xwiki.rendering.transformation.MacroTransformationContext;
+import org.xwiki.rendering.util.IdGenerator;
 
 /**
  * List footnotes at the end of the page.
  *
  * @version $Id$
@@ -75,15 +80,72 @@ public class PutFootnotesMacro extends AbstractMacro<FootnoteMacroParameters>
     private static final String FOOTNOTE_ID_PREFIX = "x_footnote_";
 
     /** Prefix for the ID of the footnote. */
     private static final String FOOTNOTE_REFERENCE_ID_PREFIX = "x_footnote_ref_";
 
+    /** Class name for the reference to the footnote. */
+    private static final String FOOTNOTE_REF_CLASS_NAME = "footnoteRef";
+
     /**
-     * Used to parse the content of the macro.
+     * Internal data structure for representing a footnote.
      */
+    private static class Footnote
+    {
+        /**
+         * The HTML id of the footnote.
+         */
+        private String id;
+
+        /**
+         * The HTML id of the reference to the footnote (for jumping back).
+         */
+        private String referenceId;
+
+        /**
+         * The macro marker block of the footnote macro.
+         */
+        private final MacroMarkerBlock macroMarkerBlock;
+
+        /**
+         * The content of the footnote.
+         */
+        private CompositeBlock content;
+
+        /**
+         * Create a new footnote for the given footnote macro marker block.
+         * <p>
+         * If the only child is a format block with a reference id, the ids are extracted from the format block and
+         * its link child, otherwise the content of the macro marker is set as content and ids should be set later.
+         *
+         * @param macroMarkerBlock the footnote macro marker block
+         */
+        Footnote(MacroMarkerBlock macroMarkerBlock)
+        {
+            this.macroMarkerBlock = macroMarkerBlock;
+
+            if (macroMarkerBlock.getChildren().size() == 1
+                && macroMarkerBlock.getChildren().get(0) instanceof FormatBlock
+                && StringUtils.startsWith(macroMarkerBlock.getChildren().get(0).getParameter(ID_ATTRIBUTE_NAME),
+                FOOTNOTE_REFERENCE_ID_PREFIX))
+            {
+                Block formatBlock = macroMarkerBlock.getChildren().get(0);
+                if (!formatBlock.getChildren().isEmpty() && formatBlock.getChildren().get(0) instanceof LinkBlock) {
+                    LinkBlock linkBlock = (LinkBlock) formatBlock.getChildren().get(0);
+                    ResourceReference reference = linkBlock.getReference();
+                    if (reference instanceof DocumentResourceReference) {
+                        this.id = ((DocumentResourceReference) reference).getAnchor();
+                    }
+                }
+                this.referenceId = formatBlock.getParameter(ID_ATTRIBUTE_NAME);
+            } else {
+                this.content = new CompositeBlock(macroMarkerBlock.getChildren());
+            }
+        }
+    }
+
     @Inject
-    private MacroContentParser contentParser;
+    private Logger logger;
 
     /**
      * Create and initialize the descriptor of the macro.
      */
     public PutFootnotesMacro()
@@ -101,73 +163,125 @@ public boolean supportsInlineMode()
         return false;
     }
 
     @Override
     public List<Block> execute(FootnoteMacroParameters parameters, String content, MacroTransformationContext context)
-        throws MacroExecutionException
     {
         List<Block> result = Collections.emptyList();
 
-        // Get the list of footnotes in the document
         Block root = context.getXDOM();
-        List<MacroMarkerBlock> footnotes =
-            root.getBlocks(new ClassBlockMatcher(MacroMarkerBlock.class), Block.Axes.DESCENDANT);
-        for (ListIterator<MacroMarkerBlock> it = footnotes.listIterator(); it.hasNext();) {
-            MacroMarkerBlock macro = it.next();
-            if (FootnoteMacro.MACRO_NAME.equals(macro.getId())) {
-                continue;
-            } else if (PutFootnotesMacro.MACRO_NAME.equals(macro.getId())) {
-                macro.getParent().replaceChild(Collections.<Block>emptyList(), macro);
-            }
-            it.remove();
+        List<MacroMarkerBlock> macroMarkerBlocks =
+            root.getBlocks(new MacroMarkerBlockMatcher(PutFootnotesMacro.MACRO_NAME, FootnoteMacro.MACRO_NAME),
+                Block.Axes.DESCENDANT);
+
+        Map<String, Footnote> footnotes = new LinkedHashMap<>();
+
+        // Give footnotes without id temporary, numeric ids, we don't need to retrieve them by id.
+        AtomicInteger temporaryId = new AtomicInteger(0);
+        // Get the list of footnotes in the document
+        macroMarkerBlocks.stream()
+            .filter(macro -> FootnoteMacro.MACRO_NAME.equals(macro.getId()))
+            .map(Footnote::new)
+            .forEach(footnote -> footnotes.put(
+                Objects.requireNonNullElseGet(footnote.id, () -> String.valueOf(temporaryId.getAndIncrement())),
+                footnote));
+
+        // Collect the footnote content from the existing footnote lists and remove them.
+        macroMarkerBlocks.stream()
+            .filter(macro -> PutFootnotesMacro.MACRO_NAME.equals(macro.getId()))
+            .forEach(macro -> collectFootnoteContents(macro, footnotes));
+
+        IdGenerator idGenerator = null;
+        if (context.getXDOM() != null) {
+            idGenerator = context.getXDOM().getIdGenerator();
         }
-        if (footnotes.isEmpty()) {
-            return result;
+
+        if (!footnotes.isEmpty()) {
+            NumberedListBlock container = new NumberedListBlock(Collections.emptyList());
+            container.setParameter(CLASS_ATTRIBUTE_NAME, "footnotes");
+            result = Collections.singletonList(container);
+
+            int counter = 1;
+            for (Footnote footnote : footnotes.values()) {
+                if (footnote.content != null) {
+                    Block footnoteResult = processFootnote(footnote, counter, idGenerator);
+                    container.addChild(footnoteResult);
+                    counter++;
+                } else {
+                    this.logger.warn("No content for footnote [{}] found, ignoring it.",
+                        footnote.macroMarkerBlock);
+                }
+            }
         }
 
-        NumberedListBlock container = new NumberedListBlock(Collections.<Block>emptyList());
-        container.setParameter(CLASS_ATTRIBUTE_NAME, "footnotes");
-        Block footnoteResult;
+        return result;
+    }
 
-        int counter = 1;
-        for (MacroMarkerBlock footnote : footnotes) {
-            footnoteResult = processFootnote(footnote, counter, context);
-            if (footnoteResult != null) {
-                container.addChild(footnoteResult);
-                counter++;
+    /**
+     * Collect and remove footnote contents from the given putFootnotes macro marker block.
+     *
+     * @param macro the putFootnotes macro marker block from which footnote contents shall be collected
+     * @param footnotes the footnotes where the content shall be collected
+     */
+    private void collectFootnoteContents(MacroMarkerBlock macro, Map<String, Footnote> footnotes)
+    {
+        if (macro.getChildren().size() == 1 && macro.getChildren().get(0) instanceof NumberedListBlock) {
+            for (Block listItemBlock : macro.getChildren().get(0).getChildren()) {
+                if (listItemBlock.getChildren().size() == 3
+                    && listItemBlock.getChildren().get(2) instanceof CompositeBlock
+                    && StringUtils.startsWith(listItemBlock.getChildren().get(0).getParameter(ID_ATTRIBUTE_NAME),
+                    FOOTNOTE_ID_PREFIX))
+                {
+                    CompositeBlock footnoteContent = (CompositeBlock) listItemBlock.getChildren().get(2);
+                    String id = listItemBlock.getChildren().get(0).getParameter(ID_ATTRIBUTE_NAME);
+                    if (footnotes.containsKey(id)) {
+                        footnotes.get(id).content = footnoteContent;
+                    } else {
+                        this.logger.warn("Could not find footnote marker for footnote [{}], ignoring it.",
+                            footnoteContent);
+                    }
+                }
             }
         }
 
-        return Collections.<Block>singletonList(container);
+        macro.getParent().removeBlock(macro);
     }
 
     /**
-     * Processes a {{footnote}} macro, by generating a footnote element to insert in the footnote list and a reference
+     * Processes a footnote macro, by generating a footnote element to insert in the footnote list and a reference
      * to it, which is placed instead of the macro call.
      *
-     * @param footnoteMacro the {{footnote}} macro element
+     * @param footnote the footnote
      * @param counter the current footnote counter
-     * @param context the execution context of the macro
      * @return the footnote element which should be inserted in the footnote list
-     * @throws MacroExecutionException if the footnote content cannot be further processed
      */
-    private ListItemBlock processFootnote(MacroMarkerBlock footnoteMacro, int counter,
-        MacroTransformationContext context) throws MacroExecutionException
+    private ListItemBlock processFootnote(Footnote footnote, int counter, IdGenerator idGenerator)
     {
-        String content = footnoteMacro.getContent();
-        if (StringUtils.isBlank(content)) {
-            content = " ";
+        if (footnote.referenceId == null) {
+            footnote.referenceId = generateId(counter, FOOTNOTE_REFERENCE_ID_PREFIX, idGenerator);
         }
+
+        if (footnote.id == null) {
+            footnote.id = generateId(counter, FOOTNOTE_ID_PREFIX, idGenerator);
+        }
+
         // Construct the footnote and reference blocks
-        Block referenceBlock = createFootnoteReferenceBlock(counter);
-        ListItemBlock footnoteBlock = createFootnoteBlock(content, counter, context);
+        Block referenceBlock = createFootnoteReferenceBlock(counter, footnote.id, footnote.referenceId);
+        ListItemBlock footnoteBlock = createFootnoteBlock(footnote.content, footnote.id, footnote.referenceId);
         // Insert the footnote and the reference in the document.
-        if (referenceBlock != null && footnoteBlock != null) {
-            addFootnoteRef(footnoteMacro, referenceBlock);
-            return footnoteBlock;
+        addFootnoteRef(footnote.macroMarkerBlock, referenceBlock);
+        return footnoteBlock;
+    }
+
+    private String generateId(int counter, String idPrefix, IdGenerator idGenerator)
+    {
+        String footnoteId = idPrefix + counter;
+        if (idGenerator != null) {
+            footnoteId = idGenerator.generateUniqueId(footnoteId.substring(0, 1), footnoteId.substring(1));
         }
-        return null;
+
+        return footnoteId;
     }
 
     /**
      * Add a footnote to the list of document footnotes. If such a list doesn't exist yet, create it and append it to
      * the end of the document.
@@ -184,50 +298,44 @@ private void addFootnoteRef(MacroMarkerBlock footnoteMacro, Block footnoteRef)
     /**
      * Generate the footnote reference (link) that should be inserted at the location of the macro, and should point to
      * the actual footnote at the end of the document.
      *
      * @param counter the current footnote counter
+     * @param footnoteId the id of the footnote
+     * @param referenceId the id of the reference
      * @return the generated reference element, displayed as {@code (superscript(link(footnote index)))}
      */
-    private Block createFootnoteReferenceBlock(int counter)
+    private Block createFootnoteReferenceBlock(int counter, String footnoteId, String referenceId)
     {
         Block result = new WordBlock(String.valueOf(counter));
         DocumentResourceReference reference = new DocumentResourceReference(null);
-        reference.setAnchor(FOOTNOTE_ID_PREFIX + counter);
+        reference.setAnchor(footnoteId);
         result = new LinkBlock(Collections.singletonList(result), reference, false);
         result = new FormatBlock(Collections.singletonList(result), Format.SUPERSCRIPT);
-        result.setParameter(ID_ATTRIBUTE_NAME, FOOTNOTE_REFERENCE_ID_PREFIX + counter);
-        result.setParameter(CLASS_ATTRIBUTE_NAME, "footnoteRef");
+        result.setParameter(ID_ATTRIBUTE_NAME, referenceId);
+        result.setParameter(CLASS_ATTRIBUTE_NAME, FOOTNOTE_REF_CLASS_NAME);
         return result;
     }
 
     /**
      * Generate the footnote block, a numbered list item containing a backlink to the footnote's reference, and the
-     * actual footnote text, parsed into XDOM.
+     * actual footnote content.
      *
-     * @param content the string representation of the actual footnote text; the content of the macro
-     * @param counter the current footnote counter
-     * @param context the macro transformation context, used for obtaining the correct parser for parsing the content
+     * @param content the block with the actual footnote content
+     * @param footnoteId the id of the footnote
+     * @param referenceId the id of the reference
      * @return the generated footnote block
-     * @throws MacroExecutionException if parsing the content fails
      */
-    private ListItemBlock createFootnoteBlock(String content, int counter, MacroTransformationContext context)
-        throws MacroExecutionException
+    private ListItemBlock createFootnoteBlock(CompositeBlock content, String footnoteId, String referenceId)
     {
-        List<Block> parsedContent;
-        try {
-            parsedContent = this.contentParser.parse(content, context, false, true).getChildren();
-        } catch (MacroExecutionException e) {
-            parsedContent = Collections.<Block>singletonList(new WordBlock(content));
-        }
         Block result = new WordBlock("^");
         DocumentResourceReference reference = new DocumentResourceReference(null);
-        reference.setAnchor(FOOTNOTE_REFERENCE_ID_PREFIX + counter);
+        reference.setAnchor(referenceId);
         result = new LinkBlock(Collections.singletonList(result), reference, false);
-        result.setParameter(ID_ATTRIBUTE_NAME, FOOTNOTE_ID_PREFIX + counter);
+        result.setParameter(ID_ATTRIBUTE_NAME, footnoteId);
         result.setParameter(CLASS_ATTRIBUTE_NAME, "footnoteBackRef");
         result = new ListItemBlock(Collections.singletonList(result));
         result.addChild(new SpaceBlock());
-        result.addChildren(parsedContent);
+        result.addChild(content);
         return (ListItemBlock) result;
     }
 }
diff --git a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/java/org/xwiki/rendering/macro/footnote/ExecutedContentMacro.java b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/java/org/xwiki/rendering/macro/footnote/ExecutedContentMacro.java
new file mode 100644
index 000000000..b2905f7e5
--- /dev/null
+++ b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/java/org/xwiki/rendering/macro/footnote/ExecutedContentMacro.java
@@ -0,0 +1,75 @@
+/*
+ * 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 org.xwiki.rendering.macro.footnote;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.rendering.block.Block;
+import org.xwiki.rendering.macro.AbstractNoParameterMacro;
+import org.xwiki.rendering.macro.MacroContentParser;
+import org.xwiki.rendering.macro.MacroExecutionException;
+import org.xwiki.rendering.macro.descriptor.DefaultContentDescriptor;
+import org.xwiki.rendering.transformation.MacroTransformationContext;
+
+/**
+ * Executed content macro used in some {@code *.test} files.
+ * <p>
+ * This macro executes its content and thus allows putFootnotes macros to be executed early.
+ *
+ * @version $Id$
+ */
+@Component
+@Named("executedcontent")
+@Singleton
+public class ExecutedContentMacro extends AbstractNoParameterMacro
+{
+    /**
+     * The parser used to parse the content.
+     */
+    @Inject
+    private MacroContentParser contentParser;
+
+    /**
+     * Default constructor.
+     */
+    public ExecutedContentMacro()
+    {
+        super("Executed Content Macro", "Executes its content.",
+            new DefaultContentDescriptor("Content", true, Block.LIST_BLOCK_TYPE));
+    }
+
+    @Override
+    public boolean supportsInlineMode()
+    {
+        return false;
+    }
+
+    @Override
+    public List<Block> execute(Object parameters, String content, MacroTransformationContext context)
+        throws MacroExecutionException
+    {
+        return this.contentParser.parse(content, context, true, context.isInline()).getChildren();
+    }
+}
diff --git a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/META-INF/components.txt b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/META-INF/components.txt
new file mode 100644
index 000000000..11610f556
--- /dev/null
+++ b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/META-INF/components.txt
@@ -0,0 +1 @@
+org.xwiki.rendering.macro.footnote.ExecutedContentMacro
\ No newline at end of file
diff --git a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote3.test b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote3.test
index 6b86e6f2d..161c34fcd 100644
--- a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote3.test
+++ b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote3.test
@@ -11,26 +11,39 @@ Nested {{footnote}}footnote{{footnote}} content
 beginDocument
 beginParagraph
 onWord [Nested]
 onSpace
 beginMacroMarkerInline [footnote] [] [footnote{{footnote}} content]
-beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
-beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
+beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-1]]
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-1]]] [false]
 onWord [1]
-endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
-endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-1]]] [false]
+endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-1]]
 endMacroMarkerInline [footnote] [] [footnote{{footnote}} content]
 endParagraph
 beginMacroMarkerStandalone [putFootnotes] []
 beginList [NUMBERED] [[class]=[footnotes]]
 beginListItem
-beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-1]]
 onWord [^]
-endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-1]]
 onSpace
 onWord [footnote]
 beginMacroMarkerInline [footnote] [] [ content]
+beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
+onWord [2]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
+endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
 endMacroMarkerInline [footnote] [] [ content]
 endListItem
+beginListItem
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
+onWord [^]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
+onSpace
+onSpace
+onWord [content]
+endListItem
 endList [NUMBERED] [[class]=[footnotes]]
 endMacroMarkerStandalone [putFootnotes] []
-endDocument
+endDocument
\ No newline at end of file
diff --git a/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote6.test b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote6.test
new file mode 100644
index 000000000..2b1d5ea02
--- /dev/null
+++ b/xwiki-rendering-macros/xwiki-rendering-macro-footnotes/src/test/resources/macrofootnote6.test
@@ -0,0 +1,138 @@
+.runTransformations
+.#-----------------------------------------------------
+.input|xwiki/2.0
+.# Verify that several footnotes and putFootnotes markers work.
+.#-----------------------------------------------------
+Footnote{{footnote}}content 1{{/footnote}} {{footnote}}content 2{{/footnote}}
+
+{{putFootnotes/}}
+
+{{executedcontent}}
+{{footnote}}Inner footnote{{/footnote}}
+{{/executedcontent}}
+
+{{executedcontent}}
+{{footnote}}Second inner footnote{{/footnote}}
+{{/executedcontent}}
+
+
+{{footnote}}These are the footnotes:
+
+{{putFootnotes/}}
+
+{{/footnote}}
+
+{{putFootnotes/}}
+.#-----------------------------------------------------
+.expect|event/1.0
+.#-----------------------------------------------------
+beginDocument
+beginParagraph
+onWord [Footnote]
+beginMacroMarkerInline [footnote] [] [content 1]
+beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-2]]
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-2]]] [false]
+onWord [1]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-2]]] [false]
+endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-2]]
+endMacroMarkerInline [footnote] [] [content 1]
+onSpace
+beginMacroMarkerInline [footnote] [] [content 2]
+beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_2]]
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_2]]] [false]
+onWord [2]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_2]]] [false]
+endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_2]]
+endMacroMarkerInline [footnote] [] [content 2]
+endParagraph
+beginMacroMarkerStandalone [executedcontent] [] [{{footnote}}Inner footnote{{/footnote}}]
+beginMacroMarkerStandalone [footnote] [] [Inner footnote]
+beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
+onWord [3]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
+endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
+endMacroMarkerStandalone [footnote] [] [Inner footnote]
+endMacroMarkerStandalone [executedcontent] [] [{{footnote}}Inner footnote{{/footnote}}]
+beginMacroMarkerStandalone [executedcontent] [] [{{footnote}}Second inner footnote{{/footnote}}]
+beginMacroMarkerStandalone [footnote] [] [Second inner footnote]
+beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-1]]
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-1]]] [false]
+onWord [4]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-1]]] [false]
+endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-1]]
+endMacroMarkerStandalone [footnote] [] [Second inner footnote]
+endMacroMarkerStandalone [executedcontent] [] [{{footnote}}Second inner footnote{{/footnote}}]
+onEmptyLines [1]
+beginMacroMarkerStandalone [footnote] [] [These are the footnotes:
+
+{{putFootnotes/}}
+]
+beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_5]]
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_5]]] [false]
+onWord [5]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_5]]] [false]
+endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_5]]
+endMacroMarkerStandalone [footnote] [] [These are the footnotes:
+
+{{putFootnotes/}}
+]
+beginMacroMarkerStandalone [putFootnotes] []
+beginList [NUMBERED] [[class]=[footnotes]]
+beginListItem
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-2]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-2]]
+onWord [^]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-2]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-2]]
+onSpace
+onWord [content]
+onSpace
+onWord [1]
+endListItem
+beginListItem
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_2]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_2]]
+onWord [^]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_2]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_2]]
+onSpace
+onWord [content]
+onSpace
+onWord [2]
+endListItem
+beginListItem
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
+onWord [^]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
+onSpace
+onWord [Inner]
+onSpace
+onWord [footnote]
+endListItem
+beginListItem
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-1]]
+onWord [^]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-1]]
+onSpace
+onWord [Second]
+onSpace
+onWord [inner]
+onSpace
+onWord [footnote]
+endListItem
+beginListItem
+beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_5]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_5]]
+onWord [^]
+endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_5]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_5]]
+onSpace
+beginParagraph
+onWord [These]
+onSpace
+onWord [are]
+onSpace
+onWord [the]
+onSpace
+onWord [footnotes]
+onSpecialSymbol [:]
+endParagraph
+endListItem
+endList [NUMBERED] [[class]=[footnotes]]
+endMacroMarkerStandalone [putFootnotes] []
+endDocument

Vulnerable code:     <!-- {{putFootnotes}} is a complex macro that needs to instantiate several different XDOM classes -->
    <checkstyle.suppressions.location>${basedir}/src/main/checkstyle/checkstyle-suppressions.xml</checkstyle.suppressions.location>
<?xml version="1.0" encoding="UTF-8"?>

<!--
 * 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.

<!DOCTYPE suppressions PUBLIC
     "-//Puppy Crawl//DTD Suppressions 1.0//EN"
     "http://www.puppycrawl.com/dtds/suppressions_1_0.dtd">

<suppressions>
  <!-- {{putFootnotes}} is a complex macro that needs to instantiate several different XDOM classes -->
  <suppress checks="ClassDataAbstractionCoupling" files="PutFootnotesMacro.java" />
</suppressions>
        return Collections.emptyList();
import java.util.ListIterator;
import org.xwiki.rendering.block.match.ClassBlockMatcher;
import org.xwiki.rendering.macro.MacroContentParser;
import org.xwiki.rendering.macro.MacroExecutionException;
     * Used to parse the content of the macro.
    private MacroContentParser contentParser;
        throws MacroExecutionException
        // Get the list of footnotes in the document
        List<MacroMarkerBlock> footnotes =
            root.getBlocks(new ClassBlockMatcher(MacroMarkerBlock.class), Block.Axes.DESCENDANT);
        for (ListIterator<MacroMarkerBlock> it = footnotes.listIterator(); it.hasNext();) {
            MacroMarkerBlock macro = it.next();
            if (FootnoteMacro.MACRO_NAME.equals(macro.getId())) {
                continue;
            } else if (PutFootnotesMacro.MACRO_NAME.equals(macro.getId())) {
                macro.getParent().replaceChild(Collections.<Block>emptyList(), macro);
            }
            it.remove();
        if (footnotes.isEmpty()) {
            return result;
        NumberedListBlock container = new NumberedListBlock(Collections.<Block>emptyList());
        container.setParameter(CLASS_ATTRIBUTE_NAME, "footnotes");
        Block footnoteResult;
        int counter = 1;
        for (MacroMarkerBlock footnote : footnotes) {
            footnoteResult = processFootnote(footnote, counter, context);
            if (footnoteResult != null) {
                container.addChild(footnoteResult);
                counter++;
        return Collections.<Block>singletonList(container);
     * Processes a {{footnote}} macro, by generating a footnote element to insert in the footnote list and a reference
     * @param footnoteMacro the {{footnote}} macro element
     * @param context the execution context of the macro
     * @throws MacroExecutionException if the footnote content cannot be further processed
    private ListItemBlock processFootnote(MacroMarkerBlock footnoteMacro, int counter,
        MacroTransformationContext context) throws MacroExecutionException
        String content = footnoteMacro.getContent();
        if (StringUtils.isBlank(content)) {
            content = " ";
        Block referenceBlock = createFootnoteReferenceBlock(counter);
        ListItemBlock footnoteBlock = createFootnoteBlock(content, counter, context);
        if (referenceBlock != null && footnoteBlock != null) {
            addFootnoteRef(footnoteMacro, referenceBlock);
            return footnoteBlock;
        return null;
    private Block createFootnoteReferenceBlock(int counter)
        reference.setAnchor(FOOTNOTE_ID_PREFIX + counter);
        result.setParameter(ID_ATTRIBUTE_NAME, FOOTNOTE_REFERENCE_ID_PREFIX + counter);
        result.setParameter(CLASS_ATTRIBUTE_NAME, "footnoteRef");
     * actual footnote text, parsed into XDOM.
     * @param content the string representation of the actual footnote text; the content of the macro
     * @param counter the current footnote counter
     * @param context the macro transformation context, used for obtaining the correct parser for parsing the content
     * @throws MacroExecutionException if parsing the content fails
    private ListItemBlock createFootnoteBlock(String content, int counter, MacroTransformationContext context)
        throws MacroExecutionException
        List<Block> parsedContent;
        try {
            parsedContent = this.contentParser.parse(content, context, false, true).getChildren();
        } catch (MacroExecutionException e) {
            parsedContent = Collections.<Block>singletonList(new WordBlock(content));
        }
        reference.setAnchor(FOOTNOTE_REFERENCE_ID_PREFIX + counter);
        result.setParameter(ID_ATTRIBUTE_NAME, FOOTNOTE_ID_PREFIX + counter);
        result.addChildren(parsedContent);
beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
endDocument
Fixed code: import javax.inject.Inject;
import org.xwiki.rendering.macro.MacroContentParser;
    /**
     * Used to parse the content of the footnote.
     */
    @Inject
    private MacroContentParser contentParser;


        return this.contentParser.parse(content, context, true, true).getChildren();
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.xwiki.rendering.block.CompositeBlock;
import org.xwiki.rendering.block.match.MacroMarkerBlockMatcher;
import org.xwiki.rendering.listener.reference.ResourceReference;
import org.xwiki.rendering.util.IdGenerator;
    /** Class name for the reference to the footnote. */
    private static final String FOOTNOTE_REF_CLASS_NAME = "footnoteRef";

     * Internal data structure for representing a footnote.
    private static class Footnote
    {
        /**
         * The HTML id of the footnote.
         */
        private String id;

        /**
         * The HTML id of the reference to the footnote (for jumping back).
         */
        private String referenceId;

        /**
         * The macro marker block of the footnote macro.
         */
        private final MacroMarkerBlock macroMarkerBlock;

        /**
         * The content of the footnote.
         */
        private CompositeBlock content;

        /**
         * Create a new footnote for the given footnote macro marker block.
         * <p>
         * If the only child is a format block with a reference id, the ids are extracted from the format block and
         * its link child, otherwise the content of the macro marker is set as content and ids should be set later.
         *
         * @param macroMarkerBlock the footnote macro marker block
         */
        Footnote(MacroMarkerBlock macroMarkerBlock)
        {
            this.macroMarkerBlock = macroMarkerBlock;

            if (macroMarkerBlock.getChildren().size() == 1
                && macroMarkerBlock.getChildren().get(0) instanceof FormatBlock
                && StringUtils.startsWith(macroMarkerBlock.getChildren().get(0).getParameter(ID_ATTRIBUTE_NAME),
                FOOTNOTE_REFERENCE_ID_PREFIX))
            {
                Block formatBlock = macroMarkerBlock.getChildren().get(0);
                if (!formatBlock.getChildren().isEmpty() && formatBlock.getChildren().get(0) instanceof LinkBlock) {
                    LinkBlock linkBlock = (LinkBlock) formatBlock.getChildren().get(0);
                    ResourceReference reference = linkBlock.getReference();
                    if (reference instanceof DocumentResourceReference) {
                        this.id = ((DocumentResourceReference) reference).getAnchor();
                    }
                }
                this.referenceId = formatBlock.getParameter(ID_ATTRIBUTE_NAME);
            } else {
                this.content = new CompositeBlock(macroMarkerBlock.getChildren());
            }
        }
    }

    private Logger logger;
        List<MacroMarkerBlock> macroMarkerBlocks =
            root.getBlocks(new MacroMarkerBlockMatcher(PutFootnotesMacro.MACRO_NAME, FootnoteMacro.MACRO_NAME),
                Block.Axes.DESCENDANT);

        Map<String, Footnote> footnotes = new LinkedHashMap<>();

        // Give footnotes without id temporary, numeric ids, we don't need to retrieve them by id.
        AtomicInteger temporaryId = new AtomicInteger(0);
        // Get the list of footnotes in the document
        macroMarkerBlocks.stream()
            .filter(macro -> FootnoteMacro.MACRO_NAME.equals(macro.getId()))
            .map(Footnote::new)
            .forEach(footnote -> footnotes.put(
                Objects.requireNonNullElseGet(footnote.id, () -> String.valueOf(temporaryId.getAndIncrement())),
                footnote));

        // Collect the footnote content from the existing footnote lists and remove them.
        macroMarkerBlocks.stream()
            .filter(macro -> PutFootnotesMacro.MACRO_NAME.equals(macro.getId()))
            .forEach(macro -> collectFootnoteContents(macro, footnotes));

        IdGenerator idGenerator = null;
        if (context.getXDOM() != null) {
            idGenerator = context.getXDOM().getIdGenerator();

        if (!footnotes.isEmpty()) {
            NumberedListBlock container = new NumberedListBlock(Collections.emptyList());
            container.setParameter(CLASS_ATTRIBUTE_NAME, "footnotes");
            result = Collections.singletonList(container);

            int counter = 1;
            for (Footnote footnote : footnotes.values()) {
                if (footnote.content != null) {
                    Block footnoteResult = processFootnote(footnote, counter, idGenerator);
                    container.addChild(footnoteResult);
                    counter++;
                } else {
                    this.logger.warn("No content for footnote [{}] found, ignoring it.",
                        footnote.macroMarkerBlock);
                }
            }
        return result;
    }
    /**
     * Collect and remove footnote contents from the given putFootnotes macro marker block.
     *
     * @param macro the putFootnotes macro marker block from which footnote contents shall be collected
     * @param footnotes the footnotes where the content shall be collected
     */
    private void collectFootnoteContents(MacroMarkerBlock macro, Map<String, Footnote> footnotes)
    {
        if (macro.getChildren().size() == 1 && macro.getChildren().get(0) instanceof NumberedListBlock) {
            for (Block listItemBlock : macro.getChildren().get(0).getChildren()) {
                if (listItemBlock.getChildren().size() == 3
                    && listItemBlock.getChildren().get(2) instanceof CompositeBlock
                    && StringUtils.startsWith(listItemBlock.getChildren().get(0).getParameter(ID_ATTRIBUTE_NAME),
                    FOOTNOTE_ID_PREFIX))
                {
                    CompositeBlock footnoteContent = (CompositeBlock) listItemBlock.getChildren().get(2);
                    String id = listItemBlock.getChildren().get(0).getParameter(ID_ATTRIBUTE_NAME);
                    if (footnotes.containsKey(id)) {
                        footnotes.get(id).content = footnoteContent;
                    } else {
                        this.logger.warn("Could not find footnote marker for footnote [{}], ignoring it.",
                            footnoteContent);
                    }
                }
        macro.getParent().removeBlock(macro);
     * Processes a footnote macro, by generating a footnote element to insert in the footnote list and a reference
     * @param footnote the footnote
    private ListItemBlock processFootnote(Footnote footnote, int counter, IdGenerator idGenerator)
        if (footnote.referenceId == null) {
            footnote.referenceId = generateId(counter, FOOTNOTE_REFERENCE_ID_PREFIX, idGenerator);

        if (footnote.id == null) {
            footnote.id = generateId(counter, FOOTNOTE_ID_PREFIX, idGenerator);
        }

        Block referenceBlock = createFootnoteReferenceBlock(counter, footnote.id, footnote.referenceId);
        ListItemBlock footnoteBlock = createFootnoteBlock(footnote.content, footnote.id, footnote.referenceId);
        addFootnoteRef(footnote.macroMarkerBlock, referenceBlock);
        return footnoteBlock;
    }

    private String generateId(int counter, String idPrefix, IdGenerator idGenerator)
    {
        String footnoteId = idPrefix + counter;
        if (idGenerator != null) {
            footnoteId = idGenerator.generateUniqueId(footnoteId.substring(0, 1), footnoteId.substring(1));

        return footnoteId;
     * @param footnoteId the id of the footnote
     * @param referenceId the id of the reference
    private Block createFootnoteReferenceBlock(int counter, String footnoteId, String referenceId)
        reference.setAnchor(footnoteId);
        result.setParameter(ID_ATTRIBUTE_NAME, referenceId);
        result.setParameter(CLASS_ATTRIBUTE_NAME, FOOTNOTE_REF_CLASS_NAME);
     * actual footnote content.
     * @param content the block with the actual footnote content
     * @param footnoteId the id of the footnote
     * @param referenceId the id of the reference
    private ListItemBlock createFootnoteBlock(CompositeBlock content, String footnoteId, String referenceId)
        reference.setAnchor(referenceId);
        result.setParameter(ID_ATTRIBUTE_NAME, footnoteId);
        result.addChild(content);
/*
 * 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 org.xwiki.rendering.macro.footnote;

import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.macro.AbstractNoParameterMacro;
import org.xwiki.rendering.macro.MacroContentParser;
import org.xwiki.rendering.macro.MacroExecutionException;
import org.xwiki.rendering.macro.descriptor.DefaultContentDescriptor;
import org.xwiki.rendering.transformation.MacroTransformationContext;

/**
 * Executed content macro used in some {@code *.test} files.
 * <p>
 * This macro executes its content and thus allows putFootnotes macros to be executed early.
 *
 * @version $Id$
 */
@Component
@Named("executedcontent")
@Singleton
public class ExecutedContentMacro extends AbstractNoParameterMacro
{
    /**
     * The parser used to parse the content.
     */
    @Inject
    private MacroContentParser contentParser;

    /**
     * Default constructor.
     */
    public ExecutedContentMacro()
    {
        super("Executed Content Macro", "Executes its content.",
            new DefaultContentDescriptor("Content", true, Block.LIST_BLOCK_TYPE));
    }

    @Override
    public boolean supportsInlineMode()
    {
        return false;
    }

    @Override
    public List<Block> execute(Object parameters, String content, MacroTransformationContext context)
        throws MacroExecutionException
    {
        return this.contentParser.parse(content, context, true, context.isInline()).getChildren();
    }
}
org.xwiki.rendering.macro.footnote.ExecutedContentMacro
beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-1]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-1]]] [false]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-1]]] [false]
endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-1]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-1]]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-1]]
beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
onWord [2]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
beginListItem
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
onWord [^]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
onSpace
onSpace
onWord [content]
endListItem
endDocument
.runTransformations
.#-----------------------------------------------------
.input|xwiki/2.0
.# Verify that several footnotes and putFootnotes markers work.
.#-----------------------------------------------------
Footnote{{footnote}}content 1{{/footnote}} {{footnote}}content 2{{/footnote}}

{{putFootnotes/}}

{{executedcontent}}
{{footnote}}Inner footnote{{/footnote}}
{{/executedcontent}}

{{executedcontent}}
{{footnote}}Second inner footnote{{/footnote}}
{{/executedcontent}}


{{footnote}}These are the footnotes:

{{putFootnotes/}}

{{/footnote}}

{{putFootnotes/}}
.#-----------------------------------------------------
.expect|event/1.0
.#-----------------------------------------------------
beginDocument
beginParagraph
onWord [Footnote]
beginMacroMarkerInline [footnote] [] [content 1]
beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-2]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-2]]] [false]
onWord [1]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-2]]] [false]
endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-2]]
endMacroMarkerInline [footnote] [] [content 1]
onSpace
beginMacroMarkerInline [footnote] [] [content 2]
beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_2]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_2]]] [false]
onWord [2]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_2]]] [false]
endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_2]]
endMacroMarkerInline [footnote] [] [content 2]
endParagraph
beginMacroMarkerStandalone [executedcontent] [] [{{footnote}}Inner footnote{{/footnote}}]
beginMacroMarkerStandalone [footnote] [] [Inner footnote]
beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
onWord [3]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1]]] [false]
endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1]]
endMacroMarkerStandalone [footnote] [] [Inner footnote]
endMacroMarkerStandalone [executedcontent] [] [{{footnote}}Inner footnote{{/footnote}}]
beginMacroMarkerStandalone [executedcontent] [] [{{footnote}}Second inner footnote{{/footnote}}]
beginMacroMarkerStandalone [footnote] [] [Second inner footnote]
beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-1]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-1]]] [false]
onWord [4]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_1-1]]] [false]
endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_1-1]]
endMacroMarkerStandalone [footnote] [] [Second inner footnote]
endMacroMarkerStandalone [executedcontent] [] [{{footnote}}Second inner footnote{{/footnote}}]
onEmptyLines [1]
beginMacroMarkerStandalone [footnote] [] [These are the footnotes:

{{putFootnotes/}}
]
beginFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_5]]
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_5]]] [false]
onWord [5]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_5]]] [false]
endFormat [SUPERSCRIPT] [[class]=[footnoteRef][id]=[x_footnote_ref_5]]
endMacroMarkerStandalone [footnote] [] [These are the footnotes:

{{putFootnotes/}}
]
beginMacroMarkerStandalone [putFootnotes] []
beginList [NUMBERED] [[class]=[footnotes]]
beginListItem
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-2]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-2]]
onWord [^]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-2]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-2]]
onSpace
onWord [content]
onSpace
onWord [1]
endListItem
beginListItem
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_2]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_2]]
onWord [^]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_2]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_2]]
onSpace
onWord [content]
onSpace
onWord [2]
endListItem
beginListItem
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
onWord [^]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1]]
onSpace
onWord [Inner]
onSpace
onWord [footnote]
endListItem
beginListItem
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-1]]
onWord [^]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_1-1]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_1-1]]
onSpace
onWord [Second]
onSpace
onWord [inner]
onSpace
onWord [footnote]
endListItem
beginListItem
beginLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_5]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_5]]
onWord [^]
endLink [Typed = [true] Type = [doc] Parameters = [[anchor] = [x_footnote_ref_5]]] [false] [[class]=[footnoteBackRef][id]=[x_footnote_5]]
onSpace
beginParagraph
onWord [These]
onSpace
onWord [are]
onSpace
onWord [the]
onSpace
onWord [footnotes]
onSpecialSymbol [:]
endParagraph
endListItem
endList [NUMBERED] [[class]=[footnotes]]
endMacroMarkerStandalone [putFootnotes] []
endDocument