Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
### 代码内容：
<?xml version="1.1" 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.
-->

<xwikidoc version="1.4" reference="XWiki.LiveTableResultsMacros" locale="">
  <web>XWiki</web>
  <name>LiveTableResultsMacros</name>
  <language/>
  <defaultLanguage/>
  <translation>0</translation>
  <creator>xwiki:XWiki.Admin</creator>
  <parent>XWiki.WebHome</parent>
  <author>xwiki:XWiki.Admin</author>
  <contentAuthor>xwiki:XWiki.Admin</contentAuthor>
  <version>1.1</version>
  <title>$services.localization.render('platform.livetable.resultsMacros')</title>
  <comment/>
  <minorEdit>false</minorEdit>
  <syntaxId>xwiki/2.0</syntaxId>
  <hidden>true</hidden>
  <content>{{velocity output="false"}}
#template('hierarchy_macros.vm')

#**
 * Macro to get the results of a livetable data call.
 * This page is called from live grids via Ajax with the argument xpage=plain. It returns a
 * set of results serialized in JSON.
 *#
#macro(gridresult $className $collist)
  #gridresultwithfilter($className $collist '' '' {})
#end

#**
 * Computes the query used to retrieve the results of a live table data call.
 * NOTE: This macro is not fully encapsulated because it defines a few Velocity variables that are used in subsequent macros.
 *#
#macro(gridresultwithfilter_buildQuery $className $collist $filterfrom $filterwhere $filterParams)
  ## Additional columns; should *not* contain raw parameters, all added column names must be filtered
  #set($fromSql = '')
  ## Parametrized filter part of the query
  #set($whereSql = '')
  ## List of values to use with $whereSql
  #if (!$filterParams)
    #set($filterParams = {})
  #end
  #if ($filterParams.entrySet())
    #set($whereParams = {})
  #else
    #set($whereParams = [])
  #end
  #set($class = $xwiki.getDocument($className).getxWikiClass())
  ##
  ## Add the columns needed for the actual data
  ##
  #set($tablelist = [])
  #foreach($colname in $collist)
    #livetable_addColumnToQuery($colname)
  #end
  ##
  ## Tag filtering
  ##
  #if($request.tag)
    #set($fromSql = "${fromSql} , BaseObject as tobject, DBStringListProperty as tagprop")
    #set($whereSql = "${whereSql} and tobject.className='XWiki.TagClass' and tobject.name=doc.fullName and tobject.id=tagprop.id.id and tagprop.id.name='tags' and (")
    #foreach($tag in $request.getParameterValues('tag'))
      #if($foreach.count > 1) #set($whereSql = "${whereSql} and ") #end
      ## Tags are case insensitive but they are stored unchanged which means we have to normalize them when performing
      ## a query. Unfortunately there's no simple way to match multiple tags (AND operator). If we join the list of
      ## tags in the FROM clause then we match at least one of the tags (OR operator). The only remaining option is to
      ## check that the list of tags contains each of the desired tags. HQL doesn't help us to lower-case the entire
      ## list of tags so we use an inner select for this.
      #if ($whereParams.entrySet())
        #set($whereSql = "${whereSql} lower(:wikitag${foreach.count}) in (select lower(tag) from tagprop.list tag)")
        #set($discard = $whereParams.put("wikitag$foreach.count", "${tag}"))
      #else
        #set($whereSql = "${whereSql} lower(?) in (select lower(tag) from tagprop.list tag)")
        #set($discard = $whereParams.add("${tag}"))
      #end
    #end
    #set($whereSql = "${whereSql})")
  #end
  ##
  ##
  ## Order
  ##
  ## if the object for the classname of the order column is not already in the from sql, put it
  #macro(addObjectClause $objectAlias)
    #if($fromSql.indexOf($objectAlias) < 0)
      #set($fromSql = "${fromSql}, BaseObject $objectAlias")
      #if ($whereParams.entrySet())
        #set($whereSql = "${whereSql} and ${objectAlias}.name = doc.fullName and ${objectAlias}.className = :${objectAlias}_className")
        #set($discard = $whereParams.put("${objectAlias}_className", $propClassName))
      #else
        #set($whereSql = "${whereSql} and ${objectAlias}.name = doc.fullName and ${objectAlias}.className = ?")
        #set($discard = $whereParams.add($propClassName))
      #end
    #end
  #end
  ## Set the order clause for a field. We first ignore the case using the lower function (so that e.g. 'aaa' equals 'AAA')
  ## but then consider it only for equal values (so that e.g. 'AAA' comes before 'aaa').
  #macro(setOrderClause $fieldName $direction $useRawValue)
    #if ($useRawValue)
      #set($orderSql = " order by ${fieldName} ${direction}")
    #else
      #set($orderSql = " order by lower(${fieldName}) ${direction}, ${fieldName} ${direction}")
    #end
  #end
  #set($order = "$!request.sort")
  #if ($order == 'doc.location')
    #set ($order = 'doc.fullName')
  #end
  #set ($orderSql = '')
  #if($order != '')
    #set($orderDirection = "$!{request.get('dir').toLowerCase()}")
    #if("$!orderDirection" != '' && "$!orderDirection" != 'asc')
      #set($orderDirection = 'desc')
    #end
    #livetable_getTableAlias($order)
    #if($order.startsWith('doc.'))
      ## The column is a document field.
      ##
      ## These document fields need to be ordered as raw values and not as strings.
      #set($rawDocumentFields = ['translation', 'date', 'contentUpdateDate', 'creationDate', 'elements', 'minorEdit1', 'hidden'])
      #set($documentField = $stringtool.removeStart($order, 'doc.'))
      #setOrderClause(${safe_tableAlias.replace('_','.')}, ${orderDirection},  $rawDocumentFields.contains($documentField))
    #else
      ## The column is an object property.
      ##
      ## Resolve the property.
      #livetable_getPropertyClassAndType($order)
      #set ($multiselect = "$!{propClass.get($order).getProperty('multiSelect').getValue()}")
      ## We can only handle single values, not multiselect ones.
      #if ($multiselect != '1')
        ## Some property types do not need lowercasing since they have unique values by design, so we use the raw values to order.
        #set($rawPropertyTypes = ['NumberClass', 'BooleanClass', 'DateClass', 'LevelsClass'])
        ## If the order column is also a filer column, this means that it was already added to the query and all we need to do is to add it to the order clause.
        #if(!$tablelist.contains($order))
          ## The order column is not also a filter column, so not yet defined in the query.
          ## We need to first define it (to the from and where clauses) before we can add it to the order clause.
          ##
          ## Resolve the table name of the property to be used in the from clause below.
          #livetable_getTableName($order)
          ## If the sort column has a _class specified, join that object in
          #set($orderObjectAlias = 'obj')
          #if($propClassName != '' && "$!propClass" != '')
            ## prepare the alias of the BaseObject table that corresponds to the class of this column
            #set($orderObjectAlias = "$!{propClassName.replaceAll('[^a-zA-Z0-9_]', '')}_obj")
            #addObjectClause($orderObjectAlias)
          #end
          #set($fromSql = "${fromSql}, ${tableName} ${safe_tableAlias}")
          ## FIXME: Check if this is indeed a property of the class. Hint: $propType can be used.
          ## Conditions are put on the object coresponding to the column of the order ($orderObjectAlias), which depends on which is the class of the $order
          #if ($whereParams.entrySet())
            #set($whereSql = "${whereSql} and ${orderObjectAlias}.id=${safe_tableAlias}.id.id and ${safe_tableAlias}.name = :${safe_tableAlias}_name")
            #set($discard = $whereParams.put("${safe_tableAlias}_name", "${order}"))
          #else
            #set($whereSql = "${whereSql} and ${orderObjectAlias}.id=${safe_tableAlias}.id.id and ${safe_tableAlias}.name = ?")
            #set($discard = $whereParams.add("${order}"))
          #end
        #end
        ## Add the column to the order clause.
        #setOrderClause("${safe_tableAlias}.value", ${orderDirection}, $rawPropertyTypes.contains($propType))
      #end
    #end
  #end
  ##
  ##
  ## Compute the final queries
  ##
  #if ($filterParams.entrySet())
    #set($sqlParams = {})
    #set($tagsMatchingParams = {})
    #set($allMatchingParams = {})
  #else
    #set($sqlParams = [])
    #set($tagsMatchingParams = [])
    #set($allMatchingParams = [])
  #end
  #if("$!className" != '')
    ## Class query
    #if ($sqlParams.entrySet())
      #set($sql = ", BaseObject as obj $!fromSql $!filterfrom where obj.name=doc.fullName and obj.className = :className and doc.fullName not in (:classTemplate1, :classTemplate2) $!whereSql $!filterwhere")
      #set($discard = $sqlParams.put('className', "${className}"))
      #set($discard = $sqlParams.put('classTemplate1', "${className}Template"))
      #set($discard = $sqlParams.put('classTemplate2', ${className.replaceAll('Class$', 'Template')}))
      #set($discard = $sqlParams.putAll($whereParams))
    #else
      #set($sql = ", BaseObject as obj $!fromSql $!filterfrom where obj.name=doc.fullName and obj.className = ? and doc.fullName not in (?, ?) $!whereSql $!filterwhere")
      #set($discard = $sqlParams.addAll(["${className}", "${className}Template", ${className.replaceAll('Class$', 'Template')}]))
      #set($discard = $sqlParams.addAll($whereParams))
    #end
    ##
    #set($tagsMatchingFiltersFrom = ", BaseObject as obj $!fromSql $!filterfrom")
    #if ($tagsMatchingParams.entrySet())
      #set($tagsMatchingFiltersWhere = "obj.name=doc.fullName and obj.className = :className and doc.fullName not in (:classTemplate1, :classTemplate2) $!whereSql $!filterwhere")
      #set($discard = $tagsMatchingParams.put('className', "${className}"))
      #set($discard = $tagsMatchingParams.put('classTemplate1', "${className}Template"))
      #set($discard = $tagsMatchingParams.put('classTemplate2', ${className.replaceAll('Class$', 'Template')}))
      #set($discard = $tagsMatchingParams.putAll($whereParams))
    #else
      #set($tagsMatchingFiltersWhere = "obj.name=doc.fullName and obj.className = ? and doc.fullName not in (?, ?) $!whereSql $!filterwhere")
      #set($discard = $tagsMatchingParams.addAll(["${className}", "${className}Template", ${className.replaceAll('Class$', 'Template')}]))
      #set($discard = $tagsMatchingParams.addAll($whereParams))
    #end
    ##
    #set($allMatchingTagsFrom = ", BaseObject as obj $!filterfrom")
    #if ($allMatchingParams.entrySet())
      #set($allMatchingTagsWhere = "obj.name=doc.fullName and obj.className = :className and doc.fullName not in (:classTemplate1, :classTemplate2) $!filterwhere")
      #set($discard = $allMatchingParams.put('className', "${className}"))
      #set($discard = $allMatchingParams.put('classTemplate1', "${className}Template"))
      #set($discard = $allMatchingParams.put('classTemplate2', ${className.replaceAll('Class$', 'Template')}))
    #else
      #set($allMatchingTagsWhere = "obj.name=doc.fullName and obj.className = ? and doc.fullName not in (?, ?) $!filterwhere")
      #set($discard = $allMatchingParams.addAll(["${className}", "${className}Template", ${className.replaceAll('Class$', 'Template')}]))
    #end
    ##
    #if($filterParams)
      #if ($filterParams.entrySet())
        #set($discard = $sqlParams.putAll($filterParams))
        #set($discard = $tagsMatchingParams.putAll($filterParams))
        #set($discard = $allMatchingParams.putAll($filterParams))
      #else
        #set($discard = $sqlParams.addAll($filterParams))
        #set($discard = $tagsMatchingParams.addAll($filterParams))
        #set($discard = $allMatchingParams.addAll($filterParams))
      #end
    #end
  #else
    ## Document query
    #set($sql = "$!fromSql $!filterfrom where 1=1 $!whereSql $!filterwhere")
    #if ($whereParams.entrySet())
      #set($discard = $sqlParams.putAll($whereParams))
    #else
      #set($discard = $sqlParams.addAll($whereParams))
    #end
    ##
    #set($tagsMatchingFiltersFrom = "$!fromSql $!filterfrom")
    #set($tagsMatchingFiltersWhere = "1=1 $!whereSql $!filterwhere")
    #if ($whereParams.entrySet())
      #set($discard = $tagsMatchingParams.putAll($whereParams))
    #else
      #set($discard = $tagsMatchingParams.addAll($whereParams))
    #end
    ##
    #set($allMatchingTagsFrom = "$!filterfrom")
    #set($allMatchingTagsWhere = "1=1 $!filterwhere")
    ##
    #if($filterParams)
      #if ($filterParams.entrySet())
        #set($discard = $sqlParams.putAll($filterParams))
        #set($discard = $tagsMatchingParams.putAll($filterParams))
        #set($discard = $allMatchingParams.putAll($filterParams))
      #else
        #set($discard = $sqlParams.addAll($filterParams))
        #set($discard = $tagsMatchingParams.addAll($filterParams))
        #set($discard = $allMatchingParams.addAll($filterParams))
      #end
    #end
  #end
  #if($orderSql != '')
    #set($sql = "$sql $!{orderSql}")
  #end
