codeconsole commented on code in PR #15987:
URL: https://github.com/apache/grails-core/pull/15987#discussion_r3608976206


##########
grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy:
##########
@@ -963,39 +965,145 @@ class FormTagLib implements ApplicationContextAware, 
InitializingBean, TagLibrar
     }
 
     /**
-     * A helper tag for creating locale selects.<br/>
+     * A helper tag for locale selection.<br/>
      *
-     * eg. &lt;g:localeSelect name="myLocale" value="${locale}" /&gt;
+     * <p>With no body it renders a control: a native {@code <select>} by 
default, or a plain list of
+     * {@code <a>} links with {@code type="links"}. With a body it becomes an 
iterating tag &mdash; it
+     * resolves the locales once and renders the body for each, exposing a 
per-locale model under the
+     * {@code var} attribute so the caller supplies its own markup (a 
Bootstrap dropdown, a footer
+     * list, etc.). The model exposes: {@code locale}, {@code tag} 
(BCP&#8209;47), {@code code}
+     * ({@code language_COUNTRY}), {@code autonym} (the name in its own 
language), {@code name} (the
+     * name in the display locale), {@code label}, {@code active} (matches the 
current locale),
+     * {@code default} (matches the configured default) and {@code index}.
      *
-     * @emptyTag
+     * eg. &lt;g:localeSelect name="myLocale" value="${locale}" 
labelType="autonym" /&gt;
      *
-     * @attr name REQUIRED The name of the select
-     * @attr value The set locale, defaults to the current request locale if 
not specified
-     * @attr locale The locale to use for formatting the locale names. 
Defaults to the current request locale and then the system default locale if 
not specified
+     * @attr name The name of the select (select mode)
+     * @attr value The selected locale, defaults to the current request locale 
if not specified
      * @attr available If <code>true</code>, list only the locales the 
application is translated into
      * (those with a <code>messages_*.properties</code> bundle, as published 
to the servlet context by
      * the i18n plugin) instead of every locale the JVM knows about. Defaults 
to <code>false</code>.
+     * @attr type <code>select</code> (default) or <code>links</code>. Ignored 
when a body is supplied.
+     * @attr labelType The option/link label: <code>autonym</code> (each 
locale in its own language),
+     * <code>name</code> (in the display locale), <code>both</code>, or 
omitted for the legacy
+     * <code>"language, [COUNTRY,] name"</code> label.
+     * @attr sort If <code>true</code>, order the locales by their label using 
a locale-independent collator.
+     * @attr tags If <code>true</code>, option/link keys are BCP&#8209;47 
language tags (<code>en-US</code>)
+     * rather than the legacy <code>en_US</code> form.
+     * @attr pinDefault If <code>true</code> (body mode), the configured 
default locale is emitted first.
+     * @attr param The request parameter name used for <code>links</code>-mode 
hrefs. Defaults to <code>lang</code>.
+     * @attr var Enables body mode: the name of the per-locale model variable 
exposed to the body.
      */
