From 8170e239de1cbc6e850b6406d4672184664951bb Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Tue, 27 Oct 2026 15:27:42 +0100 Subject: [PATCH 1/3] JSPWIKI-1317 Follow the dark colour scheme of the system when no skin was chosen The default template now also loads the dark skin, under a prefers-color-scheme media query, when the active skin is the default one and the user has not picked a skin. Preferences records which preferences come from the user's cookie, so a user who chose the default skin on purpose keeps it. --- .../apache/wiki/preferences/Preferences.java | 19 ++++- .../wiki/preferences/PreferencesTest.java | 75 +++++++++++++++++++ .../webapp/templates/default/commonheader.jsp | 5 ++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 jspwiki-main/src/test/java/org/apache/wiki/preferences/PreferencesTest.java diff --git a/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java b/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java index 5e6b0fa290..c3b99fb254 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java @@ -36,11 +36,13 @@ Licensed to the Apache Software Foundation (ASF) under one import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Properties; import java.util.ResourceBundle; +import java.util.Set; import java.util.TimeZone; @@ -51,6 +53,9 @@ public class Preferences extends HashMap< String,String > { private static final long serialVersionUID = 1L; + /** The names of the preferences whose value comes from the user's cookie, and not from the defaults. */ + private final Set< String > setByUser = new HashSet<>(); + /** * The name under which a Preferences object is stored in the HttpSession. Its value is {@value}. */ @@ -128,7 +133,7 @@ public static void reloadPreferences( final PageContext pageContext ) { * @param request * @param prefs The default hashmap of preferences */ - private static void parseJSONPreferences( final HttpServletRequest request, final Preferences prefs ) { + static void parseJSONPreferences( final HttpServletRequest request, final Preferences prefs ) { final String prefVal = TextUtil.urlDecodeUTF8( HttpUtil.retrieveCookieValue( request, COOKIE_USER_PREFS_NAME ) ); if( prefVal != null ) { // Convert prefVal JSON to a generic hashmap @@ -139,11 +144,23 @@ private static void parseJSONPreferences( final HttpServletRequest request, fina final Object value = map.get( key ); if( value != null ) { prefs.put( key, value.toString() ); + prefs.setByUser.add( key ); } } } } + /** + * Tells whether the user chose the value of a preference, as opposed to getting the default one. + * + * @param name the name of the preference. + * @return {@code true} if the value comes from the user's cookie. + * @since 3.0.1 + */ + public boolean isSetByUser( final String name ) { + return setByUser.contains( name ); + } + /** * Returns a preference value programmatically. * FIXME diff --git a/jspwiki-main/src/test/java/org/apache/wiki/preferences/PreferencesTest.java b/jspwiki-main/src/test/java/org/apache/wiki/preferences/PreferencesTest.java new file mode 100644 index 0000000000..6727f26210 --- /dev/null +++ b/jspwiki-main/src/test/java/org/apache/wiki/preferences/PreferencesTest.java @@ -0,0 +1,75 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.wiki.preferences; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + + +class PreferencesTest { + + private HttpServletRequest requestWithCookie( final String json ) { + final HttpServletRequest request = Mockito.mock( HttpServletRequest.class ); + final Cookie cookie = new Cookie( Preferences.COOKIE_USER_PREFS_NAME, URLEncoder.encode( json, StandardCharsets.UTF_8 ) ); + Mockito.doReturn( new Cookie[] { cookie } ).when( request ).getCookies(); + return request; + } + + @Test + void testNothingIsSetByUserWithoutCookie() { + final Preferences prefs = new Preferences(); + prefs.put( "SkinName", "default" ); + + Preferences.parseJSONPreferences( Mockito.mock( HttpServletRequest.class ), prefs ); + + Assertions.assertFalse( prefs.isSetByUser( "SkinName" ) ); + Assertions.assertEquals( "default", prefs.get( "SkinName" ) ); + } + + @Test + void testSkinChosenAsTheDefaultOneIsSetByUser() { + final Preferences prefs = new Preferences(); + prefs.put( "SkinName", "default" ); + + Preferences.parseJSONPreferences( requestWithCookie( "{\"SkinName\":\"default\",\"Layout\":\"fluid\"}" ), prefs ); + + Assertions.assertTrue( prefs.isSetByUser( "SkinName" ) ); + Assertions.assertTrue( prefs.isSetByUser( "Layout" ) ); + } + + @Test + void testPreferenceMissingFromCookieIsNotSetByUser() { + final Preferences prefs = new Preferences(); + prefs.put( "SkinName", "default" ); + prefs.put( "Layout", "fluid" ); + + Preferences.parseJSONPreferences( requestWithCookie( "{\"Layout\":\"fixed\"}" ), prefs ); + + Assertions.assertEquals( "fixed", prefs.get( "Layout" ) ); + Assertions.assertTrue( prefs.isSetByUser( "Layout" ) ); + Assertions.assertFalse( prefs.isSetByUser( "SkinName" ) ); + } + +} diff --git a/jspwiki-war/src/main/webapp/templates/default/commonheader.jsp b/jspwiki-war/src/main/webapp/templates/default/commonheader.jsp index fd78354ee8..d5c331f517 100644 --- a/jspwiki-war/src/main/webapp/templates/default/commonheader.jsp +++ b/jspwiki-war/src/main/webapp/templates/default/commonheader.jsp @@ -152,5 +152,10 @@ String.I18N.PREFIX = "javascript."; +<%-- Without a skin of their own choosing, users whose system asks for a dark colour scheme get the dark skin --%> + + + \ No newline at end of file From f523b30bc6282d34eca684fdd2b41009467a47a6 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Tue, 27 Oct 2026 15:27:42 +0100 Subject: [PATCH 2/3] JSPWIKI-1317 Follow the dark colour scheme of the system when no skin was chosen The default template now also loads the dark skin, under a prefers-color-scheme media query, when the active skin is the default one and the user has not picked a skin. Preferences records which preferences come from the user's cookie, so a user who chose the default skin on purpose keeps it. --- .../apache/wiki/preferences/Preferences.java | 19 ++++- .../wiki/preferences/PreferencesTest.java | 75 +++++++++++++++++++ .../webapp/templates/default/commonheader.jsp | 5 ++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 jspwiki-main/src/test/java/org/apache/wiki/preferences/PreferencesTest.java diff --git a/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java b/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java index 5e6b0fa290..c3b99fb254 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/preferences/Preferences.java @@ -36,11 +36,13 @@ Licensed to the Apache Software Foundation (ASF) under one import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Properties; import java.util.ResourceBundle; +import java.util.Set; import java.util.TimeZone; @@ -51,6 +53,9 @@ public class Preferences extends HashMap< String,String > { private static final long serialVersionUID = 1L; + /** The names of the preferences whose value comes from the user's cookie, and not from the defaults. */ + private final Set< String > setByUser = new HashSet<>(); + /** * The name under which a Preferences object is stored in the HttpSession. Its value is {@value}. */ @@ -128,7 +133,7 @@ public static void reloadPreferences( final PageContext pageContext ) { * @param request * @param prefs The default hashmap of preferences */ - private static void parseJSONPreferences( final HttpServletRequest request, final Preferences prefs ) { + static void parseJSONPreferences( final HttpServletRequest request, final Preferences prefs ) { final String prefVal = TextUtil.urlDecodeUTF8( HttpUtil.retrieveCookieValue( request, COOKIE_USER_PREFS_NAME ) ); if( prefVal != null ) { // Convert prefVal JSON to a generic hashmap @@ -139,11 +144,23 @@ private static void parseJSONPreferences( final HttpServletRequest request, fina final Object value = map.get( key ); if( value != null ) { prefs.put( key, value.toString() ); + prefs.setByUser.add( key ); } } } } + /** + * Tells whether the user chose the value of a preference, as opposed to getting the default one. + * + * @param name the name of the preference. + * @return {@code true} if the value comes from the user's cookie. + * @since 3.0.1 + */ + public boolean isSetByUser( final String name ) { + return setByUser.contains( name ); + } + /** * Returns a preference value programmatically. * FIXME diff --git a/jspwiki-main/src/test/java/org/apache/wiki/preferences/PreferencesTest.java b/jspwiki-main/src/test/java/org/apache/wiki/preferences/PreferencesTest.java new file mode 100644 index 0000000000..6727f26210 --- /dev/null +++ b/jspwiki-main/src/test/java/org/apache/wiki/preferences/PreferencesTest.java @@ -0,0 +1,75 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +package org.apache.wiki.preferences; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + + +class PreferencesTest { + + private HttpServletRequest requestWithCookie( final String json ) { + final HttpServletRequest request = Mockito.mock( HttpServletRequest.class ); + final Cookie cookie = new Cookie( Preferences.COOKIE_USER_PREFS_NAME, URLEncoder.encode( json, StandardCharsets.UTF_8 ) ); + Mockito.doReturn( new Cookie[] { cookie } ).when( request ).getCookies(); + return request; + } + + @Test + void testNothingIsSetByUserWithoutCookie() { + final Preferences prefs = new Preferences(); + prefs.put( "SkinName", "default" ); + + Preferences.parseJSONPreferences( Mockito.mock( HttpServletRequest.class ), prefs ); + + Assertions.assertFalse( prefs.isSetByUser( "SkinName" ) ); + Assertions.assertEquals( "default", prefs.get( "SkinName" ) ); + } + + @Test + void testSkinChosenAsTheDefaultOneIsSetByUser() { + final Preferences prefs = new Preferences(); + prefs.put( "SkinName", "default" ); + + Preferences.parseJSONPreferences( requestWithCookie( "{\"SkinName\":\"default\",\"Layout\":\"fluid\"}" ), prefs ); + + Assertions.assertTrue( prefs.isSetByUser( "SkinName" ) ); + Assertions.assertTrue( prefs.isSetByUser( "Layout" ) ); + } + + @Test + void testPreferenceMissingFromCookieIsNotSetByUser() { + final Preferences prefs = new Preferences(); + prefs.put( "SkinName", "default" ); + prefs.put( "Layout", "fluid" ); + + Preferences.parseJSONPreferences( requestWithCookie( "{\"Layout\":\"fixed\"}" ), prefs ); + + Assertions.assertEquals( "fixed", prefs.get( "Layout" ) ); + Assertions.assertTrue( prefs.isSetByUser( "Layout" ) ); + Assertions.assertFalse( prefs.isSetByUser( "SkinName" ) ); + } + +} diff --git a/jspwiki-war/src/main/webapp/templates/default/commonheader.jsp b/jspwiki-war/src/main/webapp/templates/default/commonheader.jsp index fd78354ee8..d5c331f517 100644 --- a/jspwiki-war/src/main/webapp/templates/default/commonheader.jsp +++ b/jspwiki-war/src/main/webapp/templates/default/commonheader.jsp @@ -152,5 +152,10 @@ String.I18N.PREFIX = "javascript."; +<%-- Without a skin of their own choosing, users whose system asks for a dark colour scheme get the dark skin --%> + + + \ No newline at end of file From f0e064a87d6e25139ae2878423e79a6eb528f1b5 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Sat, 31 Oct 2026 18:56:44 +0100 Subject: [PATCH 3/3] JSPWIKI-1317 Add the entry of the change to the ChangeLog --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index ab39a49fda..d552857944 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -37,6 +37,7 @@ under the License. * [JSPWIKI-1299](https://issues.apache.org/jira/browse/JSPWIKI-1299): `CryptoUtil`, `AbstractUserDatabase` and their tests * [JSPWIKI-1301](https://issues.apache.org/jira/browse/JSPWIKI-1301): session handling: `Session`, `WikiSession`, `SessionMonitor` and `DefaultAuthenticationManager` +* [JSPWIKI-1317](https://issues.apache.org/jira/browse/JSPWIKI-1317): the default template follows the dark colour scheme of the system when no skin was chosen ([PR#555](https://github.com/apache/jspwiki/pull/555)) * Fix for [JSPWIKI-1138](https://issues.apache.org/jira/browse/JSPWIKI-1138): Install.jsp echoed submitted values without escaping them; they are escaped now ([PR#323](https://github.com/apache/jspwiki/pull/323)) * Fix for [JSPWIKI-1164](https://issues.apache.org/jira/browse/JSPWIKI-1164): a page reached from a search result showed `<` as `<` when the query was highlighted ([PR#538](https://github.com/apache/jspwiki/pull/538)) * Fix for [JSPWIKI-919](https://issues.apache.org/jira/browse/JSPWIKI-919): `VersioningFileProvider#pageExists()` ignored the first version of a page that has no history, so a page dropped into the page directory was reported as missing when the page cache is off ([PR#539](https://github.com/apache/jspwiki/pull/539))