#end
#**
 * Adds TagCloud information to the JSON returned by a live table data call.
 * NOTE: This macro uses Velocity variables defined by gridresultwithfilter_buildQuery.
 *
 * @param $map stores the JSON in memory so that it can be adjusted before serialization
 *#
#macro(gridresult_buildTagCloudJSON $map)
  ##
  ## TagCloud matching the current filters
  ##
  #set($tagsMatchingFilters = $xwiki.tag.getTagCountForQuery($tagsMatchingFiltersFrom, $tagsMatchingFiltersWhere, $tagsMatchingParams))
  ## FIXME: We use a map just because the client expects an object, but all we really need is a list..
  #set($matchingTags = {})
  #foreach($tag in $tagsMatchingFilters.keySet())
    ## NOTE: The value doesn't have a special meaning. I've used 1 just because it takes less space when serialized.
    #set($discard = $matchingTags.put($tag, 1))
  #end
  #set($discard = $map.put('matchingtags', $matchingTags))
  ##
  ## TagCloud matching all the documents used by the live table
  ##
  #set($allMatchingTags = $xwiki.tag.getTagCountForQuery($allMatchingTagsFrom, $allMatchingTagsWhere, $allMatchingParams))
  ## FIXME: We use a list of maps just because the client expects an array, but we should simply return $allMatchingTags..
  #set($tags = [])
  #foreach($tag in $allMatchingTags.keySet())
    #set($discard = $tags.add({'tag': $tag, 'count': $allMatchingTags.get($tag)}))
  #end
  #set($discard = $map.put('tags', $tags))
#end

#**
 * Adds information about each live table row to the JSON returned by a live table data call.
 * NOTE: This macro uses Velocity variables defined by gridresultwithfilter_buildQuery.
 *
 * @param $map stores the JSON in memory so that it can be adjusted before serialization
 *#
#macro(gridresult_buildRowsJSON $map)
  #set($offset = $numbertool.toNumber($request.get('offset')).intValue())
  ## Offset starts from 0 in velocity and 1 in javascript
  #set($offset = $offset - 1)
  #if(!$offset || $offset < 0)
    #set($offset = 0)
  #end
  #set($limit = $numbertool.toNumber($request.get('limit')).intValue())
  #if(!$limit)
    #set ($limit = 15)
  #end
  #set($query = $services.query.hql($sql))
  ## Apply query filters if defined. Otherwise use default.
  #foreach ($queryFilter in $stringtool.split($!request.queryFilters, ', '))
    #set ($query = $query.addFilter($queryFilter))
  #end
  #set ($query = $query.setLimit($limit).setOffset($offset).bindValues($sqlParams))
  #set($items = $query.execute())
  #set($discard = $map.put('totalrows', $query.count()))
  #set($discard = $map.put('returnedrows', $mathtool.min($items.size(), $limit)))
  #set($discard = $map.put('offset', $mathtool.add($offset, 1)))
  #set($rows = [])
  #foreach($item in $items)
    #gridresult_buildRowJSON($item $rows)
  #end
  #set ($discard = $map.put('rows', $rows))
  #livetable_filterObfuscated($map)
#end

#**
 * Adds information about the specified live table row to the JSON returned by a live table data call.
 * NOTE: This macro uses Velocity variables available in gridresult_buildRowsJSON.
 *
 * @param $item the name of the document that feeds this live table row
 * @param $rows stores the JSON in memory so that it can be adjusted before serialization
 *#
#macro(gridresult_buildRowJSON $item $rows)
  ## Handle both the case where the "language" filter is used and thus languages are returned too and the case where
  ## only the document name is returned. When more than the document name is returned the $item variable is a list.
  #if($item.size())
    ## Extract doc name and doc language from $item
    #set($docName = $item[0])
    #set($docLanguage = $item[1])
  #else
    #set($docName = $item)
    #set($docLanguage = '')
  #end
  #set ($docReference = $services.model.resolveDocument($docName))
  #set ($isViewable = $services.security.authorization.hasAccess('view', $docReference))
  #if ($isViewable)
    #set ($row = {
      'doc_viewable': $isViewable,
      'doc_fullName': $services.model.serialize($docReference, 'local'),
      'doc_space': $services.model.serialize($docReference.parent, 'local'),
      'doc_location': "#hierarchy($docReference, {'limit': 5, 'plain': false, 'local': true, 'displayTitle': false})",
      'doc_url': $xwiki.getURL($docReference),
      'doc_space_url': $xwiki.getURL($docReference.parent),
      'doc_wiki': $docReference.wikiReference.name,
      'doc_wiki_url': $xwiki.getURL($docReference.wikiReference),
      'doc_hasadmin': $xwiki.hasAdminRights(),
      'doc_hasedit': $services.security.authorization.hasAccess('edit', $docReference),
      'doc_hasdelete': $services.security.authorization.hasAccess('delete', $docReference),
      'doc_edit_url': $xwiki.getURL($docReference, 'edit'),
      'doc_copy_url': $xwiki.getURL($docReference, 'view', 'xpage=copy'),
      'doc_delete_url': $xwiki.getURL($docReference, 'delete'),
      'doc_rename_url': $xwiki.getURL($docReference, 'view', 'xpage=rename&step=1')
    })
    #set ($isTranslation = "$!docLanguage" != '' && $xwiki.getLanguagePreference() != $docLanguage)
    ## Display the language after the document name so that not all translated documents have the same name displayed.
    #set ($row.doc_name = "$docReference.name#if ($isTranslation) ($docLanguage)#end")
    #set ($row.doc_hascopy = $row.doc_viewable)
    #set ($row.doc_hasrename = $row.doc_hasdelete)
    #set ($row.doc_hasrights = $row.doc_hasedit && $isAdvancedUser)
    #if ($docReference.name == 'WebHome')

      ## For nested pages, use the page administration.
      #set ($webPreferencesReference = $services.model.createDocumentReference(
        'WebPreferences', $docReference.lastSpaceReference))
      #set ($row.doc_rights_url = $xwiki.getURL($webPreferencesReference, 'admin',
        'editor=spaceadmin&section=PageRights'))
    #else
      ## For terminal pages, use the old rights editor.
      ## TODO: We should create a page administration for terminal pages too.
      #set ($row.doc_rights_url = $xwiki.getURL($docReference, 'edit', 'editor=rights'))
    #end
    #if ($row.doc_viewable)
      #set ($itemDoc = $xwiki.getDocument($docReference))
      ## Handle translations. We need to make sure we display the data associated to the correct document if the returned
      ## result is a translation.
      #if ($isTranslation)
        #set ($translatedDoc = $itemDoc.getTranslatedDocument($docLanguage))
      #else
        #set ($translatedDoc = $itemDoc.translatedDocument)
      #end
      #set($discard = $itemDoc.use($className))
      #set($discard = $row.put('doc_objectCount', $itemDoc.getObjectNumbers($className)))
      #set($discard = $row.put('doc_edit_url', $itemDoc.getURL($itemDoc.defaultEditMode)))
      #set($discard = $row.put('doc_author_url', $xwiki.getURL($translatedDoc.author)))
      #set($discard = $row.put('doc_date', $xwiki.formatDate($translatedDoc.date)))
      #set($discard = $row.put('doc_title', $translatedDoc.plainTitle))
      #set($rawTitle = $translatedDoc.title)
      #if($rawTitle != $row['doc_title'])
        #set($discard = $row.put('doc_title_raw', $rawTitle))
      #end
      #set($discard = $row.put('doc_author', $xwiki.getPlainUserName($translatedDoc.authorReference)))
      #set($discard = $row.put('doc_creationDate', $xwiki.formatDate($translatedDoc.creationDate)))
      #set($discard = $row.put('doc_creator', $xwiki.getPlainUserName($translatedDoc.creatorReference)))
      #set($discard = $row.put('doc_hidden', $translatedDoc.isHidden()))
      #foreach($colname in $collist)
        #gridresult_buildColumnJSON($colname $row)
      #end
    #end
  #else
    #set ($row = {
      'doc_viewable': $isViewable,
      'doc_fullName': 'obfuscated'
    })
  #end
  #set($discard = $rows.add($row))