-    def localeSelect(Map attrs) {
-        def availableAttr = attrs.remove('available')
-        boolean availableOnly = availableAttr != null && 
Boolean.valueOf(availableAttr.toString())
+    def localeSelect(Map attrs, Closure body) {
+        boolean availableOnly = 
Boolean.valueOf(attrs.remove('available')?.toString())
+        List locales
         if (availableOnly) {
-            def availableLocales = 
request.servletContext?.getAttribute('availableLocales')
-            attrs.from = availableLocales ?: [RCU.getLocale(request)]
+            def published = 
request.servletContext?.getAttribute('availableLocales')
+            locales = published ? new ArrayList(published) : 
[RCU.getLocale(request)]
         }
         else {
-            attrs.from = Locale.getAvailableLocales()
+            locales = Locale.getAvailableLocales() as List
         }
-        attrs.value = (attrs.value ?: RCU.getLocale(request))?.toString()
-        // set the key as a closure that formats the locale
-        attrs.optionKey = { it.country ? "${it.language}_${it.country}" : 
it.language }
-        // set the option value as a closure that formats the locale for 
display
-        attrs.optionValue = { it.country ? "${it.language}, ${it.country},  
${it.displayName}" : "${it.language}, ${it.displayName}" }
 
-        // use generic select
+        Locale current = RCU.getLocale(request)
+        def valueAttr = attrs.value
+        if (valueAttr instanceof Locale) {
+            current = valueAttr
+        }
+        else if (valueAttr) {
+            current = StringUtils.parseLocale(valueAttr.toString()) ?: current
+        }
+
+        boolean useTags = Boolean.valueOf(attrs.remove('tags')?.toString())
+        Closure label = localeLabel(attrs.remove('labelType'), current)
+
+        if (Boolean.valueOf(attrs.remove('sort')?.toString())) {
+            Collator collator = Collator.getInstance(Locale.ROOT)
+            locales = locales.sort(false) { a, b -> 
collator.compare(label(a).toString(), label(b).toString()) }
+        }
+
+        String varName = attrs.remove('var')
+        if (varName) {
+            boolean pinDefault = 
Boolean.valueOf(attrs.remove('pinDefault')?.toString())
+            Locale defaultLocale = configuredDefaultLocale()
+            if (pinDefault) {
+                Locale pinned = locales.find { it.language == 
defaultLocale.language }
+                if (pinned) {
+                    locales = [pinned] + locales.findAll { it.language != 
defaultLocale.language }

Review Comment:
   Fixed in 47e8ebe933 exactly as suggested: the pin resolves the exact 
configured variant (with a language-only fallback) and reorders with `[pinned] 
+ remainder`, so `grails.i18n.default.locale=pt_BR` pins `pt_BR` while `pt_PT` 
stays in the selector. The `default` flag elects the same single entry. Covered 
by the new `LocaleSelectPinnedVariantSpec`.



##########
grails-forge/grails-forge-core/src/main/resources/gsp/index.gsp:
##########
@@ -281,26 +371,784 @@
                                                         </div>
                                                     </div>
                                                 </g:link>
+                                                </g:if>
+                                                <g:else>
+                                                <form 
action="${controllerUrl}" method="post" class="m-0 d-flex align-items-center 
gap-3 min-w-0 flex-grow-1">
+                                                    <button type="submit" 
class="btn p-0 border-0 d-flex align-items-center gap-3 text-start min-w-0 
w-100">
+                                                        <span class="min-w-0">
+                                                            <span 
class="fw-semibold text-body text-truncate d-block">
+                                                                ${simpleName}
+                                                            </span>
+                                                        </span>
+                                                    </button>
+                                                </form>
+                                                </g:else>
 
-                                                <a href="${controllerUrl}"
-                                                   class="small link-primary 
link-offset-2 link-underline-opacity-0 link-underline-opacity-75-hover 
flex-shrink-0">
-                                                    ${controllerUrl}
-                                                </a>
+                                                <div class="d-flex 
align-items-center gap-2 flex-shrink-0 ms-auto">
+                                                    <g:if test="${hasShow}">
+                                                        <%-- Falls back to GET 
/controller/show?id=… when JS is
+                                                             off; welcome.js 
upgrades it to the /show/{id} path. --%>
+                                                        <form class="show-jump 
d-lg-none" action="${showBase}" method="get" data-show-base="${showBase}">
+                                                            <%-- No inputmode: 
ids may be Long, String, or ObjectId, and a
+                                                                 numeric 
keypad would lock out non-digit ids on mobile. --%>
+                                                            <input type="text" 
name="id" autocomplete="off"
+                                                                   
class="form-control form-control-sm show-jump-input"
+                                                                   
placeholder="${message(code: 'welcome.show.placeholder')}"
+                                                                   
data-focus-placeholder="${message(code: 'welcome.show.hint')}"
+                                                                   
aria-label="${message(code: 'welcome.show.aria', args: [simpleName])}">
+                                                            <button 
type="submit" class="btn btn-sm btn-primary show-jump-go">
+                                                                <g:message 
code="welcome.show.label"/>
+                                                            </button>
+                                                        </form>
+                                                    </g:if>
+
+                                                    <g:if test="${ctrlGetOk}">
+                                                    <a href="${controllerUrl}"
+                                                       class="small 
link-primary link-offset-2 link-underline-opacity-0 
link-underline-opacity-75-hover">
+                                                        ${controllerUrl}
+                                                    </a>
+                                                    </g:if>
+                                                    <g:else>
+                                                    <span class="badge 
bg-body-tertiary text-body-secondary border">
+                                                        
${[ctrlMethods].flatten()*.toString()*.toUpperCase().join(' / ')}
+                                                    </span>
+                                                    <span class="small 
text-body-secondary">${controllerUrl}</span>
+                                                    </g:else>
+                                                </div>
                                             </div>
                                         </li>
                                     </g:each>
                                 </ul>
                             </div>
                         </g:each>
                         </div>
-                        <p id="controllers-empty" class="small 
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+                        <g:if test="${numControllers != 0}">
+                            <p id="controllers-empty" class="small 
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+                        </g:if>
+                        <g:else>
+                            <p class="small text-body-secondary 
mb-0"><g:message code="welcome.artefacts.none"/></p>
+                        </g:else>
+                        </div>
+
+                        <div data-switch-for="domains" class="d-none">
+                        <div id="domains-list">
+                            <ul class="list-group list-group-flush">
+                                <g:each var="d" 
in="${grailsApplication.domainClasses.toList().sort { it.shortName }}">
+                                    <li class="list-group-item px-2 d-flex 
align-items-center justify-content-between gap-2" data-name="${d.shortName}">
+                                        <span class="fw-semibold text-body 
text-truncate">${d.shortName}</span>
+                                        <span class="small text-body-secondary 
text-truncate">${d.packageName}</span>
+                                    </li>
+                                </g:each>
+                            </ul>
+                        </div>
+                        <g:if test="${numDomains != 0}">
+                            <p id="domains-empty" class="small 
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+                        </g:if>
+                        <g:else>
+                            <p class="small text-body-secondary 
mb-0"><g:message code="welcome.artefacts.none"/></p>
+                        </g:else>
+                        </div>
+
+                        <div data-switch-for="services" class="d-none">
+                        <div id="services-list">
+                            <ul class="list-group list-group-flush">
+                                <g:each var="s" 
in="${grailsApplication.serviceClasses.toList().sort { it.shortName }}">
+                                    <li class="list-group-item px-2 d-flex 
align-items-center justify-content-between gap-2" data-name="${s.shortName}">
+                                        <span class="fw-semibold text-body 
text-truncate">${s.shortName}</span>
+                                        <span class="small text-body-secondary 
text-truncate">${s.packageName}</span>
+                                    </li>
+                                </g:each>
+                            </ul>
+                        </div>
+                        <g:if test="${numServices != 0}">
+                            <p id="services-empty" class="small 
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+                        </g:if>
+                        <g:else>
+                            <p class="small text-body-secondary 
mb-0"><g:message code="welcome.artefacts.none"/></p>
+                        </g:else>
+                        </div>
+
+                        <div data-switch-for="taglibs" class="d-none">
+                        <div id="taglibs-list">
+                            <ul class="list-group list-group-flush">
+                                <g:each var="t" 
in="${grailsApplication.tagLibClasses.toList().sort { it.shortName }}">
+                                    <li class="list-group-item px-2 d-flex 
align-items-center justify-content-between gap-2" data-name="${t.shortName}">
+                                        <span class="fw-semibold text-body 
text-truncate">${t.shortName}</span>
+                                        <span class="small 
text-truncate"><code>${t.namespace}</code> <span 
class="text-body-secondary">${t.packageName}</span></span>
+                                    </li>
+                                </g:each>
+                            </ul>
+                        </div>
+                        <g:if test="${numTagLibs != 0}">
+                            <p id="taglibs-empty" class="small 
text-body-secondary d-none mb-0"><g:message code="welcome.filter.none"/></p>
+                        </g:if>
+                        <g:else>
+                            <p class="small text-body-secondary 
mb-0"><g:message code="welcome.artefacts.none"/></p>
+                        </g:else>
+                        </div>
+                    </div>
+                </div>
+
+                <%-- RUNTIME INTERNALS: listeners, data binding, mime handling 
and
+                     the request filter pipeline resolved from the running 
application
+                     context; reuses the artefacts card's data-switch-scope 
pattern. --%>
+                <g:set var="appListeners"
+                       
value="${applicationContext.applicationListeners.toList()
+                               .collect { l -> [name: (l.getClass().simpleName 
?: l.getClass().name.tokenize('.').last()),
+                                                packageName: 
(l.getClass().package?.name ?: ''),
+                                                detail: l.toString()] }
+                               .sort { a, b -> (a.name.toLowerCase() <=> 
b.name.toLowerCase()) ?: (a.detail <=> b.detail) }}"/>
+                <g:set var="bindingGroups"
+                       value="${[[code: 'welcome.binding.value', beans: 
applicationContext.getBeansOfType(grails.databinding.converters.ValueConverter)],
+                                 [code: 'welcome.binding.formatted', beans: 
applicationContext.getBeansOfType(grails.databinding.converters.FormattedValueConverter)],
+                                 [code: 'welcome.binding.structured', beans: 
applicationContext.getBeansOfType(grails.databinding.TypedStructuredBindingEditor)],
+                                 [code: 'welcome.binding.listeners', beans: 
applicationContext.getBeansOfType(grails.databinding.events.DataBindingListener)]]}"/>
+                <g:set var="numBindingBeans" value="${bindingGroups.sum { g -> 
g.beans.size() } ?: 0}"/>
+                <g:set var="mimeTypeProviders"
+                       
value="${applicationContext.getBeansOfType(grails.web.mime.MimeTypeProvider)
+                               .entrySet().toList().sort { 
it.key.toLowerCase() }}"/>
+                <%-- The filters still on the call stack ARE this request's 
pipeline, in
+                     execution order: walk the reversed stack, keep Filter 
classes, collapse
+                     the extra frames a filter contributes through its 
abstract bases, and
+                     number what remains. No registry can report this actual 
order. --%>
+                <g:set var="requestFilters"
+                       
value="${Thread.currentThread().stackTrace.toList().reverse()
+                               .findResults { ste ->
+                                   def cls = null
+                                   try { cls = Class.forName(ste.className, 
false, Thread.currentThread().contextClassLoader) } catch (Throwable ignored) { 
}
+                                   (cls != null && 
jakarta.servlet.Filter.isAssignableFrom(cls)) ? cls : null
+                               }
+                               .inject([]) { acc, cls ->
+                                   def prev = acc ? acc[-1] : null
+                                   if (prev == cls) { return acc }
+                                   if (prev != null && 
prev.isAssignableFrom(cls)) { acc[-1] = cls; return acc }
+                                   if (prev != null && 
cls.isAssignableFrom(prev)) { return acc }
+                                   acc << cls
+                               }
+                               .unique()}"/>
+                <g:set var="filterRegistrations"
+                       
value="${applicationContext.getBeansOfType(org.springframework.boot.web.servlet.FilterRegistrationBean)
+                               .entrySet().toList().sort { it.value.order }}"/>
+                <g:set var="filterChainProxyType"
+                       
value="${ClassUtils.isPresent('org.springframework.security.web.FilterChainProxy',
 null) ? 
