CVE-2023-37912 (published 2023-10-25T18:17Z)

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.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/xwiki/xwiki-rendering
commit_details: repo_url: https://github.com/xwiki/xwiki-rendering
author: Michael Hamann
author_date: 2023-02-16 13:46:00+01:00
msg: 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.
repository: repo_url: https://github.com/xwiki/xwiki-rendering
repo_name: xwiki/xwiki-rendering
description: The XWiki Rendering Engine
repo_language: Java
owner: xwiki
file_changes: filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -34,8 +34,6 @@
     <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 -->

programming_language: HTML
filename: checkstyle-suppressions.xml
change_type: ModificationType.DELETE
diff: @@ -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>

programming_language: unknown
filename: FootnoteMacro.java
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,7 @@
 import java.util.List;
 import java.util.Set;
 
+import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
@@ -33,6 +34,7 @@
 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;
@@ -81,6 +83,12 @@ public class FootnoteMacro extends AbstractMacro<FootnoteMacroParameters>
     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.
      */
@@ -116,6 +124,7 @@ public List<Block> execute(FootnoteMacroParameters parameters, String content, M
                 root.addChild(putFootnotesMacro);
             }
         }
-        return Collections.emptyList();
+
+        return this.contentParser.parse(content, context, true, true).getChildren();
     }
 }

programming_language: Java
filename: PutFootnotesMacro.java
change_type: ModificationType.MODIFY
diff: @@ -20,17 +20,22 @@
 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;
@@ -38,14 +43,14 @@
 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.
@@ -77,11 +82,68 @@ public class PutFootnotesMacro extends AbstractMacro<FootnoteMacroParameters>
     /** 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.
@@ -103,69 +165,121 @@ public boolean supportsInlineMode()
 
     @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;
     }
 
     /**
@@ -186,48 +300,42 @@ private void addFootnoteRef(MacroMarkerBlock footnoteMacro, Block footnoteRef)
      * 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;
     }
 }

programming_language: Java
filename: ExecutedContentMacro.java
change_type: ModificationType.ADD
diff: @@ -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();
+    }
+}

programming_language: Java
filename: components.txt
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+org.xwiki.rendering.macro.footnote.ExecutedContentMacro
\ No newline at end of file

programming_language: Batchfile
filename: macrofootnote3.test
change_type: ModificationType.MODIFY
diff: @@ -13,24 +13,37 @@ 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

programming_language: Matlab
filename: macrofootnote6.test
change_type: ModificationType.ADD
diff: @@ -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

programming_language: Matlab