#end

#**
 * Adds information about the given column to the JSON returned by a live table data call.
 * NOTE: This macro uses Velocity variables available in gridresult_buildRowJSON.
 *
 * @param $colname the name of the live table column for which to retrieve information
 * @param $row stores the JSON in memory so that it can be adjusted before serialization
 *#
#macro(gridresult_buildColumnJSON $colname $row)
  #if($colname.startsWith('doc.'))
  #elseif($colname == '_action')
    #set($discard = $row.put($colname, $services.localization.render("${request.transprefix}actiontext")))
  #elseif($colname == '_attachments')
    #livetable_getAttachmentsList($translatedDoc)
    #set($discard = $row.put($colname, $attachlist))
  #elseif($colname == '_avatar')
    #livetable_getAvatar($itemDoc)
    #set($discard = $row.put($colname, $avatar))
  #elseif($colname == '_images')
    #livetable_getImagesList($itemDoc)
    #set($discard = $row.put($colname, $imagesList))
  ## Output likes if they are available.
  #elseif($colname == '_likes' && "$!services.like" != "")
    #set($likes = $services.like.getLikes($docReference))
    #if ($likes.isPresent())
      #set($discard = $row.put('_likes', $likes.get()))
    #end
  #else
    #livetable_getPropertyClassAndType($colname)
    #if(!$propClass.equals($class))
      #set($discard = $itemDoc.use($propClassName))
    #end
    #set($fieldObject = $itemDoc.getFirstObject($colname))
    #set($fieldProperty = $fieldObject.getProperty($colname))
    #if ($fieldProperty.getPropertyClass().classType == 'Password')
      #set($fieldValue = '********')
    #else
      #set($fieldValue = "$!fieldProperty.getValue()")
    #end
    #set($fieldDisplayValue = "$!itemDoc.display($colname, 'view')")
    #if($fieldDisplayValue == '')
      #set($fieldDisplayValue = $services.localization.render("${request.transprefix}emptyvalue"))
    #end
    #set($fieldUrl = '')
    ## Only retrieve an URL for a DBListClass item
    #if(($propType == 'DBListClass' || $propType == 'PageClass') && $propClass.get($colname).getProperty('multiSelect').value != 1)
      #set($fieldUrl = $xwiki.getURL($fieldValue))
      #if($fieldUrl == $xwiki.getURL($services.model.resolveDocument('', 'default', $doc.documentReference.extractReference('WIKI'))))
        #set($fieldUrl = '')
      #end
    #end
    #set($discard = $row.put($colname, $fieldDisplayValue.replaceFirst($regextool.quote('{{html clean="false" wiki="false"}}'), '').replaceAll("$regextool.quote('{{/html}}')$", '')))
    #set($discard = $row.put("${colname}_value", $fieldValue))
    #set($discard = $row.put("${colname}_url", $fieldUrl))
    ## Reset to the default class
    #set($discard = $itemDoc.use($className))
  #end
#end

#**
 * Builds the JSON response to a live table data call.
 *
 * @param $map stores the JSON in memory so that it can be adjusted before serialization
 *#
#macro(gridresultwithfilter_buildJSON $className $collist $filterfrom $filterwhere $filterParams $map)
  #gridresultwithfilter_buildQuery($className $collist $filterfrom $filterwhere $filterParams)
  #if("$!request.sql" == '1')
    #set($discard = $map.put('sql', $sql))
    #set($discard = $map.put('params', $sqlParams))
  #end
  #set($discard = $map.put('reqNo', $numbertool.toNumber($request.reqNo).intValue()))
  #gridresult_buildTagCloudJSON($map)
  #gridresult_buildRowsJSON($map)
#end

#**
 * Builds the JSON response to a live table data call.
 *
 * @param $map stores the JSON in memory so that it can be adjusted before serialization
 *#
#macro(gridresult_buildJSON $className $collist $map)
  #gridresultwithfilter_buildJSON($className $collist '' '' {} $map)
#end

#**
 * Macro to get the results of a livetable data call.
 * This page is called from live grids via Ajax with the argument xpage=plain. It returns a
 * set of results serialized in JSON.
 *#
#macro(gridresultwithfilter $className $collist $filterfrom $filterwhere $filterParams)
  #if($xcontext.action == 'get' && "$!{request.outputSyntax}" == 'plain')
    ## Build the JSON in memory (using basic Java data types) so that it can be adjusted before serialization.
    #set($map = {})
    #gridresultwithfilter_buildJSON($className $collist $filterfrom $filterwhere $filterParams $map)
    #jsonResponse($map)
  #end
#end

#**
 * Get the name of the Property that should be used for a given livetable column.
 * NOTE the resulting $tableName is safe to use inside SQL queries
 *#
#macro(livetable_getTableName $colname)
  #livetable_getPropertyClassAndType($colname)
  #if($propType == 'NumberClass')
    #set($numberType = $propClass.get($colname).getProperty('numberType').getValue())
    #if($numberType == 'integer')
      #set($tableName = 'IntegerProperty')
    #elseif($numberType == 'float')
      #set($tableName = 'FloatProperty')
    #elseif($numberType == 'double')
      #set($tableName = 'DoubleProperty')
    #else
      #set($tableName = 'LongProperty')
    #end
  #elseif($propType == 'BooleanClass')
    #set($tableName = 'IntegerProperty')
  #elseif($propType == 'DateClass')
    #set($tableName = 'DateProperty')
  #elseif($propType == 'TextAreaClass' || $propType == 'UsersClass' || $propType == 'GroupsClass')
    #set($tableName = 'LargeStringProperty')
  #elseif($propType == 'StaticListClass' || $propType == 'DBListClass' || $propType == 'DBTreeListClass' || $propType == 'PageClass')
    #set($multiSelect = $propClass.get($colname).getProperty('multiSelect').getValue())
    #set($relationalStorage = $propClass.get($colname).getProperty('relationalStorage').getValue())
    #if($multiSelect == 1)
      #if($relationalStorage == 1)
        #set($tableName = 'DBStringListProperty')
      #else
        #set($tableName = 'StringListProperty')
      #end
    #else
      #set($tableName = 'StringProperty')
    #end
  #else
    #set($tableName = 'StringProperty')
  #end
#end

#**
 * Get the property class and type for a given livetable column.
 *#
#macro(livetable_getPropertyClassAndType $colname)
  #set($propClassName = "$!request.get(${colname.concat('_class')})")
  #if($propClassName != '')
    #set($propClass = $xwiki.getDocument($propClassName).getxWikiClass())
  #else
    #set($propClass = $class)
  #end
  #set($propType = '')
  #if($propClass.getPropertyNames().contains($colname))
    #set($propType = "$!{propClass.get($colname).type}")
  #end
#end