ClassUtils.forName('org.springframework.security.web.FilterChainProxy', null) : 
null}"/>
+                <g:set var="securityFilterChains"

Review Comment:
   Agreed — as of 47e8ebe933 the whole diagnostic block renders only when 
`Environment.current == Environment.DEVELOPMENT`: the runtime-internals card 
(listeners, binding, mime providers, the filter pipeline, registrations, 
security chains and the URL-mappings resolver) plus the Mime Types and 
Datastores cards, with their `g:set` computations inside the gate so none of 
the introspection even executes in production. Applied to both template copies; 
the artefact/plugins/actuator cards that the previous welcome page also showed 
remain ungated.



##########
grails-spring-security/ui/plugin/grails-app/taglib/grails/plugin/springsecurity/ui/SecurityUiTagLib.groovy:
##########
@@ -705,59 +700,59 @@ class SecurityUiTagLib {
         }
 
         def out = getOut()
-        out << """<a id="$elementId" """
+        out << """<button type="submit" id="$elementId" class="btn 
btn-primary" """

Review Comment:
   Fixed in 47e8ebe933 — `submitButton`/`linkButton` now encode the element id 
and text, and `writeRemainingAttributes` encodes every passed-through attribute 
key and value (the stale `TODO encodeAsHTML` is gone). Added 
`SecurityUiTagLibSpec` feeding markup-sensitive values through both tags to pin 
the encoding.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to