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()))
  #if ($limit > 0)
    #set($discard = $map.put('returnedrows', $mathtool.min($items.size(), $limit)))
  #else
    ## When the limit is 0, it's equivalent to no limit at all and the actual number of returned results can be used.
    #set($discard = $map.put('returnedrows', $items.size()))
  #end
  #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>

## 内容2
### 文件路径：
xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.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.3" reference="WikiManager.WikisLiveTableResultsMacros" locale="">
  <web>WikiManager</web>
  <name>WikisLiveTableResultsMacros</name>
  <language/>
  <defaultLanguage/>
  <translation>0</translation>
  <creator>xwiki:XWiki.Admin</creator>
  <parent>WikiManager.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>{{include reference="XWiki.LiveTableResultsMacros" /}}

{{velocity output="false"}}
#*
* We override the following macro from XWiki.LiveTableResultsMacros to:
* - disable the filter on XWikiServerTemplate (revert when https://jira.xwiki.org/browse/XWIKI-10010 is fixed)
* - put order by value in MAX() function and GROUP BY (revert when https://jira.xwiki.org/browse/XWIKI-15595 is fixed)
*#
#macro(gridresultwithfilter_buildQuery $className $collist $filterfrom $filterwhere $filterParams)
  ## Additional columns, should *not* contain parameters, all added column names must be filtered
  #set($fromSql = '')
  ## parametrized filter part of the query
  #set($whereSql = '')
  ## list of parameters to use with $whereSql
  #set($whereParams = {})
  #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.
      #set($whereSql = "${whereSql} lower(:wikitag${foreach.count}) in (select lower(tag) from tagprop.list tag)")
      #set($discard = $whereParams.put("wikitag$foreach.count", "${tag}"))
    #end
    #set($whereSql = "${whereSql})")
  #end
  ##
  ##
  ## Order
  ##
  #set($order = "$!request.sort")
  #if($order != '')
    #set($orderDirection = "$!{request.get('dir').toLowerCase()}")
    #if("$!orderDirection" != '' && "$!orderDirection" != 'asc')
      #set($orderDirection = 'desc')
    #end
    #if($order == 'description')
      #set($tableAlias = 'dtable')
    #else
      #set($tableAlias = $order)
    #end
    ## only safe version should be used in SQL queries
    #livetable_getTableAlias($tableAlias)
    ##
    #if($order.startsWith('doc.'))
      #if ($order == "doc.fullName")
        #set($orderSql = " order by ${safe_tableAlias.replace('_','.')} ${orderDirection}")
      #else
        #set($orderSql = " order by MAX(${safe_tableAlias.replace('_','.')}) ${orderDirection}")
      #end
    #elseif($tablelist.contains($tableAlias))
      #set ($multiselect = "$!{class.get($order).getProperty('multiSelect').getValue()}")
      #if ($multiselect != '1')
        #set ($orderSql = " order by MAX(${safe_tableAlias}.value) ${orderDirection}")
      #end
    #elseif($order == 'averagevote')
      #livetable_getTableName($order)
      #set($fromSql = "${fromSql}, BaseObject as objratings, ${tableName} ${safe_tableAlias}")
      #set($whereSql = "${whereSql} and doc.fullName=objratings.name and objratings.className='XWiki.AverageRatingsClass' and objratings.id=${safe_tableAlias}.id.id and ${safe_tableAlias}.name = :wikiorder")
      #set($discard = $whereParams.put('wikorder', "${order}"))
      #set($orderSql = " order by MAX(${safe_tableAlias}.value) ${orderDirection}")
    #else
      #set($multiselect = $class.get($order).getProperty('multiSelect').getValue())
      #if("$multiselect" != '1')
        #livetable_getTableName($order)
        #set($fromSql = "${fromSql}, ${tableName} ${safe_tableAlias}")
        #set($whereSql = "${whereSql} and obj.id=${safe_tableAlias}.id.id and ${safe_tableAlias}.name = :wikiorder")
        #set($discard = $whereParams.put('wikiorder', "${order}"))
        #set($orderSql = " order by MAX(${safe_tableAlias}.value) ${orderDirection}")
      #end
    #end
  #end
  ##
  ##
  ## Compute the final queries
  ##
  #set($sqlParams = {})
  #set($tagsMatchingParams = {})
  #set($allMatchingParams = {})
  #if("$!className" != '')
    ## Class query
    ######## WIKI UI MAINWIKI CUSTOMIZATION #########
    #set($countSql = ", BaseObject as obj $!fromSql $!filterfrom where obj.name=doc.fullName and obj.className = :className and doc.fullName <> :classTemplate $!whereSql $!filterwhere")
    #set($discard = $sqlParams.put('className', "${className}"))
    #set($discard = $sqlParams.put('classTemplate', "${className}Template"))
    #set($discard = $sqlParams.putAll($whereParams))
    ##
    #set($tagsMatchingFiltersFrom = ", BaseObject as obj $!fromSql $!filterfrom")
    #set($tagsMatchingFiltersWhere = "obj.name=doc.fullName and obj.className = :tagsClassName and doc.fullName <> :tagsClassTemplate $!whereSql $!filterwhere")
    #set($discard = $tagsMatchingParams.put('tagsClassName', "${className}"))
    #set($discard = $tagsMatchingParams.put('tagsClassTemplate', "${className}Template"))
    #set($discard = $tagsMatchingParams.putAll($whereParams))
    ##
    #set($allMatchingTagsFrom = ", BaseObject as obj $!filterfrom")
    #set($allMatchingTagsWhere = "obj.name=doc.fullName and obj.className = :allClassName and doc.fullName <> :allClassTemplate $!filterwhere")
    #set($discard = $allMatchingParams.put('allClassName', "${className}"))
    #set($discard = $allMatchingParams.put('allClassTemplate', "${className}Template"))
    #################################################
    ##
    #if($filterParams)
      #set($discard = $sqlParams.putAll($filterParams))
      #set($discard = $tagsMatchingParams.putAll($filterParams))
      #set($discard = $allMatchingParams.putAll($filterParams))
    #end
  #else
    ## Document query
    #set($countSql = "$!fromSql $!filterfrom where 1=1 $!whereSql $!filterwhere")
    #set($discard = $sqlParams.putAll($whereParams))
    ##
    #set($tagsMatchingFiltersFrom = "$!fromSql $!filterfrom")
    #set($tagsMatchingFiltersWhere = "1=1 $!whereSql $!filterwhere")
    #set($discard = $tagsMatchingParams.putAll($whereParams))
    ##
    #set($allMatchingTagsFrom = "$!filterfrom")
    #set($allMatchingTagsWhere = "1=1 $!filterwhere")
    ##
    #if($filterParams)
      #set($discard = $sqlParams.putAll($filterParams))
      #set($discard = $tagsMatchingParams.putAll($filterParams))
      #set($discard = $allMatchingParams.putAll($filterParams))
    #end
  #end
  ######## WIKI UI MAINWIKI CUSTOMIZATION #########
  ## Group by doc.fullName if needed
  #if ($orderSql && $orderSql.contains('order by MAX('))
    #set($orderSql = "GROUP BY doc.fullName $orderSql")
  #end
  #################################################
  #set($sql = "$countSql $!{orderSql}")
#end
#*
* We override the following macro from XWiki.LiveTableResultsMacros to disable the filter on hidden documents and to display wiki ui properties with a custom code
*#
#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
  ######## WIKI UI MAINWIKI CUSTOMIZATION #########
  ## The descriptors are always hidden, so we don't filter on the hidden column.
  #set($query = $services.query.hql($sql).addFilter('currentlanguage').setLimit($limit).setOffset($offset).bindValues($sqlParams))
  #set($items = $query.execute())
  #if ($sql.contains('GROUP BY doc.fullName'))
    ## The group by we have to use to make sure we get unique documents break $query.count so we remove it
    #set($countQuery = $services.query.hql($sql.replace('GROUP BY doc.fullName', '')).addFilter('unique'))
    #set($void = $countQuery.setWiki($query.wiki))
    #set($void = $countQuery.bindValues($query.namedParameters))
  #else
    $countQuery = $query
  #end
  #set($discard = $map.put('totalrows', $countQuery.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))
#end
#*
* We override the following macro from XWiki.LiveTableResultsMacros to add custom actions (join, leave, etc...)
* and to display the membershipType.
*#
#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($viewable = $xwiki.hasAccessLevel('view', $xcontext.user, "${xcontext.database}:${docName}"))
  #set($row = {'doc_viewable': $viewable})
  #if(!$viewable)
    #set($discard = $row.put('doc_fullName', "${xcontext.database}:${item}"))
  #else
    #set($itemDoc = $xwiki.getDocument($docName))
    ######## WIKI UI MAINWIKI CUSTOMIZATION #########
    ## Some constants which are usefull while processing each item.
    #set ($currentUser = $services.model.serialize($xcontext.userReference, 'default'))
    #set ($deleteWikiDocumentReference = $services.model.createDocumentReference('', 'WikiManager', 'DeleteWiki'))
    #set ($joinWikiDocumentReference = $services.model.createDocumentReference('', 'WikiManager', 'JoinWiki'))
    #set ($leaveWikiDocumentReference = $services.model.createDocumentReference('', 'WikiManager', 'LeaveWiki'))
    #set ($wikiId = $docName.toLowerCase().replaceAll("xwiki.xwikiserver", ""))
    #set ($wiki = $services.wiki.getById($wikiId))
    ## Init membership actions as denied.
    #set ($itemMembershipType = $services.wiki.user.getMembershipType($wiki.id))
    #set ($itemUserScope = $services.wiki.user.getUserScope($wiki.id))
    #set ($itemHasJoin = false)
    #set ($itemHasRequestJoin = false)
    #set ($itemHasCancelJoinRequest = false)
    #set ($itemHasViewInvitation = false)
    #set ($itemHasLeave = false)
    ## Determine if current user is already a member.
    #set ($currentUserIsMember = ($services.wiki.user.isMember($currentUser, $wiki.id) || $currentUser == $wiki.ownerId))
    ## Determine available membership action and action URL.
    #if ($currentUserIsMember)
      ## Don't allow the owner to leave the workspace.
      #if ($wiki.ownerId != $currentUser)
        #set ($itemHasLeave = true)
        #set ($leaveUrl = $xwiki.getURL($leaveWikiDocumentReference, 'view', "wikiId=${wikiId}"))
      #end
    #elseif($itemUserScope != 'LOCAL_ONLY')
      ##
      ## Determine if, regardless of membership type, an existing invitation exists for the
      #set ($invitations = $services.wiki.user.getAllInvitations($wikiId))
      #set ($existingInvitation = false)
      #if ($invitations && !$invitations.isEmpty())
        #foreach($invitation in $invitations)
          #if ($invitation.userId == $currentUser && $invitation.status == 'PENDING')
            #set ($existingInvitation = $invitation)
          #end
        #end
      #end
      #if ($existingInvitation)
        #set ($itemHasViewInvitation = true)
        #set ($viewInvitationUrl = $xwiki.getURL($joinWikiDocumentReference, 'view', "wikiId=${wikiId}"))
      #elseif ($itemMembershipType == 'OPEN')
        #set ($itemHasJoin = true)
        #set ($joinUrl = $xwiki.getURL($joinWikiDocumentReference, 'view', "wikiId=${wikiId}"))
      #elseif ($itemMembershipType == 'REQUEST')
        ## Determine if a request was already sent.
        #set ($pendingRequest = false)
        #set ($existingRequests = $services.wiki.user.getAllRequests($wikiId))
        #foreach ($existingRequest in $existingRequests)
          #if ($existingRequest.userId == $currentUser && $existingRequest.status == 'PENDING')
            #set ($pendingRequest = true)
          #end
        #end
        #if ($pendingRequest)
          #set ($itemHasCancelJoinRequest = true)
          #set ($cancelJoinRequestUrl = $xwiki.getURL($joinWikiDocumentReference, 'view', "wikiId=${wikiId}&cancelJoinRequest=1"))
        #else
          #set ($itemHasRequestJoin = true)
          #set ($requestJoinUrl = $xwiki.getURL($joinWikiDocumentReference, 'view', "wikiId=${wikiId}&requestJoin=1"))
        #end
      #end
    #end
    #################################################
    ## Handle translations. We need to make sure we display the data associated to the correct document if the returned
    ## result is a translation.
    #if ("$!docLanguage" != "" && $xwiki.getLanguagePreference() != $docLanguage)
      #set($translatedDoc = $itemDoc.getTranslatedDocument($docLanguage))
      #set($isTranslation = true)
    #else
      #set($translatedDoc = $itemDoc)
      #set($isTranslation = false)
    #end
    #set($fullname = $services.model.serialize($itemDoc.documentReference, 'default'))
    #set($discard = $itemDoc.use($className))
    #if ($isTranslation)
      ## Display the language after the document name so that not all translated documents have the same name displayed
      #set($discard = $row.put('doc_name', "$itemDoc.documentReference.name ($docLanguage)"))
    #else
      #set($discard = $row.put('doc_name', $itemDoc.documentReference.name))
    #end
    #set($discard = $row.put('doc_fullName', $fullname))
    #set($discard = $row.put('doc_space', $itemDoc.space))
    #set($discard = $row.put('doc_url', $xwiki.getURL($docName)))
    #set($discard = $row.put('doc_space_url', $xwiki.getURL($services.model.createDocumentReference($!itemDoc.wiki, $!itemDoc.space, 'WebHome'))))
    #set($discard = $row.put('doc_wiki', $itemDoc.wiki))
    #set($discard = $row.put('doc_wiki_url', $xwiki.getURL($services.model.resolveDocument('', 'default', $itemDoc.documentReference.extractReference('WIKI')))))
    #set($discard = $row.put('doc_hasadmin', $xwiki.hasAdminRights()))
    #set($discard = $row.put('doc_hasedit', $xwiki.hasAccessLevel('edit', $xcontext.user, $fullname)))
    ######## WIKI UI MAINWIKI CUSTOMIZATION #########
    #set($discard = $row.put('doc_hasdelete', $services.wiki.canDeleteWiki($currentUser, $wikiId)))
    #################################################
    #set($discard = $row.put('doc_copy_url', $itemDoc.getURL('view', 'xpage=copy')))
    ######## WIKI UI MAINWIKI CUSTOMIZATION #########
    #set($deleteUrl = $xwiki.getURL($deleteWikiDocumentReference, 'view', "wikiId=${wikiId}"))
    #set($discard = $row.put('doc_delete_url', $deleteUrl))
    #set($discard = $row.put('doc_edit_url', $itemDoc.getURL('view')))
    #################################################
    #set($discard = $row.put('doc_rename_url', $itemDoc.getURL('view', 'xpage=rename&step=1')))
    #set($discard = $row.put('doc_rights_url', $itemDoc.getURL('edit', 'editor=rights')))
    #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($discard = $row.put('doc_author', $xwiki.getLocalUserName($translatedDoc.author, false)))
    #set($discard = $row.put('doc_creationDate', $xwiki.formatDate($translatedDoc.creationDate)))
    #set($discard = $row.put('doc_creator', $xwiki.getLocalUserName($translatedDoc.creator, false)))
    #foreach($colname in $collist)
      #gridresult_buildColumnJSON($colname $row)
    #end
    ######## WIKI UI MAINWIKI CUSTOMIZATION #########
    #if($itemUserScope == 'LOCAL_ONLY' || $wikiId == $services.wiki.mainWikiId)
      #set($discard = $row.put('membershipType', $services.localization.render("platform.wiki.browse.emptyvalue")))
    #else
      #set($discard = $row.put('membershipType', $services.localization.render("WikiManager.WikiUserClass_membershipType_${itemMembershipType.name().toLowerCase()}")))
    #end
    #set($discard = $row.put('doc_hasjoin', $itemHasJoin))
    #set($discard = $row.put('doc_hasrequestJoin', $itemHasRequestJoin))
    #set($discard = $row.put('doc_hasviewInvitation', $itemHasViewInvitation))
    #set($discard = $row.put('doc_hascancelJoinRequest', $itemHasCancelJoinRequest))
    #set($discard = $row.put('doc_hasleave', $itemHasLeave))
    #if ($itemHasJoin)
      #set($discard = $row.put('doc_join_url', $joinUrl))
    #elseif ($itemHasLeave)
      #set($discard = $row.put('doc_leave_url', $leaveUrl))
    #elseif ($itemHasRequestJoin)
      #set($discard = $row.put('doc_requestJoin_url', $requestJoinUrl))
    #elseif ($itemHasViewInvitation)
      #set($discard = $row.put('doc_viewInvitation_url', $viewInvitationUrl))
    #elseif ($itemHasCancelJoinRequest)
      #set($discard = $row.put('doc_cancelJoinRequest_url', $cancelJoinRequestUrl))
    #end
    #################################################
  #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))
  #else
    #set($propClassName = "$!request.get($colname.concat('_class'))")
    #if($propClassName != '')
      #set($propClass = $xwiki.getDocument($propClassName).getxWikiClass())
      #set($discard = $itemDoc.use($propClassName))
    #else
      #set($propClass = $class)
    #end
    ## Retrieve the property type as it might be usefull
    #set($propType = '')
    #if($propClass.getPropertyNames().contains($colname))
      #set($propType = "$!propClass.get($colname).type")
    #end
    #set($fieldValue = "$!itemDoc.getValue($colname)")
    #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' && $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))
    ######## WIKI UI MAINWIKI CUSTOMIZATION #########
    #if($colname == 'wikiprettyname')
      #set($fieldUrl = $xwiki.getURL("$wiki.mainPageReference"))
    #end
    #################################################
    #set($discard = $row.put("${colname}_url", $fieldUrl))
    ## reset to the default class
    #set($discard = $itemDoc.use($className))
  #end
#end
{{/velocity}}</content>
</xwikidoc>

Output:
【漏洞类型】信息泄露漏洞
【严重性】High
【漏洞摘要】此代码允许通过发送768个特定请求来推断密码哈希。具体表现为，`gridresultwithfilter_buildQuery`宏在构建查询时，未对`Password`类型的字段进行特殊处理，导致这些字段被包含在返回的JSON结果中。攻击者可以利用这些信息进行密码哈希碰撞攻击，从而尝试重建用户的密码。
【关键代码模式】`gridresultwithfilter_buildQuery`、`livetable_addColumnToQuery`、`Password`类型字段。
【修复方法说明】通过检查字段类型并跳过`Password`类型的字段，防止敏感数据泄露。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-ui/src/main/resources/IconThemesCode/IconThemeSheet.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.1">
  <web>IconThemesCode</web>
  <name>IconThemeSheet</name>
  <language/>
  <defaultLanguage/>
  <translation>0</translation>
  <creator>xwiki:XWiki.Admin</creator>
  <parent>IconThemesCode.IconThemeClass</parent>
  <author>xwiki:XWiki.Admin</author>
  <contentAuthor>xwiki:XWiki.Admin</contentAuthor>
  <version>1.1</version>
  <title>#if($doc.documentReference.name == 'IconThemeSheet')IconTheme Sheet#{else}$services.display.title($doc, {'displayerHint': 'default'})#end</title>
  <comment/>
  <minorEdit>false</minorEdit>
  <syntaxId>xwiki/2.1</syntaxId>
  <hidden>true</hidden>
  <content>{{velocity}}
#if($doc.fullName != 'IconThemesCode.IconThemeSheet')
{{info}}
This document is an Icon Theme.
{{/info}}

{{{
$doc.content.replace('}}}', '~}}}')
}}}
#else
Sheet of [[IconThemesCode.IconThemeClass]]
#end
{{/velocity}}</content>
  <object>
    <name>IconThemesCode.IconThemeSheet</name>
    <number>0</number>
    <className>XWiki.SheetDescriptorClass</className>
    <guid>654a75c9-88ad-4574-a242-6674ae911967</guid>
    <class>
      <name>XWiki.SheetDescriptorClass</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <action>
        <customDisplay/>
        <disabled>0</disabled>
        <name>action</name>
        <number>1</number>
        <picker>0</picker>
        <prettyName>Action</prettyName>
        <size>30</size>
        <unmodifiable>0</unmodifiable>
        <validationMessage/>
        <validationRegExp/>
        <classType>com.xpn.xwiki.objects.classes.StringClass</classType>
      </action>
    </class>
    <property>
      <action>view</action>
    </property>
  </object>
</xwikidoc>

Output:
【漏洞类型】代码注入漏洞
【严重性】Medium
【漏洞摘要】在原始代码中，使用 Velocity 模板语言的 `$doc.content.replace('}}}', '~}}}')` 方法直接处理用户输入的内容。这意味着如果用户输入的内容中包含恶意的 Velocity 代码，这些代码将被替换并执行，从而导致潜在的安全风险，如代码注入攻击。
【关键代码模式】
- `$doc.content.replace('}}}', '~}}}')`：此行代码直接处理用户输入的内容，未进行适当的验证或转义。
- `{{velocity}}`：Velocity 模板语言的标签，用于在 XWiki 中嵌入 Velocity 代码。
【修复方法说明】修复方法通过使用 `$services.rendering.escape($doc.content, 'xwiki/2.1')` 来转义用户输入的内容，防止潜在的代码注入攻击。这是通过采用最佳实践中的转义用户输入技术来解决的。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-async/xwiki-platform-rendering-async-api/src/main/java/org/xwiki/rendering/async/internal/block/BlockAsyncRendererConfiguration.java
### 代码内容：
/*
 * 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.async.internal.block;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.xwiki.component.descriptor.ComponentRole;
import org.xwiki.component.descriptor.DefaultComponentRole;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.rendering.async.internal.AsyncRendererConfiguration;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.rendering.transformation.TransformationContext;

/**
 * Configuration to pass to {@link BlockAsyncRendererExecutor}.
 *
 * @version $Id$
 * @since 10.10RC1
 */
public class BlockAsyncRendererConfiguration extends AsyncRendererConfiguration
{
    private List<String> id;

    private Block block;

    private XDOM xdom;

    private boolean asyncAllowed;

    private boolean cacheAllowed;

    private Syntax defaultSyntax;

    private String transformationId;

    private Syntax targetSyntax;

    private boolean inline;

    private BlockAsyncRendererDecorator decorator;

    private Set<EntityReference> references;

    private Set<ComponentRole<?>> roles;

    /**
     * @param idElements the id used as prefix (concatenated with contextual information) for the actual job identifier
     * @param block the block to transform
     */
    public BlockAsyncRendererConfiguration(List<?> idElements, Block block)
    {
        if (idElements != null) {
            this.id = new ArrayList<>(idElements.size());
            addElements(idElements);
        } else {
            this.id = new ArrayList<>();
        }
        this.block = block;

        // Enabled by default
        this.asyncAllowed = true;
    }

    private void addElements(Iterable<?> elements)
    {
        for (Object element : elements) {
            if (element instanceof Iterable) {
                addElements((Iterable<?>) element);
            } else {
                this.id.add(element != null ? element.toString() : null);
            }
        }
    }

    /**
     * @return the id
     */
    public List<String> getId()
    {
        return this.id;
    }

    /**
     * @param id the id to set
     */
    public void setId(List<String> id)
    {
        this.id = id;
    }

    /**
     * @return the block
     */
    public Block getBlock()
    {
        return this.block;
    }

    /**
     * @return the XDOM to use in the transformation context
     * @since 14.8RC1
     * @since 14.4.5
     * @since 13.10.10
     */
    public XDOM getXDOM()
    {
        return this.xdom;
    }

    /**
     * @param xdom the XDOM to use in the transformation context
     * @since 14.8RC1
     * @since 14.4.5
     * @since 13.10.10
     */
    public void setXDOM(XDOM xdom)
    {
        this.xdom = xdom;
    }

    /**
     * @return true if the execution should be asynchronous if possible
     */
    public boolean isAsyncAllowed()
    {
        return this.asyncAllowed;
    }

    /**
     * @param asyncAllowed true if the execution should be asynchronous if possible
     */
    public void setAsyncAllowed(boolean asyncAllowed)
    {
        this.asyncAllowed = asyncAllowed;
    }

    /**
     * @return true if the result cache be reused several times
     */
    public boolean isCacheAllowed()
    {
        return cacheAllowed;
    }

    /**
     * @param cacheAllowed true if the result cache be reused several times
     */
    public void setCacheAllowed(boolean cacheAllowed)
    {
        this.cacheAllowed = cacheAllowed;
    }

    /**
     * @return the default syntax
     */
    public Syntax getDefaultSyntax()
    {
        return this.defaultSyntax;
    }

    /**
     * @param defaultSyntax the default syntax
     */
    public void setDefaultSyntax(Syntax defaultSyntax)
    {
        this.defaultSyntax = defaultSyntax;
    }

    /**
     * @return the transformation id to set in the {@link TransformationContext}
     */
    public String getTransformationId()
    {
        return this.transformationId;
    }

    /**
     * @param transformationId the transformation id to set in the {@link TransformationContext}
     */
    public void setTransformationId(String transformationId)
    {
        this.transformationId = transformationId;
    }

    /**
     * @return the target Syntax
     */
    public Syntax getTargetSyntax()
    {
        return this.targetSyntax;
    }

    /**
     * @param targetSyntax the target syntax
     */
    public void setTargetSyntax(Syntax targetSyntax)
    {
        this.targetSyntax = targetSyntax;
    }

    /**
     * @return true if the rendering should be done in an inline context
     */
    public boolean isInline()
    {
        return this.inline;
    }

    /**
     * @param inline true if the rendering should be done in an inline context
     */
    public void setInline(boolean inline)
    {
        this.inline = inline;
    }

    /**
     * @return the decorator
     */
    public BlockAsyncRendererDecorator getDecorator()
    {
        return this.decorator;
    }

    /**
     * @param decorator the decorator
     */
    public void setDecorator(BlockAsyncRendererDecorator decorator)
    {
        this.decorator = decorator;
    }

    /**
     * @return the references involved in the rendering (they will be used to invalidate the cache when one of those
     *         entities is modified). More can be injected by the {@link Block}s trough script macros.
     */
    public Set<EntityReference> getReferences()
    {
        return this.references != null ? this.references : Collections.emptySet();
    }

    /**
     * @param references the references involved in the rendering (they will be used to invalidate the cache when one of
     *            those entities is modified). More can be injected by the {@link Block}s trough script macros.
     */
    public void setReferences(Set<EntityReference> references)
    {
        this.references = new HashSet<>(references);
    }

    /**
     * @param reference the reference to add
     */
    public void useEntity(EntityReference reference)
    {
        if (this.references == null) {
            this.references = new HashSet<>();
        }

        this.references.add(reference);
    }

    /**
     * @return the components involved in the rendering (they will be used to invalidate the cache when one of those
     *         component is modified). More can be injected by the {@link Block}s trough script macros.
     */
    public Set<ComponentRole<?>> getRoles()
    {
        return this.roles != null ? this.roles : Collections.emptySet();
    }

    /**
     * @param roles involved in the rendering (they will be used to invalidate the cache when one of those component is
     *            modified). More can be injected by the {@link Block}s trough script macros.
     */
    public void setRoles(Set<ComponentRole<?>> roles)
    {
        this.roles = roles;
    }

    /**
     * Indicate that the execution manipulate components of the passed type and the result will need to be removed from
     * the cache if any is unregistered or a new one registered.
     *
     * @param roleType the type of the component role
     */
    public void useComponent(Type roleType)
    {
        if (this.roles == null) {
            this.roles = new HashSet<>();
        }

        this.roles.add(new DefaultComponentRole<>(roleType, null));
    }

    /**
     * Indicate that the execution manipulate component wit the passed type and hint and the result will need to be
     * removed from the cache if it's registered or unregistered.
     *
     * @param roleType the type of the component role
     * @param roleHint the hint of the component
     */
    public void useComponent(Type roleType, String roleHint)
    {
        if (this.roles == null) {
            this.roles = new HashSet<>();
        }

        this.roles.add(new DefaultComponentRole<>(roleType, roleHint));
    }
}

## 内容2
### 文件路径：
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-async/xwiki-platform-rendering-async-api/src/main/java/org/xwiki/rendering/async/internal/block/DefaultBlockAsyncRenderer.java
### 代码内容：
/*
 * 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.async.internal.block;

import java.util.Collections;
import java.util.List;

import javax.inject.Inject;

import org.xwiki.component.annotation.Component;
import org.xwiki.component.descriptor.ComponentRole;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.rendering.RenderingException;
import org.xwiki.rendering.async.AsyncContext;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.CompositeBlock;
import org.xwiki.rendering.block.MetaDataBlock;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.rendering.transformation.TransformationContext;
import org.xwiki.rendering.util.ErrorBlockGenerator;
import org.xwiki.rendering.util.ParserUtils;

/**
 * Helper to execute Block based asynchronous renderer.
 *
 * @version $Id$
 * @since 10.10RC1
 */
@Component(roles = DefaultBlockAsyncRenderer.class)
public class DefaultBlockAsyncRenderer extends AbstractBlockAsyncRenderer
{
    private static final String TM_FAILEDASYNC = "rendering.async.error.failed";

    private static final ParserUtils PARSERUTILS = new ParserUtils();

    @Inject
    private AsyncContext asyncContext;

    @Inject
    private ErrorBlockGenerator errorBlockGenerator;

    private BlockAsyncRendererConfiguration configuration;

    /**
     * @param configuration the configuration of the renderer
     */
    public void initialize(BlockAsyncRendererConfiguration configuration)
    {
        this.configuration = configuration;
    }

    @Override
    public List<String> getId()
    {
        return this.configuration.getId();
    }

    @Override
    public boolean isAsyncAllowed()
    {
        return this.configuration.isAsyncAllowed();
    }

    @Override
    public boolean isCacheAllowed()
    {
        return this.configuration.isCacheAllowed();
    }

    @Override
    public boolean isInline()
    {
        return this.configuration.isInline();
    }

    @Override
    public Syntax getTargetSyntax()
    {
        return this.configuration.getTargetSyntax();
    }

    @Override
    public Block execute(boolean async, boolean cached) throws RenderingException
    {
        Block resultBlock;

        try {
            // Register the known involved references and components
            for (EntityReference reference : this.configuration.getReferences()) {
                this.asyncContext.useEntity(reference);
            }
            for (ComponentRole<?> role : this.configuration.getRoles()) {
                this.asyncContext.useComponent(role.getRoleType(), role.getRoleHint());
            }

            // Get the block to transform
            Block block = this.configuration.getBlock();

            // Make sure the source is inline if needed
            if (this.configuration.isInline()) {
                block = PARSERUTILS.convertToInline(block, true);
            }

            // Get the XDOM instance for the transformation context
            XDOM xdom = this.configuration.getXDOM();
            if (xdom == null) {
                Block rootBlock = block.getRoot();

                if (rootBlock instanceof XDOM) {
                    xdom = (XDOM) rootBlock;
                } else {
                    xdom = new XDOM(Collections.singletonList(rootBlock));
                }
            }

            ///////////////////////////////////////
            // Transformations

            resultBlock = tranform(xdom, block);
        } catch (Exception e) {
            // Display the error in the result
            resultBlock = new CompositeBlock(this.errorBlockGenerator.generateErrorBlocks(this.configuration.isInline(),
                TM_FAILEDASYNC, "Failed to execute asynchronous content", null, e));
        }

        return resultBlock;
    }

    private Block tranform(XDOM xdom, Block block) throws RenderingException
    {
        TransformationContext transformationContext =
            new TransformationContext(xdom, this.configuration.getDefaultSyntax(), false);
        transformationContext.setTargetSyntax(this.configuration.getTargetSyntax());
        transformationContext.setId(this.configuration.getTransformationId());

        transform(block, transformationContext);

        // The result is often inserted in a bigger content so we remove the XDOM around it
        if (block instanceof XDOM) {
            return new MetaDataBlock(block.getChildren(), ((XDOM) block).getMetaData());
        }

        return block;
    }
}

## 内容3
### 文件路径：
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-async/xwiki-platform-rendering-async-macro/src/main/java/org/xwiki/rendering/async/internal/AbstractExecutedContentMacro.java
### 代码内容：
/*
 * 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.async.internal;

import java.util.List;

import javax.inject.Inject;

import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.rendering.async.internal.block.BlockAsyncRendererConfiguration;
import org.xwiki.rendering.async.internal.block.BlockAsyncRendererExecutor;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.Block.Axes;
import org.xwiki.rendering.block.MacroBlock;
import org.xwiki.rendering.block.MetaDataBlock;
import org.xwiki.rendering.block.match.MetadataBlockMatcher;
import org.xwiki.rendering.listener.MetaData;
import org.xwiki.rendering.macro.AbstractMacro;
import org.xwiki.rendering.macro.Macro;
import org.xwiki.rendering.macro.MacroContentParser;
import org.xwiki.rendering.macro.descriptor.ContentDescriptor;
import org.xwiki.rendering.transformation.MacroTransformationContext;
import org.xwiki.rendering.transformation.RenderingContext;

/**
 * Base class to implement a macro which have content to execute.
 *
 * @param <P> the type of the macro parameters bean
 * @version $Id$
 * @since 14.8RC1
 * @since 14.4.5
 * @since 13.10.10
 */
public abstract class AbstractExecutedContentMacro<P> extends AbstractMacro<P>
{
    @Inject
    protected BlockAsyncRendererExecutor executor;

    @Inject
    protected MacroContentParser parser;

    @Inject
    protected DocumentAccessBridge documentAccessBridge;

    @Inject
    protected DocumentReferenceResolver<String> resolver;

    @Inject
    protected RenderingContext renderingContext;

    /**
     * Creates a new {@link Macro} instance.
     *
     * @param name the name of the macro (eg "Table Of Contents" for the TOC macro)
     * @param description string describing this macro.
     * @param contentDescriptor the {@link ContentDescriptor} describing the content of this macro.
     * @param parametersBeanClass class of the parameters bean.
     */
    protected AbstractExecutedContentMacro(String name, String description, ContentDescriptor contentDescriptor,
        Class<?> parametersBeanClass)
    {
        super(name, description, contentDescriptor, parametersBeanClass);
    }

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

    protected String getCurrentSource(MacroTransformationContext context)
    {
        String currentSource = null;

        if (context != null) {
            currentSource =
                context.getTransformationContext() != null ? context.getTransformationContext().getId() : null;

            MacroBlock currentMacroBlock = context.getCurrentMacroBlock();

            if (currentMacroBlock != null) {
                MetaDataBlock metaDataBlock =
                    currentMacroBlock.getFirstBlock(new MetadataBlockMatcher(MetaData.SOURCE), Axes.ANCESTOR_OR_SELF);

                if (metaDataBlock != null) {
                    currentSource = (String) metaDataBlock.getMetaData().getMetaData(MetaData.SOURCE);
                }
            }
        }

        return currentSource;
    }

    protected BlockAsyncRendererConfiguration createBlockAsyncRendererConfiguration(List<?> idElements, Block content,
        MacroTransformationContext context)
    {
        return createBlockAsyncRendererConfiguration(idElements, content, getCurrentSource(context), context);
    }

    protected BlockAsyncRendererConfiguration createBlockAsyncRendererConfiguration(List<?> idElements, Block content,
        String source, MacroTransformationContext context)
    {
        BlockAsyncRendererConfiguration configuration = new BlockAsyncRendererConfiguration(idElements, content);

        // Set author
        if (source != null) {
            DocumentReference sourceReference = this.resolver.resolve(source);
            configuration.setSecureReference(sourceReference, this.documentAccessBridge.getCurrentAuthorReference());

            // Invalidate the cache when the document containing the macro call is modified
            configuration.useEntity(sourceReference);
        }

        // Indicate if the result should be inline or not
        configuration.setInline(context.isInline());

        // Indicate the syntax of the content
        configuration.setDefaultSyntax(this.parser.getCurrentSyntax(context));

        // Indicate the target syntax
        configuration.setTargetSyntax(this.renderingContext.getTargetSyntax());

        // Set the transformation id
        configuration.setTransformationId(context.getTransformationContext().getId());

        return configuration;
    }
}