#**
 * Old alias of the #livetable_getTableName macro.
 * @deprecated since 2.2.3, use {@link #livetable_getTableName}
 *#
#macro(grid_gettablename $colname)
  #livetable_getTableName($colname)
#end

#**
 * List attachments for a document, putting the result as HTML markup in the $attachlist variable.
 *#
#macro(livetable_getAttachmentsList $itemDoc)
  #set($attachlist = '')
  #foreach($attachment in $itemDoc.attachmentList)
    #set($attachmentUrl = $itemDoc.getAttachmentURL($attachment.filename))
    #set($attachlist = "${attachlist}<a href='${attachmentUrl}'>$attachment.filename</a><br/>")
  #end
#end

#**
 * Old alias of the #livetable_getAttachmentsList macro.
 * @deprecated since 2.2.3, use {@link #livetable_getAttachmentsList}
 *#
#macro(grid_attachlist $itemDoc)
  #livetable_getAttachmentsList($itemDoc)
#end

#**
 * List image attachments for a document, putting the result as HTML markup in the $imagesList variable.
 *#
#macro(livetable_getImagesList $itemDoc)
  #set($imagesList = '')
  #foreach ($attachment in $itemDoc.attachmentList)
    #if($attachment.isImage())
      ## Create a thumbnail by resizing the image on the server side, if needed, to fit inside a 50x50 pixel square.
      #set($thumbnailURL = $itemDoc.getAttachmentURL($attachment.filename, 'download', "width=50&height=50&keepAspectRatio=true"))
      #set($imageURL = $itemDoc.getAttachmentURL($attachment.filename))
      #set($imagesList = "${imagesList}<a href=""$imageURL""><img src=""$thumbnailURL"" alt=""$attachment.filename"" title=""$attachment.filename"" /></a>")
    #end
  #end
#end

#**
 * Old alias of the #livetable_getImagesList macro.
 * @deprecated since 2.2.3, use {@link #livetable_getImagesList}
 *#
#macro(grid_photolist $itemDoc)
  #livetable_getImagesList($itemDoc)
#end

#**
 * Generate the HTML code for a user avatar.
 *#
#macro(livetable_getAvatar $itemDoc)
  #set ($avatar = "#mediumUserAvatar($itemDoc.fullName)")
  #set ($avatar = $avatar.trim())
#end

#**
 * Old alias of the #livetable_getAvatar macro.
 * @deprecated since 2.2.3, use {@link #livetable_getAvatar}
 *#
#macro(grid_avatar $itemDoc)
  #livetable_getAvatar($itemDoc)
#end

#**
 * Macro to extend the query to select the properties for the livetable columns.
 * NOTE $colName is filtered (all characters but [a-zA-Z0-9_.] are removed) before use
 *#
#macro (livetable_addColumnToQuery $colName)
  ## Safe because / is not allowed in property names
  ## The $joinModeMarker is used in #livetable_filterDBStringListProperty.
  #set ($joinModeMarker = "/join_mode")
  #if (!$colName.endsWith($joinModeMarker))
    #set ($filterValue = "$!request.getParameter($colName)")
    #if ("$!filterValue" != '')
      #set ($discard = $tablelist.add($colName))
      ## Some columns may support filtering with multiple constraints (multiple filter values).
      #set ($filterValues = $request.getParameterValues($colName))
      #if ($colName.startsWith('doc.'))
        #if ($colName == 'doc.location')
          #set ($safeColName = 'doc.fullName')
          ## Use filterLocation since addLivetableLocationFilter is buggy when called several times (it'll add the
          ## same HQL binding name every time it's called! See https://jira.xwiki.org/browse/XWIKI-17463).
          ## Also note that we don't call addLocationFilter since we use a Map for $params.
          #filterLocation($whereSql, $whereParams, $filterValue, 'locationFilterValue2', true)
        #elseif ($colName == 'doc.date' || $colName == 'doc.creationDate' || $colName == 'doc.contentUpdateDate')
          #livetable_getTableAlias($colName)
          #livetable_filterDateProperty()
        #else
          #set ($safeColName = $colName.replaceAll('[^a-zA-Z0-9_.]', '').replace('_', '.'))
          #if ($whereParams.entrySet())
            #set ($whereSql = "${whereSql} and upper(str($safeColName)) like upper(:${safeColName.replace('.', '_')}_filter)")
            #set ($discard = $whereParams.put("${safeColName.replace('.', '_')}_filter", "%$filterValue%"))
          #else
            #set ($whereSql = "${whereSql} and upper(str($safeColName)) like upper(?)")
            #set ($discard = $whereParams.add("%$filterValue%"))
          #end
        #end
      #else
        #livetable_filterProperty($colName)
      #end
    #end
  #end
#end

#**
 * Determine how the filter values should be matched against the stored values. This macro sets two variables:
 * <ul>
 *   <li>$matchType: use this when the specified column supports only a single filter value</li>
 *   <li>$matchTypes: use this when the specified column supports multiple filter values.</li>
 * </ul>
 *
 * @param column the column name; each column can have a different match type
 * @param filterValueCount the number of filter values for which to determine the match type; each filter value can have
 *          a different match type
 * @param defaultMatchType the default match type to use for the given column when the request doesn't specify one
 *#
#macro (livetable_getMatchTypes $column $filterValueCount $defaultMatchType)
  #set ($macro.matchTypes = $request.getParameterValues("${column}_match"))
  #if (!$macro.matchTypes || $macro.matchTypes.isEmpty())
    ## No match type specified for this column.
    #set ($matchType = $defaultMatchType)
    #set ($matchTypes = $stringtool.repeat($matchType, ',', $filterValueCount).split(','))
  #else
    ## At least one match type specified for this column.
    #set ($matchType = $macro.matchTypes.get(0))
    #set ($matchTypes = [])
    #set ($discard = $matchTypes.addAll($macro.matchTypes.subList(0, $mathtool.min($macro.matchTypes.size(),
      $filterValueCount))))
    #if ($matchTypes.size() < $filterValueCount)
      ## Add missing match types.
      #set ($discard = $matchTypes.addAll($stringtool.repeat($matchType, ',', $mathtool.sub($filterValueCount,
        $matchTypes.size())).split(',')))
    #end
  #end
#end

#macro (livetable_filterProperty $colname)
  #livetable_getTableAlias($colname)
  #livetable_getTableName($colname)
  #set ($fromSql = "$fromSql, $tableName as $safe_tableAlias")
  ##
  ## If the column is not from $class, we need to make sure we join with the proper table.
  #set ($filterObjectAlias = 'obj')
  #set ($propClass = $class)
  #set ($propClassName = $request.getParameter("${colname}_class"))
  #if ("$!propClassName" != '')
    #set ($propClass = $xwiki.getDocument($propClassName).getxWikiClass())
    #if ("$!propClass" != '')
      ## Prepare the alias of the BaseObject table that corresponds to the class of this column
      ## Property table is to be joined with its object, determined depending on $propClassName.
      #set ($filterObjectAlias = "$!{propClassName.replaceAll('[^a-zA-Z0-9_]', '')}_obj")
      #addObjectClause($filterObjectAlias)
    #end
  #end
  #if ($whereParams.entrySet())
    #set ($joinObjectTable = "${filterObjectAlias}.id = ${safe_tableAlias}.id.id and ${safe_tableAlias}.id.name = :${safe_tableAlias}_id_name")
    #set ($discard = $whereParams.put("${safe_tableAlias}_id_name", $colname))
  #else
    #set ($joinObjectTable = "${filterObjectAlias}.id = ${safe_tableAlias}.id.id and ${safe_tableAlias}.id.name = ?")
    #set ($discard = $whereParams.add($colname))
  #end
  #set ($whereSql = "$whereSql and $joinObjectTable")
  ##
  ## We determine the default match type (when not specified) based on the property meta class (e.g. DateClass).
  #set ($propMetaClass = $NULL)
  #if ($propClass && $propClass.getPropertyNames().contains($colname))
    #set ($propMetaClass = $propClass.get($colname).type)
  #end
  ##
  #set ($numberProperties = ['IntegerProperty', 'LongProperty', 'FloatProperty', 'DoubleProperty'])
  #if ($numberProperties.contains($tableName))
    #livetable_filterNumberProperty()
  #elseif ($tableName == 'DateProperty')
    #livetable_filterDateProperty()
  #elseif ($tableName == 'DBStringListProperty')
    #livetable_filterDBStringListProperty()
  #elseif ($tableName == 'StringListProperty')
    #livetable_filterStringListProperty()
  #else
    ## StringProperty or LargeStringProperty
    #livetable_filterStringProperty()
  #end
#end

#**
 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
 *#
#macro (livetable_filterNumberProperty)
  #set($numberValue = $numbertool.toNumber($filterValue))
  #if($tableName == 'IntegerProperty' || $tableName == 'LongProperty')
    #if($tableName == 'LongProperty')
      #set($numberValue = $numberValue.longValue())
    #else
      ## IntegerProperty
      #set($numberValue = $numberValue.intValue())
    #end
    #if ($whereParams.entrySet())
      #set($whereSql = "${whereSql} and ${safe_tableAlias}.value = :${safe_tableAlias}_value")
      #set($discard = $whereParams.put("${safe_tableAlias}_value", $numberValue))
    #else
      #set($whereSql = "${whereSql} and ${safe_tableAlias}.value = ?")
      #set($discard = $whereParams.add($numberValue))
    #end
  #else
    #if($tableName == 'FloatProperty')
      #set($numberValue = $numberValue.floatValue())
    #else
      ## DoubleProperty
      #set($numberValue = $numberValue.doubleValue())
    #end
    #set($precision = 0.000001)
    #if ($whereParams.entrySet())
      #set($whereSql = "${whereSql} and abs(:${safe_tableAlias}_value - ${safe_tableAlias}.value) <= ${precision}")
      #set($discard = $whereParams.put("${safe_tableAlias}_value", $numberValue))
    #else
      #set($whereSql = "${whereSql} and abs(? - ${safe_tableAlias}.value) <= ${precision}")
      #set($discard = $whereParams.add($numberValue))
    #end
  #end
#end

#**
 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
 *#
#macro (livetable_filterDateProperty)
  #if ($safe_tableAlias.startsWith('doc.'))
    #set ($dateProperty = $safe_tableAlias)
  #else
    #set ($dateProperty = "${safe_tableAlias}.value")
  #end
  #set ($safeDateProperty = $dateProperty.replace('.', '_'))
  #set ($dateRange = {})
  ## Perform partial string matching by default if no match type is specified.
  ## Note that for the moment we support only one filter value (e.g. one date range) and thus only the first match type
  ## is taken into account.
  #livetable_getMatchTypes($colname $filterValues.size() 'partial')
  #parseDateRange($matchType $filterValue $dateRange)
  #if ($dateRange.start || $dateRange.end)
    ## Date range.
    #if ($dateRange.start)
      #if ($whereParams.entrySet())
        #set ($whereSql = "${whereSql} and $dateProperty >= :${safeDateProperty}1")
        #set ($discard = $whereParams.put("${safeDateProperty}1", $dateRange.start))
      #else
        #set ($whereSql = "${whereSql} and $dateProperty >= ?")
        #set ($discard = $whereParams.add($dateRange.start))
      #end
    #end
    #if ($dateRange.end)
      #if ($whereParams.entrySet())
        #set ($whereSql = "${whereSql} and $dateProperty <= :${safeDateProperty}2")
        #set ($discard = $whereParams.put("${safeDateProperty}2", $dateRange.end))
      #else
        #set ($whereSql = "${whereSql} and $dateProperty <= ?")
        #set ($discard = $whereParams.add($dateRange.end))
      #end
    #end
  #else
    ## String matching (contains).
    #if ($whereParams.entrySet())
      #set ($whereSql = "${whereSql} and upper(str($dateProperty)) like upper(:$safeDateProperty)")
      #set ($discard = $whereParams.put($safeDateProperty, "%$filterValue%"))
    #else
      #set ($whereSql = "${whereSql} and upper(str($dateProperty)) like upper(?)")
      #set ($discard = $whereParams.add("%$filterValue%"))
    #end
  #end
