Zuletzt geändert von Daniel Herrmann am 2026/02/07 23:23

Zeige letzte Bearbeiter
1 {{velocity output="false"}}
2 ## Constants:
3 #set($redirectParameter = 'xredirect')
4 #set($nameOfThisDocument = 'XWiki.ConfigurableClass')
5
6
7 #**
8 * Try to determine whether a document was edited by a user who has edit right on this page. This is tricky because
9 * documents are imported with the name XWiki.XWikiGuest who has no access to anything after import.
10 *
11 * @param theDoc - Document who's editor should be checked for edit access on this document.
12 *#
13 #macro(checkDocumentSavedByAuthorizedUser, $docToCheck, $currentDoc, $hasAccess)
14 ## The system is started and the only user is XWikiGuest who has admin right but gives it up when he imports the default
15 ## documents, we are checking to see if this looks like the guest imported the document with the first import.
16 #if($docToCheck.getWiki() == $xcontext.getMainWikiName()
17 && $docToCheck.getVersion() == '1.1'
18 && $docToCheck.getCreator() != $docToCheck.getContentAuthor()
19 && $docToCheck.getContentAuthor() == 'XWiki.XWikiGuest')
20 ##
21 #set($userToCheck = $docToCheck.getCreator())
22 #else
23 #set($userToCheck = $docToCheck.getAuthor())
24 #end
25 #set ($hasAccess = $NULL)
26 #setVariable ("$hasAccess" $xwiki.hasAccessLevel('edit', $userToCheck, $currentDoc))
27 #end
28
29
30 #**
31 * Find names of documents which contain objects of the class 'XWiki.ConfigurableClass'
32 *
33 * @param $section - String - Look for apps which specify that they should be configured in this section,
34 * if null or "" then returns them for all sections.
35 * @param $globaladmin - boolean - If true then we will look for applications which should be configured globally.
36 * @param $space - String - If not looking for apps which are configured globally, then this is the space where we
37 * will look for apps in. If null or "" or if $globaladmin is true, then all spaces will be
38 * searched.
39 * @param $outputList - List - The returns from this macro will be put in this list, passing the list as a parameter
40 * a safety measure because macros can't return values.
41 *#
42 #macro(findNamesOfAppsToConfigure, $section, $globaladmin, $space, $outputList)
43 ## We keep looking in the old configureGlobally property since we do not provide a migration for it:
44 ## this choice has been made to avoid any problem during the upgrade of old wikis.
45 ## And we are doing this look in a separate query for performance reason: adding a or statement is very bad
46 ## in terms of performance. This all should be improved in the future with proper caching.
47 ##
48 #set ($fromClauseWithoutGlobal = [
49 'BaseObject as obj',
50 'StringProperty as category',
51 'IntegerProperty as sectionOrder'
52 ])
53 ##
54 #set ($fromClause = [])
55 #set ($discard = $fromClause.addAll($fromClauseWithoutGlobal))
56 #set ($discard = $fromClause.add('StringProperty as global'))
57 ##
58 #set ($fromClauseDeprecated = [])
59 #set ($discard = $fromClauseDeprecated.addAll($fromClauseWithoutGlobal))
60 #set ($discard = $fromClauseDeprecated.add('IntegerProperty as deprecatedGlobal'))
61 ##
62 #set ($whereClauseWithoutGlobal = [
63 "obj.name = doc.fullName and obj.className = :className",
64 "category.id = obj.id and category.name = 'displayInCategory'",
65 "sectionOrder.id = obj.id and sectionOrder.name = 'sectionOrder'"
66 ])
67 ##
68 #set ($whereClause = [])
69 #set ($discard = $whereClause.addAll($whereClauseWithoutGlobal))
70 #set ($discard = $whereClause.add("global.id = obj.id and global.name = 'scope' and global.value in (:global)"))
71 ##
72 #set ($whereClauseDeprecated = [])
73 #set ($discard = $whereClauseDeprecated.addAll($whereClauseWithoutGlobal))
74 #set ($discard = $whereClauseDeprecated.add("deprecatedGlobal.id = obj.id and deprecatedGlobal.name = 'configureGlobally' and deprecatedGlobal.value = :deprecatedGlobal"))
75 ##
76 #set ($params = {'className' : $nameOfThisDocument})
77 ## Order by category and section priority, leaving empty/null category at the end, because we want to read the
78 ## category meta data from the first (most important) section in each category.
79 #set ($orderByClause = [
80 "case when category.value is null or category.value = '' then 1 else 0 end",
81 'category.value',
82 'sectionOrder.value'
83 ])
84 #if ($globaladmin == true)
85 #set ($discard = $params.put('deprecatedGlobal', 1))
86 #set ($discard = $params.put('global', ['WIKI','WIKI+ALL_SPACES']))
87 #else
88 #set ($discard = $params.put('deprecatedGlobal', 0))
89 #if ("$!space" != '')
90 #set ($discard = $params.put('global', ['SPACE']))
91 #set ($discard = $whereClause.add('doc.space = :space'))
92 #set ($discard = $params.put('space', $space))
93 #else
94 #set ($discard = $params.put('global', ['ALL_SPACES','WIKI+ALL_SPACES']))
95 #end
96 #end
97 #if ("$!section" != '')
98 #set ($discard = $fromClause.add('StringProperty as section'))
99 #set ($discard = $fromClauseDeprecated.add('StringProperty as section'))
100 #set ($discard = $whereClause.add("section.id = obj.id and section.name = 'displayInSection' and section.value = :section"))
101 #set ($discard = $whereClauseDeprecated.add("section.id = obj.id and section.name = 'displayInSection' and section.value = :section"))
102 #set ($discard = $params.put('section', $section))
103 #end
104 #set ($statement = ', ' + $stringtool.join($fromClause, ', ') +
105 ' where ' + $stringtool.join($whereClause, ' and ') +
106 ' order by ' + $stringtool.join($orderByClause, ', '))
107 #set ($statementDeprecated = ', ' + $stringtool.join($fromClauseDeprecated, ', ') +
108 ' where ' + $stringtool.join($whereClauseDeprecated, ' and ') +
109 ' order by ' + $stringtool.join($orderByClause, ', '))
110 ##
111 #set ($deprecatedParams = {})
112 #set ($discard = $deprecatedParams.putAll($params))
113 #set ($discard = $deprecatedParams.remove('global'))
114 #set ($discard = $deprecatedParams.remove('space'))
115 #set ($discard = $params.remove('deprecatedGlobal'))
116 ##
117 ## We can't remove duplicates using the unique filter because the select clause will be extended with the information
118 ## needed by the order by clause. Thus we remove the duplicates after we get the results.
119 #set ($orderedSetOfAppNames = $collectiontool.orderedSet)
120 #set ($discard = $orderedSetOfAppNames.addAll($services.query.hql($statement).bindValues($params).execute()))
121 #set ($discard = $orderedSetOfAppNames.addAll($services.query.hql($statementDeprecated).bindValues($deprecatedParams).execute()))
122 #set ($discard = $outputList.addAll($orderedSetOfAppNames))
123 #if ($globaladmin == false && "$!space" != '')
124 ## If we are looking for the apps of a specific space, we should also get the one configured for all spaces.
125 ## Note that we're doing that call at the end to avoid polluting the different velocity variables during the
126 ## execution.
127 #findNamesOfAppsToConfigure($section, false, '', $outputList)
128 #end
129 #end
130
131 #**
132 * Utility for findCustomSectionsToConfigure to get an adminMenu.
133 *
134 * @param appNames (List of Strings) Name of applications to build the adminMenu from
135 * @param adminMenu the pre-filled content for the administration menu (macro is non-destructive on this parameter)
136 * @param outputList
137 *#
138 #macro(_buildAdminMenuFromNameOfApps, $appNames, $adminMenu, $outputList)
139 ## We start by copying what's already in the adminMenu.
140 ## We can't use addAll directly because we want to make a deep copy of the structure
141 #set ($discard = $outputList.addAll($jsontool.fromString($jsontool.serialize($adminMenu))))
142 ## Reset the category and section helper hashmaps so that they point to $outputlist and not $adminMenu anymore.
143 #set ($categoriesByName = {})
144 #set ($sectionsByName = {})
145 #foreach ($category in $outputList)
146 #set ($discard = $categoriesByName.put($category.id, $category))
147 #foreach ($section in $category.children)
148 #set ($discard = $sectionsByName.put($section.id, $section))
149 #end
150 #end
151 ## The $query variable is used as a basis for the URL used in sections. (search for ${query} )
152 #set ($query = "editor=$escapetool.url(${editor})")
153 #if ($editor != 'globaladmin')
154 #set ($query = $query + "&space=$escapetool.url(${currentSpace})")
155 #end
156 #foreach ($appName in $appNames)
157 ##
158 ## Get the configurable application
159 #set ($app = $xwiki.getDocument($appName))
160 ##
161 ## If getDocument returns null, then warn the user that they don't have view access to that application.
162 #if (!$app)
163 #set ($discard = $appsUserCannotView.add($appName))
164 #end
165 ##
166 #foreach ($configurableObject in $app.getObjects($nameOfThisDocument))
167 #set ($displayInCategory = $app.getValue('displayInCategory', $configurableObject))
168 ## It's OK to not specify a category, in which case the app will be added to a default one (i.e. "other").
169 #if ("$!displayInCategory" != '')
170 #if ($categoriesByName.containsKey($displayInCategory))
171 ## Add a new section in this category.
172 #set ($appCategory = $categoriesByName.get($displayInCategory))
173 #else
174 ## Add a new category.
175 #set ($key = "admin.$displayInCategory.toLowerCase()")
176 #set ($name = $displayInCategory)
177 #if ($services.localization.get($key))
178 #set ($name = $services.localization.render($key))
179 #end
180 #set ($appCategory= {
181 'id': $displayInCategory,
182 'name': $name,
183 'children': []
184 })
185 #set ($discard = $categoriesByName.put($displayInCategory, $appCategory))
186 ## Insert the category at the end for now. We'll sort the categories after we add all of them.
187 #set ($discard = $outputList.add($appCategory))
188 #end
189 #set ($categoryIcon = $app.getValue('categoryIcon', $configurableObject))
190 #if ("$!categoryIcon" != '')
191 #set ($appCategory.icon = $categoryIcon)
192 #end
193 #set ($displayBeforeCategory = $app.getValue('displayBeforeCategory', $configurableObject))
194 #if ("$!displayBeforeCategory" != '')
195 #set ($appCategory.displayBeforeCategory = $displayBeforeCategory)
196 #end
197 #else
198 #set ($appCategory = $categoriesByName.get('other'))
199 #end
200 ##
201 #set ($displayInSection = $app.getValue('displayInSection', $configurableObject))
202 ##
203 ## If there is no section specified in the object, just skip it
204 #if ("$!displayInSection" == '')
205 ## Skip, bad object
206 ## If there is no section for this configurable or if the section cannot be edited, then check if the
207 ## application can be edited by the current user, if so then we display the icon from the current app and
208 ## don't display any message to tell the user they can't edit that section.
209 #elseif ($sectionsByName.containsKey($displayInSection))
210 #set ($appSection = $sectionsByName.get($displayInSection))
211 #set ($appSection.configurable = true)
212 #set ($newSection = false)
213 #else
214 ##
215 ## If there is no section for this configurable, then we will have to add one.
216 #set ($key = "admin.$displayInSection.toLowerCase()")
217 #set ($name = $displayInSection)
218 #if ($services.localization.get($key))
219 #set ($name = $services.localization.render($key))
220 #end
221 #set ($appSection = {
222 'id': $displayInSection,
223 'name': $name,
224 'url': $xwiki.getURL($currentDoc, $adminAction, "${query}&section=$escapetool.url($displayInSection)"),
225 'configurable': true,
226 'order': $configurableObject.getValue('sectionOrder')
227 })
228 #if ($app.getValue('configureGlobally', $configurableObject) != 1)
229 #set ($appSection.perSpace = true)
230 #end
231 #set ($key = "admin.${displayInSection.toLowerCase()}.description")
232 #if ($services.localization.get($key))
233 #set ($appSection.description = $services.localization.render($key))
234 #end
235 #set ($discard = $sectionsByName.put($displayInSection, $appSection))
236 #set ($discard = $appCategory.children.add($appSection))
237 #set ($newSection = true)
238 #end
239 ##
240 ## If an attachment by the filename iconAttachment exists and is an image
241 #set ($attachment = $app.getAttachment("$!app.getValue('iconAttachment', $configurableObject)"))
242 #if ($attachment && $attachment.isImage())
243 ## Set the icon for this section as the attachment URL.
244 #set ($appSection.iconReference = "${appName}@$attachment.getFilename()")
245 #elseif (!$appSection.iconReference)
246 #set ($appSection.iconReference = 'XWiki.ConfigurableClass@DefaultAdminSectionIcon.png')
247 #end
248 ##
249 ## If the user doesn't have edit access to the application, we want to show a message on the icon
250 #if (!$xcontext.hasAccessLevel('edit', $app.getFullName()) && $newSection)
251 #set ($appSection.readOnly = true)
252 #elseif ($xcontext.hasAccessLevel('edit', $app.getFullName()) && $appSection.readOnly)
253 #set ($appSection.readOnly = false)
254 #end
255 #end## Foreach configurable object in this app.
256 #end## Foreach application which is configurable.
257 #end
258
259 #**
260 * Computes the scores of categories and sorts the list. The first category will be the one with the highest score.
261 *
262 * @param completeOrderMenu the list of categories, containing all of the necessary ordering information;
263 * This macro is destructive on $completeOrderMenu.
264 *#
265 #macro(_computeScores, $completeOrderMenu)
266 ## Initialize scores
267 #foreach ($category in $completeOrderMenu)
268 #set ($category.score = 0)
269 #end
270 ## Compute scores
271 #foreach ($round in [1..$completeOrderMenu.size()])
272 #set ($scoreChanged = false)
273 #foreach ($category in $completeOrderMenu)
274 #if ($category.displayBeforeCategory)
275 #set ($newScore = $categoriesByName.get($category.displayBeforeCategory).score + 1)
276 #if ($newScore && $newScore > $category.score)
277 #set ($category.score = $newScore)
278 #set ($scoreChanged = true)
279 #end
280 #end
281 #end
282 #if (!$scoreChanged)
283 #break
284 #end
285 #end
286 #end
287
288 #**
289 * Utility macro to compute the full order of categories, which is contained sparsely through xobjects
290 * that are not always retrieved when building the menu.
291 *
292 * @param adminMenu the list of categories to use in the menu (macro is non-destructive on this parameter)
293 * @param outputList the full list of categories, in order. Even categories without any child will be returned.
294 *#
295 #macro(_getCategoriesInOrder, $adminMenu, $outputList)
296 #set ($params = {
297 'className' : $nameOfThisDocument,
298 'global': ['WIKI','WIKI+ALL_SPACES']
299 })
300 #set ($appNames = [])
301 #set ($discard = $appNames.addAll($services.query.hql($statement).bindValues($params).execute()))
302 ## $completeOrderMenu contains a menu that contains all the information needed for sorting, no matter what.
303 #set ($completeOrderMenu = [])
304 #_buildAdminMenuFromNameOfApps($appNames, $adminMenu, $completeOrderMenu)
305 ## We use $completeOrderMenu to set the score of each category.
306 #_computeScores($completeOrderMenu)
307 #set ($completeOrderMenu = $collectiontool.sort($completeOrderMenu, 'score:desc'))
308 ## Write the now sorted $completeOrderMenu to the output variable.
309 #set ($discard = $outputList.addAll($completeOrderMenu))
310 #end
311
312 #**
313 * Augment the $adminMenu variable with all $nameOfThisDocument (i.e. XWiki.ConfigurableClass)
314 * XObjects found on this wiki.
315 *
316 * @param adminMenu the basis of the menu on which to add on (the macro makes hard to reverse changes on $adminMenu)
317 *#
318 #macro(findCustomSectionsToConfigure $adminMenu)
319 #set ($appNames = [])
320 #set ($global = ($editor == 'globaladmin'))
321 #findNamesOfAppsToConfigure('', $global, $currentSpace, $appNames)
322 ##
323 ## $completedAdminMenu contains most of the info needed to build the administration menu.
324 #set ($completedAdminMenu = [])
325 #_buildAdminMenuFromNameOfApps($appNames, $adminMenu, $completedAdminMenu)
326 #set ($adminMenu = [])
327 #set ($discard = $adminMenu.addAll($completedAdminMenu))
328 ## Sort the categories
329 ## The information in $adminMenu is sometimes not enough to create a full order
330 ## in those case we need to retrieve more information to build the full order.
331 ## $categoriesFullOrder contains all the categories and their associated ordering information
332 #if (!$global)
333 #set ($categoriesFullOrder = [])
334 #_getCategoriesInOrder($adminMenu, $categoriesFullOrder)
335 ## Once it's retrieved, we use this total order on the categories in contained $adminMenu.
336 #set ($sortedAdminMenu = [])
337 #foreach ($orderedCategory in $categoriesFullOrder)
338 #foreach ($menuCategory in $adminMenu)
339 #if ($orderedCategory.id == $menuCategory.id)
340 #set ($discard = $sortedAdminMenu.add($menuCategory))
341 #break
342 #end
343 #end
344 #end
345 #set ($adminMenu = [])
346 #set ($discard = $adminMenu.addAll($sortedAdminMenu))
347 #else
348 ## We're in the case where all the categories and sections are already in $adminMenu
349 ## We can easily figure out an order
350 #_computeScores($adminMenu)
351 #set ($adminMenu = $collectiontool.sort($adminMenu, 'score:desc'))
352 #end
353 #end
354
355
356 #**
357 * Show the heading for configuration for a given application.
358 *
359 * @param appName (String) Name of the application to show configuration heading for.
360 * @param headingAlreadyShowing (boolean) If true then we don't make another heading. Otherwise it is set to true.
361 *#
362 #macro(showHeading, $appName, $headingAlreadyShowing)
363 #if(!$headingAlreadyShowing)
364 #set($headingAlreadyShowing = true)
365 #set($escapedAppName = $services.rendering.escape($appName, 'xwiki/2.1'))
366 #set($doubleEscapedAppName = $services.rendering.escape($escapedAppName, 'xwiki/2.1'))
367
368 == {{translation key="admin.customize"/}} [[$doubleEscapedAppName>>$escapedAppName]]: ==
369 #end
370 #end
371
372 #define($formHtml)
373 #if ($objClass.getPropertyNames().size() > 0)
374 <dl>
375 #foreach($propName in $objClass.getPropertyNames())
376 #if($propertiesToShow.size() > 0 && !$propertiesToShow.contains($propName))
377 ## Silently skip over this property.
378 #else
379 #set($hintKey = "${obj.xWikiClass.name}_${propName}.hint")
380 #set($hint = $services.localization.render($hintKey))
381 #if($hint == $hintKey)
382 #set($hint = $NULL)
383 #end
384 ##
385 ## Further processing of the field display HTML is needed.
386 ## Step 1: Strip <pre> tags which $obj.display inserts, this won't affect content because it's escaped.
387 #set($out = $obj.display($propName, 'edit').replaceAll('<[/]?+pre>', ''))
388 ## Step 2: Select only content between first < and last > because $obj.display inserts html macros.
389 ## Careful not to remove html macros from the content because they are not escaped!
390 #set ($out = $out.substring($out.indexOf('<'), $mathtool.add(1, $out.lastIndexOf('>'))))
391 ## Step 3: Prepend app name to all ID and FOR attributes to prevent id collision with multiple apps on one page.
392 #set ($oldId = "$objClass.getName()_$obj.getNumber()_$propName")
393 #set ($newId = "${escapedAppName}_$oldId")
394 #set ($out = $out.replaceAll(" (id|for)=('|"")$regextool.quote($oldId)",
395 " ${escapetool.d}1=${escapetool.d}2$regextool.quoteReplacement($newId)"))
396 ## App Name is prepended to for= to make label work with id which is modified to prevent collisions.
397 <dt><label#if ($out.matches("(?s).*id=['""]${newId}['""].*")) for="${newId}"#end>##
398 #if ($out.indexOf('type=''checkbox''') != -1 && $out.indexOf('class="xwiki-form-listclass"') == -1)
399 $out ##
400 #set ($out = '')
401 #end
402 $escapetool.xml($app.displayPrettyName($propName, $obj))
403 </label>
404 #if($linkPrefix != '')
405 <a href="$escapetool.xml($linkPrefix + $propName)" class="xHelp" title="$services.localization.render('admin.documentation')">$services.localization.render('admin.documentation')</a>
406 #end
407 #if ($hint)
408 <span class="xHint">$hint</span>
409 #end
410 </dt>
411 #if ($out != '')
412 <dd>$out</dd>
413 #else
414 ## We always display a dd element to avoid having a last dt element alone, which would lead to an invalid html.
415 <dd class="hidden">$out</dd>
416 #end
417 #end## If property is in propertiesToShow
418 #end## Foreach property in this class
419 </dl>
420 #end
421 #end## define $formHtml
422 {{/velocity}}