#end

#**
 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
 *#
#macro (livetable_filterDBStringListProperty)
  ## Perform exact matching by default if no match type is specified.
  ## Note that for DBStringList properties we take into account only the first match type, even if multiple filter
  ## values are specified. Basically the first match type is used for all filter values.
  #livetable_getMatchTypes($colname $filterValues.size() 'exact')
  #if ($matchType == 'partial' || $matchType == 'prefix')
    ## We need to join with the list of values in order to be able to use the LIKE operator.
    #set ($matchTarget = "${safe_tableAlias}_item")
    #if ($whereParams.entrySet())
      #set ($paramPrefix = "${safe_tableAlias}_item_")
    #else
      #set ($paramPrefix = $NULL)
    #end
    #set ($joinPos = $mathtool.add($fromSql.lastIndexOf(" $safe_tableAlias"), $mathtool.add($safe_tableAlias.length(), 1)))
    #set ($fromSql = "$fromSql.substring(0, $joinPos) join ${safe_tableAlias}.list as $matchTarget $fromSql.substring($joinPos)")
  #else
    ## Fall-back on exact matching even if the match type is specified, when its value is not supported.
    #set ($matchType = 'exact')
    #set ($matchTarget = "${safe_tableAlias}.list")
    #if ($whereParams.entrySet())
      #set ($paramPrefix = "${safe_tableAlias}_list_")
    #else
      #set ($paramPrefix = $NULL)
    #end
  #end
  #set ($filterQuery = "#livetable_getFilterQuery($matchTarget $matchType true $filterValues.size() $paramPrefix $NULL)")
  #set ($whereSql = "$whereSql and ($filterQuery.trim())")
  #foreach ($filterValue in $filterValues)
    #livetable_addFilterParam($filterValue $matchType $whereParams "${paramPrefix}${foreach.count}")
  #end
#end

#**
 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
 *#
#macro (livetable_filterStringListProperty)
  ## From the user point of view we support only exact matching for StringList properties, due to the way the values of
  ## these properties are stored (concatenated). But when building the actual query, the match type is in fact partial
  ## because we have to use the like operator in order to match the concatenated list of values.
  #livetable_getMatchTypes($colname $filterValues.size() 'exact')
  #set ($matchTarget = "concat('|', concat(${safe_tableAlias}.textValue, '|'))")
  #if ($whereParams.entrySet())
    #set ($paramPrefix = "${safe_tableAlias}_textValue_")
  #else
    #set ($paramPrefix = $NULL)
  #end
  ## As noted above, we have to use the like operator because the list of values is saved concatenated, so from the
  ## point of view of the query the match type is always partial.
  #set ($filterQuery = "#livetable_getFilterQuery($matchTarget 'partial' false $filterValues.size() $paramPrefix $NULL)")
  #set ($whereSql = "${whereSql} and ($filterQuery.trim())")
  #foreach ($filterValue in $filterValues)
    #if ($matchTypes.get($foreach.index) == 'empty')
      ## The client side cannot pass an empty filter value so it specifies that the value is empty using the match type.
      #set ($filterValue = '')
    #end
    ## As noted above, we can only perform exact matching due to the way the values are stored (concatenated).
    #livetable_addFilterParam("%|$filterValue|%" 'exact' $whereParams "${paramPrefix}${foreach.count}")
  #end
#end

#**
 * NOTE: This macro uses variables defined in livetable_filterProperty . It was not meant to be used alone.
 *#
#macro (livetable_filterStringProperty)
  #if ($propMetaClass.endsWith('ListClass'))
    ## Perform exact matching by default for StaticListClass, DBListClass and DBTreeListClass
    ## when they are stored as StringProperty (i.e. single value and no relational storage).
    #set ($defaultStringMatchType = 'exact')
  #else
    ## Perform partial matching by default otherwise.
    #set ($defaultStringMatchType = 'partial')
  #end
  #livetable_getMatchTypes($colname $filterValues.size() $defaultStringMatchType)
  ## Group the filter values by match type so that we cann optimize the query.
  #livetable_groupFilterValuesByMatchType($matchTypes $filterValues)
  #if ($whereParams.entrySet())
    #set ($paramPrefix = "${safe_tableAlias}_value_")
  #else
    #set ($paramPrefix = $NULL)
  #end
  ## Note that unlike other property types, the String property supports different match types for different filter
  ## values. This means we have to call livetable_getFilterQuery for each filter value and then join the constraints
  ## ourselves.
  #set ($constraints = [])
  #set ($paramOffset = 1)
  #foreach ($entry in $filterValuesByMatchType.entrySet())
    #set ($matchType = $entry.key)
    #set ($filterValues = $entry.value)
    #set ($constraint = "#livetable_getFilterQuery(""${safe_tableAlias}.value"" $matchType false $filterValues.size() $paramPrefix $paramOffset)")
    #set ($discard = $constraints.add($constraint.trim()))
    #foreach ($filterValue in $filterValues)
      #livetable_addFilterParam($filterValue $matchType $whereParams
        "${paramPrefix}${mathtool.add($paramOffset, $foreach.index)}")
    #end
    #set ($paramOffset = $paramOffset + $filterValues.size())
  #end
  #set ($whereSql = "${whereSql} and ($stringtool.join($constraints, "" $joinOperator ""))")
#end

#macro (livetable_groupFilterValuesByMatchType $matchTypes $filterValues)
  #set ($filterValuesByMatchType = {})
  #foreach ($matchType in $matchTypes)
    #set ($discard = $filterValuesByMatchType.putIfAbsent($matchType, []))
    #set ($discard = $filterValuesByMatchType.get($matchType).add($filterValues.get($foreach.index)))
  #end
#end

#macro (livetable_getJoinOperator $colName)
  #set ($joinOperator = "$!{request.get(""${colName}${joinModeMarker}"").toUpperCase()}")
  #if ($joinOperator != 'AND' && $joinOperator != 'OR')
    #set ($joinOperator = 'AND')
  #end
#end

#macro (livetable_getFilterQuery $column $matchType $isList $valueCount $paramPrefix $paramOffset)
  #livetable_getJoinOperator($colname)
  #if (!$paramOffset)
    #set ($paramOffset = 1)
  #end
  #if ($matchType == 'partial' || $matchType == 'prefix')
    #livetable_repeatParams("upper($column) like upper(?)", " $joinOperator ", $valueCount, $paramPrefix, $paramOffset)
  #elseif($matchType == 'empty')
    ## Check if the value of the column is like the empty parameter (which is often the empty string), or if the value
    ## of the column is null (to be compliant with Oracle which stores the empty string as a NULL value).
    #livetable_repeatParams("($column like ? or $column is null)", " $joinOperator ", $valueCount, $paramPrefix,
    $paramOffset)
  #elseif ($isList)
    #livetable_repeatParams("? in elements($column)", " $joinOperator ", $valueCount, $paramPrefix, $paramOffset)
  #elseif ($valueCount > 1 && $joinOperator == 'OR')
    $column in (#livetable_repeatParams('?', ', ', $valueCount, $paramPrefix, $paramOffset))
  #else
    #livetable_repeatParams("$column = ?", ' AND ', $valueCount, $paramPrefix, $paramOffset)
  #end
#end

#macro (livetable_repeatParams $str $separator $valueCount $paramPrefix $paramOffset)
  #if ($valueCount > 0)
    #foreach ($count in [1..$valueCount])
      #if ($count > 1)
        $separator##
      #end
      #if ($paramPrefix)
        $str.replace('?', ":${paramPrefix}${mathtool.add($paramOffset, $foreach.index)}")##
      #else
        $str##
      #end
    #end
  #end
#end

#macro (livetable_addFilterParam $filterValue $matchType $params $paramName)
  #if ($matchType == 'partial')
    #if ($params.entrySet())
      #set ($discard = $params.put($paramName, "%$!filterValue%"))
    #else
      #set ($discard = $params.add("%$!filterValue%"))
    #end
  #elseif ($matchType == 'prefix')
    #if ($params.entrySet())
      #set ($discard = $params.put($paramName, "$!filterValue%"))
    #else
      #set ($discard = $params.add("$!filterValue%"))
    #end
  #elseif ($matchType == 'empty')
    #if ($params.entrySet())
      #set ($discard = $params.put($paramName, ''))
    #else
      #set ($discard = $params.add(''))
    #end
  #else
    #if ($params.entrySet())
      #set ($discard = $params.put($paramName, $filterValue))
    #else
      #set ($discard = $params.add($filterValue))
    #end
  #end
#end

#**
 * Old alias of the #livetable_addColumnToQuery macro.
 * @deprecated since 2.2.3, use {@link #livetable_addColumnToQuery}
 *#
#macro(grid_addcolumn $colname)
  #livetable_addColumnToQuery($colname)
#end

#**
 * Generates a valid SQL table alias for the specified live table column.
 *#
#macro (livetable_getTableAlias $columnName)
  #set ($prefix = 'doc.')
  #if ($columnName.startsWith($prefix))
    #set ($suffix = $stringtool.removeStart($columnName, $prefix))
  #else
    ## Force a prefix to avoid the cases when the column name is a reserved SQL keyword.
    #set ($prefix = 'prop_')
    #set ($suffix = $columnName)
  #end
  ## Remove non-word characters.
  #set ($safe_tableAlias = "$prefix$suffix.replaceAll('\W', '')")
#end
{{/velocity}}</content>
</xwikidoc>

Output:
【漏洞类型】输入验证漏洞
【严重性】Medium
【漏洞摘要】此代码中存在输入验证漏洞，具体表现为未验证用户输入的 `limit` 参数。当用户通过 `$request.limit` 传递的值为0时，代码没有进行检查，直接使用该值来计算返回的行数。虽然这种情况下不会导致严重的安全问题，如路径遍历或SQL注入，但可能会导致应用程序返回大量不必要的数据，增加服务器负担，甚至潜在的拒绝服务（DoS）风险。
【关键代码模式】与漏洞相关的代码模式包括：
- `$limit = $numbertool.toNumber($request.get('limit')).intValue()`
- `#if(!$limit) #set ($limit = 15) #end`
- `#set($discard = $map.put('returnedrows', $mathtool.min($items.size(), $limit)))`
【修复方法说明】修复方法通过检查 `limit` 参数是否大于0来解决问题，确保不会在 `limit` 为0时返回所有行。最佳实践是始终验证用户输入的参数，以防止潜在的问题。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-menu/xwiki-platform-menu-ui/src/main/resources/Menu/MenuMacro.xml
### 代码内容：
<?xml version="1.1" encoding="UTF-8"?>

<xwikidoc version="1.3" reference="Menu.MenuMacro" locale="">
  <web>Menu</web>
  <name>MenuMacro</name>
  <language/>
  <defaultLanguage/>
  <translation>0</translation>
  <creator>xwiki:XWiki.Admin</creator>
  <parent>Menu.WebHome</parent>
  <author>xwiki:XWiki.Admin</author>
  <contentAuthor>xwiki:XWiki.Admin</contentAuthor>
  <version>1.1</version>
  <title>Menu Macro</title>
  <comment/>
  <minorEdit>false</minorEdit>
  <syntaxId>xwiki/2.1</syntaxId>
  <hidden>true</hidden>
  <content>{{toc/}}

= Horizontal Menu =

{{velocity}}
{{code language="none"}}
{{menu type="horizontal fixedWidth"}}
$xwiki.getDocument('MenuTemplate').content
{{/menu}}
{{/code}}
{{/velocity}}

{{menu type="horizontal fixedWidth"}}
{{include reference="MenuTemplate" /}}
{{/menu}}

= Vertical Menu =

{{velocity}}
{{code language="none"}}
{{menu type="vertical"}}
...
{{/menu}}
{{/code}}
{{/velocity}}

{{menu type="vertical"}}
{{include reference="MenuTemplate" /}}
{{/menu}}</content>
  <object>
    <name>Menu.MenuMacro</name>
    <number>0</number>
    <className>XWiki.JavaScriptExtension</className>
    <guid>4671684e-f58c-4704-850f-7e75208d0e4a</guid>
    <class>
      <name>XWiki.JavaScriptExtension</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <cache>
        <cache>0</cache>
        <defaultValue>long</defaultValue>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>cache</name>
        <number>5</number>
        <prettyName>Caching policy</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>long|short|default|forbid</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </cache>

        <contenttype>PureText</contenttype>
        <disabled>0</disabled>
        <editor>PureText</editor>
        <name>code</name>
        <number>2</number>
        <prettyName>Code</prettyName>
        <rows>20</rows>
        <size>50</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>

      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>1</number>
        <prettyName>Name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <parse>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>parse</name>
        <number>4</number>
        <prettyName>Parse content</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </parse>
      <use>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>use</name>
        <number>3</number>
        <prettyName>Use this extension</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>currentPage|onDemand|always</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </use>
    </class>
    <property>
      <cache>long</cache>
    </property>
    <property>
      require(['jquery'], function($) {
  // It's not possible to write a CSS selector that targets list items containing lists so we rely on JavaScript.
  // The 'dropdown' CSS class is used only to display the down/left arrow.
  $('.menu-horizontal li ul').parent().addClass('xDropdown');

  // Make sure the menu separators are really empty.
  $('.menu-horizontal, .menu-vertical').find('li > br:first-child').remove();

  // Collapsible menu bahavior.
  $('.menu-vertical.collapsible').each(function(){
    var open = $(this).hasClass('open');
    $(this).find('li ul').each(function() {
      $(this).addClass('xDropdown-menu').parent().addClass('xDropdown' + (open ? ' open' : ''));
      // Wrap everything (including text nodes) before the sub-menu in a DIV that will toggle its state.
      var toggle = this.ownerDocument.createElement('div');
      $(this).parent().prepend(toggle);
      for(var next = toggle.nextSibling; next != this; next = toggle.nextSibling) {
        toggle.appendChild(next);
      }
      $(toggle).addClass('xDropdown-toggle').on('click', function() {
        $(this).parent().toggleClass('open');
      });
    });
  });

  // In case of horizontal responsive menus, make sub-submenus in the navbar work on mobile devices
  $(document).on('touchstart.dropdown.data-api', '.dropdown-submenu > a', function (event) {
    event.preventDefault();
  });
})
    </property>
    <property>
      <name/>
    </property>
    <property>
      <parse>0</parse>
    </property>
    <property>
      <use>onDemand</use>
    </property>
  </object>
  <object>
    <name>Menu.MenuMacro</name>
    <number>0</number>
    <className>XWiki.StyleSheetExtension</className>
    <guid>75fa0b90-a8bc-4a88-a9e9-bc938d79da85</guid>
    <class>
      <name>XWiki.StyleSheetExtension</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <cache>
        <cache>0</cache>
        <defaultValue>long</defaultValue>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>cache</name>
        <number>5</number>
        <prettyName>Caching policy</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>long|short|default|forbid</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </cache>

        <contenttype>PureText</contenttype>
        <disabled>0</disabled>
        <editor>PureText</editor>
        <name>code</name>
        <number>2</number>
        <prettyName>Code</prettyName>
        <rows>20</rows>
        <size>50</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>

      <contentType>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>contentType</name>
        <number>6</number>
        <prettyName>Content Type</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>CSS|LESS</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </contentType>
      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>1</number>
        <prettyName>Name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <parse>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>parse</name>
        <number>4</number>
        <prettyName>Parse content</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </parse>
      <use>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>use</name>
        <number>3</number>
        <prettyName>Use this extension</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>currentPage|onDemand|always</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </use>
    </class>
    <property>
      <cache>long</cache>
    </property>
    <property>
      #template('colorThemeInit.vm')

/* Since Colibri doesn't support LESS, we manually prefix selectors with '.skin-colibri' */

/* General Styles */

.skin-colibri .menu-vertical ul,
.skin-colibri .menu-horizontal ul {
  list-style-type: none;
  margin: auto;
  padding: 0;
}

.skin-colibri .menu-vertical ul *,
.skin-colibri .menu-horizontal ul * {
  line-height: normal;
}

.skin-colibri .menu-vertical li,
.skin-colibri .menu-horizontal li {
  cursor: default;
  line-height: 16px;
  min-height: 16px;
}

.skin-colibri .menu-vertical img,
.skin-colibri .menu-horizontal img {
  vertical-align: middle;
}

/* Horizontal separator */
.skin-colibri .menu-vertical li:empty,
.skin-colibri .menu-horizontal li li:empty {
  border-top: 1px solid $theme.borderColor;
  line-height: 1px;
  min-height: 0;
  padding: 0;
}

/* Horizontal Menu */

.skin-colibri .menu.menu-horizontal {
  #css3_backgroundLinearGradient({
    'to': 'bottom',
    'colors': [
      {'color': $theme.menuGradientColor, 'position': '0%'},
      {'color': $theme.menuBackgroundColor, 'position': '50%'}
    ]
  })
}

.skin-colibri .menu.menu-horizontal {
  background-color: $theme.menuBackgroundColor;
  color: $theme.menuLinkColor;
  font-size: .85em;
  height: 2em;
  padding-left: 25px;
}

.skin-colibri .menu-horizontal li {
  padding: 0.2em 1.3em;
}

.skin-colibri .menu-horizontal li li:empty {
  border-top: 1px solid $theme.textSecondaryColor;
  margin: .5em 1em;
}

/* Vertical separator */
.skin-colibri .menu-horizontal > ul > li:empty {
  border-left: 1px solid $theme.textSecondaryColor;
  margin: 0 .5em;
  padding: 0;
  vertical-align: middle;
}

.skin-colibri .menu-horizontal li a,
.skin-colibri .menu-horizontal li a:visited {
  color: inherit;
}

.skin-colibri .menu-horizontal > ul > li {
  display: inline-block;
}

.skin-colibri .menu-horizontal > ul > li.xDropdown:after {
  content: '\25BC';
  font-size: .8em;
  margin-left: .5em;
}

.skin-colibri .menu-horizontal li li.xDropdown:after {
  content: '\25B8';
  float: right;
  margin-left: .5em;
}

.skin-colibri .menu-horizontal > ul ul {
  background: none repeat scroll 0 0 $theme.menuBackgroundColor;
  border-radius: 0 0 .8em .8em;
  box-shadow: 0 1px 1px $theme.borderColor;
  display: none;
  margin: 0.2em 0 0 -1.3em;
  padding-bottom: .3em;
  position: absolute;
  z-index: 1000;
}

.skin-colibri .menu-horizontal li:hover > ul {
  display: block;
}

/* The only way to have a menu with more than 2 levels without JavaScript is to use a fixed width. */
.skin-colibri .menu-horizontal.fixedWidth li li {
  position: relative;
}

.skin-colibri .menu-horizontal.fixedWidth ul ul {
  width: 12em;
}

.skin-colibri .menu-horizontal.fixedWidth ul ul ul {
  left: 12em;
  margin: 0;
  top: 0;
}

/* Vertical Menu */

.skin-colibri .menu.menu-vertical {
  color: inherit;
}

.skin-colibri .menu.menu-vertical ul {
  margin-top: .3em;
}

.skin-colibri .menu.menu-vertical ul ul {
  margin-left: 1em;
}

.skin-colibri .menu-vertical li {
  padding: .3em;
}

.skin-colibri .menu-vertical li:empty {
  margin: .3em 0;
}

.skin-colibri .menu-vertical .xDropdown-toggle {
  cursor: pointer;
}

.skin-colibri .menu-vertical .xDropdown-toggle:hover {
  background-color: $theme.highlightColor;
}

.skin-colibri .menu-vertical .xDropdown-toggle:after {
  content: '\25C2';
  float: right;
}

.skin-colibri .menu-vertical .xDropdown.open .xDropdown-toggle:after {
  content: '\25BE';
}

.skin-colibri .menu-vertical .xDropdown-menu {
  display: none;
}

.skin-colibri .menu-vertical .xDropdown.open > .xDropdown-menu {
  display: block;
}
    </property>
    <property>
      <contentType>CSS</contentType>
    </property>
    <property>
      <name>Colibri Styling</name>
    </property>
    <property>
      <parse>1</parse>
    </property>
    <property>
      <use>onDemand</use>
    </property>
  </object>
  <object>
    <name>Menu.MenuMacro</name>
    <number>1</number>
    <className>XWiki.StyleSheetExtension</className>
    <guid>81584261-5cad-4a9c-b96d-9d6237122407</guid>
    <class>
      <name>XWiki.StyleSheetExtension</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <cache>
        <cache>0</cache>
        <defaultValue>long</defaultValue>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>cache</name>
        <number>5</number>
        <prettyName>Caching policy</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>long|short|default|forbid</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </cache>

        <contenttype>PureText</contenttype>
        <disabled>0</disabled>
        <editor>PureText</editor>
        <name>code</name>
        <number>2</number>
        <prettyName>Code</prettyName>
        <rows>20</rows>
        <size>50</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>

      <contentType>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>contentType</name>
        <number>6</number>
        <prettyName>Content Type</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>CSS|LESS</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </contentType>
      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>1</number>
        <prettyName>Name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <parse>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>parse</name>
        <number>4</number>
        <prettyName>Parse content</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </parse>
      <use>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>use</name>
        <number>3</number>
        <prettyName>Use this extension</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>currentPage|onDemand|always</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </use>
    </class>
    <property>
      <cache>long</cache>
    </property>
    <property>
      @media print {
  .menu {
    display: none;
  }
}
.menu {
  &.menu-vertical {
    ul {
      list-style-type: none;
      margin-top: .3em;
      padding: 0;
      ul {
        padding: 0 1em;
      }
    }
    li {
      padding: .3em;
      /* Separator horizontal inside menu */
      &:empty {
        .nav-divider(@dropdown-divider-bg);
        margin: .3em 0;
        padding: 0;
      }
    }
    .xDropdown-menu {
      display: none;
    }
    .xDropdown-toggle {
      cursor: pointer;
      position: relative;
      &:hover {
        background-color: @nav-link-hover-bg;
      }
      &:after {
        .caret;
        content: '';
        /* Positioning */
        position: absolute;
        margin-top: @line-height-computed / 3;
        right: 1em;
        /* Collapsed arrow style */
        border-bottom: 4px solid transparent;
        border-right: 4px solid;
        border-top: 4px solid transparent;
      }
    }
    .xDropdown.open {
      > .xDropdown-toggle:after {
        /* Expanded arrow style */
        .caret;
        margin-top: @line-height-computed / 2;
      }
      > .xDropdown-menu {
        display: block;
      }
    }
  }
  &.menu-horizontal {
    /* Stylization: Navbars */
    .clearfix;
    background-color: @navbar-default-bg;
    border-color: @navbar-default-border;
    /* Custom styling */
    .box-shadow(0 2px 8px rgba(0,0,0,0.4) inset);
    min-height: @navbar-height;
    padding-left: 25px;
    & > ul {
      padding-left: 0;
      list-style-type: none;
      margin: 0;
      & > li {
        position: relative;
        display: block;
        padding: @nav-link-padding;
        padding-top: @navbar-padding-vertical;
        padding-bottom: @navbar-padding-vertical;
        @media (min-width: @grid-float-breakpoint) {
          float: left;
        }
        line-height: @line-height-computed;
        color: @navbar-default-link-color;
        &:hover {
          color: @navbar-default-link-hover-color;
          background-color: @navbar-default-link-hover-bg;
          background-color: @navbar-default-link-active-bg;
          color: @navbar-default-link-active-color;
          /* When hovering, have the same color for text and link usage */
          & > span > a {
            background-color: @navbar-default-link-active-bg;
            color: @navbar-default-link-active-color;
          }
        }
        /* Links inside menu */
        a {
          color: @navbar-default-link-color;
          &:hover {
            text-decoration: none;
          }
        }
        /* Containers, images inside menu */
        div, img {
          /* Limit the height to the nav height minus the padding and minus border */
          max-height: @navbar-height - (2 * @navbar-padding-vertical) - 2px;
          overflow: hidden;
        }
        /* Separator vertical inside menu */
        &:empty {
          height: @navbar-height;
          margin: 0 ((@line-height-computed / 2) - 1);
          padding: 0;
          border-right: 1px solid @navbar-default-border;
        }
      }
      /* Stylization: Dropdowns */
      ul {
        position: absolute;
        top: 100%;
        left: 0;
        z-index: @zindex-dropdown;
        display: none; // none by default, but block on "open" of the menu
        float: left;
        min-width: 160px;
        padding: 5px 0;
        margin: 2px 0 0; // override default ul
        list-style: none;
        font-size: @font-size-base;
        text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)
        background-color: @dropdown-bg;
        border: 1px solid @dropdown-border;
        border-radius: @border-radius-base;
        .box-shadow(0 6px 12px rgba(0,0,0,.175));
        background-clip: padding-box;
        margin-top: 0;
        border-top-right-radius: 0;
        border-top-left-radius: 0;
        li {
          /* Text inside menu */
          color: @dropdown-link-color;
          /* Links inside menu */
          a {
            display: block;
            padding: 3px 20px;
            clear: both;
            font-weight: normal;
            line-height: @line-height-base;
            color: @dropdown-link-color;
            overflow: hidden;
            text-overflow: ellipsis; // Displaying ... if the text is too long
            &:hover {
              /* &:extend(.dropdown-menu>li>a:hover); */
              text-decoration: none;
              color: @dropdown-link-hover-color;
              background-color: @dropdown-link-hover-bg;
            }
          }
          /* Submenus inside menu */
          &.xDropdown {
            /* Even if we don't have links, display the content the same way */
            display: block;
            padding: 3px 20px;
            clear: both;
            font-weight: normal;
            line-height: @line-height-base;
            color: @dropdown-link-color;
            /* Empty dropdowns should have height in order to display the arrow */
            min-height: 2 * @font-size-base;
            &:hover {
              text-decoration: none;
              color: @dropdown-link-hover-color;
              background-color: @dropdown-link-hover-bg;
              /* When in dropdown we also have a link, adjust the hover color */
              & > span > a {
                color: @dropdown-link-hover-color;
              }
            }
            /* When in dropdown we also have a link, reset the duplicated padding */
            & > span > a {
              padding: 0;
              display: inherit;
            }
            /* Place the arrow on the right */
            &:after {
              position: absolute;
              margin-top: @line-height-computed / 2;
              right: 8px;
            }
          }
          /* Separator horizontal inside menu */
          &:empty {
            .nav-divider(@dropdown-divider-bg);
            padding: 0;
          }
        }
      }
    }
    /* Stylization: Generic */
    li {
      /* Display submenus on hover */
      &:hover > ul {
        display: block;
      }
      /* Display an arrow for expandable items */
      &.xDropdown {
        &:after {
          .caret;
          content: '';
          margin-left: .5em;
        }
      }
    }
    /* The only way to have a menu with more than 2 levels without JavaScript is to use a fixed width. */
    &.fixedWidth {
      li li {
        position: relative;
      }
      ul ul {
        width: 12em;
        ul {
          left: 12em;
          margin: 0;
          top: 0;
          /* Bring the dropdown closer in order to hover it */
          margin-left: -2px;
          /* Adjust corner to better look as a submenu */
          border-top-left-radius: 0;
        }
      }
    }
    /* Resetting rules for mobile view */
    @media (max-width: @screen-xs-max) {
      > ul {
        margin: 0 0 0 -25px; /* Remove padding added in normal view */
        > li {
          &:empty {
            .nav-divider(@navbar-default-border);
          }
        }
        ul {
          /* Resetting rules for mobile view */
          position: static;
          float: none;
          width: auto;
          margin-top: 0;
          background-color: transparent;
          border: 0;
          box-shadow: none;
          li {
            /* Text inside menu */
            color: @navbar-default-link-color;
            /* Links inside menu */
            a {
              color: @navbar-default-link-color;
              &:hover {
                /* Preserve the styling from dropdown */
              }
            }
            /* Submenus inside menu */
            &.xDropdown {
              color: @navbar-default-link-color;
              &:hover {
                background-color: transparent;
                color: inherit;
              }
              /* When in dropdown we also have a link */
              > span > a {
                color: @navbar-default-link-color;
              }
            }
            &:empty {
              .nav-divider(@navbar-default-border);
            }
          }
        }
      }
      li {
        &.xDropdown {
          /* Place the arrow on the right */
          &:after {
            position: absolute;
            margin-top: @line-height-computed;
            right: 1em;
            top: 0;
          }
        }
      }
      &.fixedWidth ul ul {
        width: auto;
      }
    }
  }
}

.menu .wikiexternallink {
  background: rgba(0, 0, 0, 0) none repeat scroll 0 center;
  padding: 0;
}

.menu-horizontal-toggle {
  .clearfix;
  background-color: @navbar-default-bg;
  border-color: @navbar-default-border;
  .box-shadow(0 2px 8px rgba(0,0,0,0.4) inset);
  min-height: @navbar-height;
  & .navbar-toggle {
    float: left;
    padding-left: 15px;
    & .icon-bar {
      background-color: @navbar-default-link-color;
      transition: .3s ease all;
      &:nth-of-type(2) {
        opacity: 0;
      }
      &:nth-of-type(3) {
        transform: rotate(45deg);
      }
      &:nth-of-type(4) {
        transform: rotate(-45deg);
        margin-top: -2px;
        margin-bottom: 6px;
      }
    }
    &.collapsed {
      .icon-bar {
        &:nth-of-type(2) {
          opacity: 1;
        }
        &:nth-of-type(3) {
          transform: rotate(0);
        }
        &:nth-of-type(4) {
          transform: rotate(0);
          margin-top: 4px;
          margin-bottom: 0;
        }
      }
    }
  }
}
    </property>
    <property>
      <contentType>LESS</contentType>
    </property>
    <property>
      <name>Flamingo Styling</name>
    </property>
    <property>
      <parse/>
    </property>
    <property>
      <use>onDemand</use>
    </property>
  </object>
  <object>
    <name>Menu.MenuMacro</name>
    <number>0</number>
    <className>XWiki.WikiMacroClass</className>
    <guid>1f17c513-df60-481f-b8d0-15ef11b75d45</guid>
    <class>
      <name>XWiki.WikiMacroClass</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <async_cached>
        <defaultValue>0</defaultValue>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType/>
        <name>async_cached</name>
        <number>12</number>
        <prettyName>Cached</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </async_cached>
      <async_context>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>1</multiSelect>
        <name>async_context</name>
        <number>13</number>
        <prettyName>Context elements</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>5</size>
        <unmodifiable>0</unmodifiable>
        <values>doc.reference=Document|icon.theme=Icon theme|locale=Language|request.base=Request base URL|request.parameters=Request parameters|request.url=Request URL|request.wiki=Request wiki|user=User|wiki=Wiki</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </async_context>
      <async_enabled>
        <defaultValue>0</defaultValue>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType/>
        <name>async_enabled</name>
        <number>11</number>
        <prettyName>Asynchronous rendering</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </async_enabled>

        <disabled>0</disabled>
        <editor>Text</editor>
        <name>code</name>
        <number>10</number>
        <prettyName>Macro code</prettyName>
        <rows>20</rows>
        <size>40</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>

      <contentDescription>
        <contenttype>PureText</contenttype>
        <disabled>0</disabled>
        <editor>PureText</editor>
        <name>contentDescription</name>
        <number>9</number>
        <prettyName>Content description (Not applicable for "No content" type)</prettyName>
        <rows>5</rows>
        <size>40</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>
      </contentDescription>
      <contentJavaType>
        <cache>0</cache>
        <defaultValue>Unknown</defaultValue>
        <disabled>0</disabled>
        <displayType>input</displayType>
        <freeText>allowed</freeText>
        <largeStorage>1</largeStorage>
        <multiSelect>0</multiSelect>
        <name>contentJavaType</name>
        <number>8</number>
        <picker>1</picker>
        <prettyName>Macro content type</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator>|</separator>
        <separators>|</separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>Unknown|Wiki</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </contentJavaType>
      <contentType>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>contentType</name>
        <number>7</number>
        <prettyName>Macro content availability</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator>|</separator>
        <separators>|</separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>Optional|Mandatory|No content</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </contentType>
      <defaultCategory>
        <disabled>0</disabled>
        <name>defaultCategory</name>
        <number>4</number>
        <prettyName>Default category</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </defaultCategory>
      <description>
        <contenttype>PureText</contenttype>
        <disabled>0</disabled>
        <editor>PureText</editor>
        <name>description</name>
        <number>3</number>
        <prettyName>Macro description</prettyName>
        <rows>5</rows>
        <size>40</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>
      </description>
      <id>
        <disabled>0</disabled>
        <name>id</name>
        <number>1</number>
        <prettyName>Macro id</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </id>
      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>2</number>
        <prettyName>Macro name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <supportsInlineMode>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>supportsInlineMode</name>
        <number>5</number>
        <prettyName>Supports inline mode</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </supportsInlineMode>
      <visibility>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>visibility</name>
        <number>6</number>
        <prettyName>Macro visibility</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator>|</separator>
        <separators>|</separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>Current User|Current Wiki|Global</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </visibility>
    </class>
    <property>
      <async_cached>0</async_cached>
    </property>
    <property>
      <async_context/>
    </property>
    <property>
      <async_enabled>0</async_enabled>
    </property>
    <property>
      {{velocity}}
#set ($id = $xcontext.macro.params.id)
#set ($type = $xcontext.macro.params.type)
#set ($colorTheme = $xwiki.getUserPreference('colorTheme'))
#if ("$!colorTheme" != '')
  ## Make sure we use an absolute reference (see XWIKI-9672)
  #set ($colorTheme = $services.model.resolveDocument($colorTheme, $doc.documentReference))
#end
#set ($discard = $xwiki.ssx.use("$xcontext.macro.doc.prefixedFullName", {'colorTheme': $colorTheme}))
#set ($discard = $xwiki.jsx.use("$xcontext.macro.doc.prefixedFullName"))
#if($type.contains('horizontal'))
  (% role="navigation" class="menu-horizontal-toggle" %)(((
    (% class="navbar-header" %)(((
      {{html}}
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#$!{id}" aria-expanded="false">
          <span class="sr-only"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
      {{/html}}
    )))
    (% id="${id}" class="menu menu-$!type collapse navbar-collapse" %)(((
      $xcontext.macro.content
    )))
  )))
#else
  (% #if ("$!id" != '') id="$id"#end class="menu menu-$!type" %)(((
    $xcontext.macro.content
  )))
#end
{{/velocity}}
    </property>
    <property>
      <contentDescription>Define the menu structure using wiki syntax. Each menu item should be a list item and should contain the menu item label or link. You can use nested lists for sub-menu items.</contentDescription>
    </property>
    <property>
      <contentJavaType/>
    </property>
    <property>
      <contentType>Mandatory</contentType>
    </property>
    <property>
      <defaultCategory>Navigation</defaultCategory>
    </property>
    <property>
      <description>Displays a menu created using simple wiki syntax (nested lists and links).</description>
    </property>
    <property>
      <id>menu</id>
    </property>
    <property>
      <name>Menu</name>
    </property>
    <property>
      <supportsInlineMode>0</supportsInlineMode>
    </property>
    <property>
      <visibility>Global</visibility>
    </property>
  </object>
  <object>
    <name>Menu.MenuMacro</name>
    <number>0</number>
    <className>XWiki.WikiMacroParameterClass</className>
    <guid>bbeba321-44be-4e3a-b102-f59cada761ec</guid>
    <class>
      <name>XWiki.WikiMacroParameterClass</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <defaultValue>
        <disabled>0</disabled>
        <name>defaultValue</name>
        <number>4</number>
        <prettyName>Parameter default value</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </defaultValue>
      <description>
        <disabled>0</disabled>
        <name>description</name>
        <number>2</number>
        <prettyName>Parameter description</prettyName>
        <rows>5</rows>
        <size>40</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>
      </description>
      <mandatory>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>mandatory</name>
        <number>3</number>
        <prettyName>Parameter mandatory</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </mandatory>
      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>1</number>
        <prettyName>Parameter name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <type>
        <disabled>0</disabled>
        <name>type</name>
        <number>5</number>
        <prettyName>Parameter type</prettyName>
        <size>60</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </type>
    </class>
    <property>
      <defaultValue>horizontal</defaultValue>
    </property>
    <property>
      <description>The optional menu type determines the menu appearance and behaviour. The supported values are: horizontal (default value) and vertical.</description>
    </property>
    <property>
      <mandatory>0</mandatory>
    </property>
    <property>
      <name>type</name>
    </property>
    <property>
      <type/>
    </property>
  </object>
  <object>
    <name>Menu.MenuMacro</name>
    <number>2</number>
    <className>XWiki.WikiMacroParameterClass</className>
    <guid>00eee99c-d118-4b26-8784-98ade58b5af7</guid>
    <class>
      <name>XWiki.WikiMacroParameterClass</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <defaultValue>
        <disabled>0</disabled>
        <name>defaultValue</name>
        <number>4</number>
        <prettyName>Parameter default value</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </defaultValue>
      <description>
        <disabled>0</disabled>
        <name>description</name>
        <number>2</number>
        <prettyName>Parameter description</prettyName>
        <rows>5</rows>
        <size>40</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>
      </description>
      <mandatory>
        <disabled>0</disabled>
        <displayFormType>select</displayFormType>
        <displayType>yesno</displayType>
        <name>mandatory</name>
        <number>3</number>
        <prettyName>Parameter mandatory</prettyName>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
      </mandatory>
      <name>
        <disabled>0</disabled>
        <name>name</name>
        <number>1</number>
        <prettyName>Parameter name</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </name>
      <type>
        <disabled>0</disabled>
        <name>type</name>
        <number>5</number>
        <prettyName>Parameter type</prettyName>
        <size>60</size>
        <unmodifiable>0</unmodifiable>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </type>
    </class>
    <property>
      <defaultValue/>
    </property>
    <property>
      <description>Optional menu identifier that will be set on the HTML element that wraps the menu. You can use this identifier in JavaScript code to access the menu DOM or in functional tests to assert the menu items.</description>
    </property>
    <property>
      <mandatory>0</mandatory>
    </property>
    <property>
      <name>id</name>
    </property>
    <property>
      <type/>
    </property>
  </object>
</xwikidoc>

Output:
【漏洞类型】XSS（跨站脚本攻击）
【严重性】High

【漏洞摘要】
此代码存在XSS漏洞。具体表现为在原始代码中，宏代码和样式表代码直接使用了用户输入的内容（如`$xcontext.macro.content`和`$xwiki.getDocument('MenuTemplate').content`），但未进行适当的转义处理。例如，在`MenuMacro.xml`文件中，`$xwiki.getDocument('MenuTemplate').content`直接插入到HTML中，这可能导致攻击者通过注入恶意脚本到`MenuTemplate`文档中，从而在用户浏览器中执行恶意脚本。

【关键代码模式】
- 未转义的用户输入内容插入到HTML或JavaScript中，例如：
  ```velocity
  $xwiki.getDocument('MenuTemplate').content
  ```
- 使用`{{include reference="MenuTemplate" /}}`宏而未对内容进行转义

【修复方法说明】
修复方法通过使用`$services.rendering.escape`函数对用户输入的内容进行转义处理，确保插入到HTML中的内容不会被恶意脚本破坏。同时，对`MenuTemplate`文档的内容进行了转义处理，避免直接插入HTML宏。