Skip to content

Commit dcf3355

Browse files
Merge remote-tracking branch 'origin/release26.7-SNAPSHOT' into 26.7_fb_pg14_mutlitabMetric
2 parents d696568 + 445dc4d commit dcf3355

28 files changed

Lines changed: 951 additions & 399 deletions

api/src/org/labkey/api/data/DataColumn.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ protected ColumnInfo getDisplayField(@NotNull ColumnInfo col, boolean withLookup
191191
return null==display ? col : display;
192192
}
193193

194+
@Override
195+
public void setWithLookup(boolean withLookup)
196+
{
197+
_displayColumn = withLookup ? getDisplayField(_boundColumn, true) : _boundColumn;
198+
}
199+
194200
@Override
195201
public String toString()
196202
{

api/src/org/labkey/api/data/DisplayColumn.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,11 @@ public void setRequiresHtmlFiltering(boolean requiresHtmlFiltering)
12321232
_requiresHtmlFiltering = requiresHtmlFiltering;
12331233
}
12341234

1235+
public void setWithLookup(boolean withLookup)
1236+
{
1237+
// subclasses override as needed
1238+
}
1239+
12351240
public void setLinkTarget(String linkTarget)
12361241
{
12371242
_linkTarget = linkTarget;

api/src/org/labkey/api/data/dialect/SqlDialect.java

Lines changed: 108 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.labkey.api.module.ModuleLoader;
6060
import org.labkey.api.query.FieldKey;
6161
import org.labkey.api.util.ExceptionUtil;
62+
import org.labkey.api.util.HtmlString;
6263
import org.labkey.api.util.MemTracker;
6364
import org.labkey.api.util.StringUtilsLabKey;
6465
import org.labkey.api.util.SystemMaintenance;
@@ -80,6 +81,8 @@
8081
import java.sql.SQLException;
8182
import java.sql.Statement;
8283
import java.sql.Types;
84+
import java.time.Duration;
85+
import java.time.LocalDateTime;
8386
import java.util.ArrayList;
8487
import java.util.Arrays;
8588
import java.util.Calendar;
@@ -95,6 +98,7 @@
9598
import java.util.regex.Pattern;
9699

97100
import static org.junit.Assert.assertEquals;
101+
import static org.junit.Assert.assertFalse;
98102
import static org.junit.Assert.assertThrows;
99103
import static org.junit.Assert.assertTrue;
100104

@@ -168,7 +172,7 @@ public String getOtherDatabaseThreads()
168172
{
169173
StringBuilder sb = new StringBuilder();
170174

171-
// Per 18789, also include threads without db connections.
175+
// Per Issue 18789, also include threads without db connections.
172176
List<Thread> dbThreads = new ArrayList<>();
173177

174178
for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet())
@@ -863,25 +867,25 @@ public SQLFragment getNumericCast(SQLFragment expression)
863867
* @param arguments Arguments passed from the LK SQL
864868
* @return the dialect equivalent SQLFragrment
865869
*/
866-
public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments)
867-
{
868-
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
869-
}
870-
871-
public boolean supportsIsNumeric()
872-
{
873-
return false;
874-
}
875-
876-
public SQLFragment isNumericExpr(SQLFragment expression)
877-
{
878-
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
879-
}
880-
881-
public void handleCreateDatabaseException(SQLException e) throws ServletException
882-
{
883-
throw(new ServletException("Can't create database", e));
884-
}
870+
public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments)
871+
{
872+
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
873+
}
874+
875+
public boolean supportsIsNumeric()
876+
{
877+
return false;
878+
}
879+
880+
public SQLFragment isNumericExpr(SQLFragment expression)
881+
{
882+
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
883+
}
884+
885+
public void handleCreateDatabaseException(SQLException e) throws ServletException
886+
{
887+
throw(new ServletException("Can't create database", e));
888+
}
885889

886890
/**
887891
* Wrap one or more INSERT statements to allow explicit specification
@@ -1990,12 +1994,61 @@ public final Collection<String> getExecutionPlan(DbScope scope, SQLFragment sql,
19901994
// Add any database configuration warnings (e.g., missing aggregate function or deprecated database server version)
19911995
// to display in the page header for administrators. This will be called:
19921996
// - Only on the LabKey DataSource's dialect instance (not external data sources)
1993-
// - After the core module has been upgraded and the dialect has been prepared for the last time, meaning the dialect
1997+
// - After the core module has been upgraded, and the dialect has been prepared for the last time, meaning the dialect
19941998
// should reflect the final database configuration
19951999
public void addAdminWarningMessages(Warnings warnings, boolean showAllWarnings)
19962000
{
19972001
}
19982002

2003+
public static final long TIME_DIFFERENCE_WARNING_SECONDS = 10;
2004+
2005+
public static ServerDatabaseTimeDifference getServerDatabaseTimeDifference(DbScope scope)
2006+
{
2007+
// Compare LocalDateTime to capture any difference in server and database times (skew or timezone).
2008+
LocalDateTime serverTime = LocalDateTime.now();
2009+
LocalDateTime databaseTime = new SqlSelector(scope, "SELECT CURRENT_TIMESTAMP").getObject(LocalDateTime.class);
2010+
2011+
return new ServerDatabaseTimeDifference(serverTime, databaseTime);
2012+
}
2013+
2014+
public record ServerDatabaseTimeDifference(LocalDateTime serverTime, LocalDateTime databaseTime)
2015+
{
2016+
public long getSeconds()
2017+
{
2018+
return Math.abs(Duration.between(serverTime, databaseTime).toSeconds());
2019+
}
2020+
2021+
public boolean exceedsWarningThreshold()
2022+
{
2023+
return getSeconds() > TIME_DIFFERENCE_WARNING_SECONDS;
2024+
}
2025+
}
2026+
2027+
// GH Issue #1223: Add a site configuration warning for administrators if the server and database clocks differ.
2028+
protected void addTimeDifferenceWarning(Warnings warnings, boolean showAllWarnings)
2029+
{
2030+
try
2031+
{
2032+
ServerDatabaseTimeDifference difference = getServerDatabaseTimeDifference(DbScope.getLabKeyScope());
2033+
2034+
if (difference.exceedsWarningThreshold())
2035+
warnings.add(getTimeDifferenceWarning(difference.getSeconds()));
2036+
else if (showAllWarnings)
2037+
warnings.add(getTimeDifferenceWarning(TIME_DIFFERENCE_WARNING_SECONDS + 1));
2038+
}
2039+
catch (Exception e)
2040+
{
2041+
LOG.warn("Unable to compare web server and database server times", e);
2042+
}
2043+
}
2044+
2045+
private HtmlString getTimeDifferenceWarning(long seconds)
2046+
{
2047+
return HtmlString.of("The web server and database server times differ by " + seconds + " seconds. " +
2048+
"LabKey Server often relies on comparing timestamps stored in the database with timestamps generated by " +
2049+
"the web server, so this difference can lead to data integrity issues. Synchronize the clocks on these servers.");
2050+
}
2051+
19992052
public abstract List<SQLFragment> getChangeStatements(TableChange change);
20002053

20012054
public abstract void purgeTempSchema(Map<String, TempTableTracker> createdTableNames);
@@ -2017,7 +2070,7 @@ protected void trackTempTables(Map<String, TempTableTracker> createdTableNames)
20172070
// Defragment an index, if necessary
20182071
public void defragmentIndex(DbSchema schema, String tableSelectName, String indexName)
20192072
{
2020-
// By default do nothing
2073+
// By default, do nothing
20212074
}
20222075

20232076
public boolean isTableExists(DbScope scope, String schema, String name)
@@ -2045,7 +2098,7 @@ public boolean hasTriggers(DbSchema scope, String schema, String tableName)
20452098

20462099
/**
20472100
*
2048-
* @return true If the dialect is one supported for the backend LabKey database. ie, Postgres or SQL Server
2101+
* @return true If the dialect is one supported for the backend LabKey database. i.e., Postgres or SQL Server
20492102
*/
20502103
public boolean isLabKeyDbDialect()
20512104
{
@@ -2419,5 +2472,37 @@ public void testProcedureIdentifierQuoting()
24192472
// A dotted name is rejected: schema and procedure are separate parameters, so a '.' in a single component is an ambiguous schema-qualified reference
24202473
assertThrows(IllegalArgumentException.class, () -> dialect.buildProcedureCall(core.getName(), "a.b", 0, false, false, scope));
24212474
}
2475+
2476+
// GH Issue #1223: Verify the server/database clock-skew arithmetic and warning threshold
2477+
@Test
2478+
public void testServerDatabaseTimeDifference()
2479+
{
2480+
LocalDateTime base = LocalDateTime.of(2026, 7, 6, 12, 0, 0);
2481+
2482+
// Identical times: zero difference, no warning
2483+
ServerDatabaseTimeDifference equal = new ServerDatabaseTimeDifference(base, base);
2484+
assertEquals(0, equal.getSeconds());
2485+
assertFalse(equal.exceedsWarningThreshold());
2486+
2487+
// Comfortably below the threshold: no warning
2488+
ServerDatabaseTimeDifference below = new ServerDatabaseTimeDifference(base, base.plusSeconds(5));
2489+
assertEquals(5, below.getSeconds());
2490+
assertFalse(below.exceedsWarningThreshold());
2491+
2492+
// Exactly at the threshold: no warning (comparison is strictly greater-than)
2493+
ServerDatabaseTimeDifference atThreshold = new ServerDatabaseTimeDifference(base, base.plusSeconds(TIME_DIFFERENCE_WARNING_SECONDS));
2494+
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS, atThreshold.getSeconds());
2495+
assertFalse(atThreshold.exceedsWarningThreshold());
2496+
2497+
// Just past the threshold: warning
2498+
ServerDatabaseTimeDifference over = new ServerDatabaseTimeDifference(base, base.plusSeconds(TIME_DIFFERENCE_WARNING_SECONDS + 1));
2499+
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS + 1, over.getSeconds());
2500+
assertTrue(over.exceedsWarningThreshold());
2501+
2502+
// Difference is absolute: database clock behind the web server is treated the same as ahead
2503+
ServerDatabaseTimeDifference behind = new ServerDatabaseTimeDifference(base, base.minusSeconds(TIME_DIFFERENCE_WARNING_SECONDS + 1));
2504+
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS + 1, behind.getSeconds());
2505+
assertTrue(behind.exceedsWarningThreshold());
2506+
}
24222507
}
24232508
}

api/src/org/labkey/api/security/AuthFilter.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ public class AuthFilter implements Filter
5555
private static final Object FIRST_REQUEST_LOCK = new Object();
5656

5757
public static final String STRICT_TRANSPORT_SECURITY_HEADER_NAME = "Strict-Transport-Security";
58-
public static final String X_FRAME_OPTIONS_HEADER_NAME = "X-Frame-Options";
5958
public static final String X_CONTENT_TYPE_OPTIONS_HEADER_NAME = "X-Content-Type-Options";
6059
public static final String REFERRER_POLICY_HEADER_NAME = "Referrer-Policy";
6160
public static final String SERVER_HEADER_NAME = "Server";
@@ -87,8 +86,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
8786

8887
if (ModuleLoader.getInstance().isStartupComplete())
8988
{
90-
if (!"ALLOW".equals(AppProps.getInstance().getXFrameOption()))
91-
resp.setHeader(X_FRAME_OPTIONS_HEADER_NAME, AppProps.getInstance().getXFrameOption());
9289
resp.setHeader(X_CONTENT_TYPE_OPTIONS_HEADER_NAME, "nosniff");
9390
resp.setHeader(REFERRER_POLICY_HEADER_NAME, "origin-when-cross-origin" );
9491

api/src/org/labkey/api/settings/AppProps.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,6 @@ static WriteableAppProps getWriteableInstance()
211211

212212
// configurable http security settings
213213

214-
/**
215-
* @return "SAMEORIGIN" or "DENY" or "ALLOW"
216-
*/
217-
String getXFrameOption();
218-
219214
String getStaticFilesPrefix();
220215

221216
boolean isWebfilesRootEnabled();

api/src/org/labkey/api/settings/AppPropsImpl.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -554,13 +554,6 @@ public boolean isAllowSessionKeys()
554554
return lookupBooleanValue(allowSessionKeys, false);
555555
}
556556

557-
@Override
558-
public String getXFrameOption()
559-
{
560-
return lookupStringValue(XFrameOption, "SAMEORIGIN");
561-
}
562-
563-
564557
private static final String not_init = "";
565558
private String staticFilesPrefix = not_init;
566559

api/src/org/labkey/api/settings/SiteSettingsProperties.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,6 @@ public void setValue(WriteableAppProps writeable, String value)
178178
writeable.setAdminOnlyMessage(value);
179179
}
180180
},
181-
XFrameOption("Controls whether or not a browser may render a server page in a <frame> , <iframe> or <object>. Valid values: [SAMEORIGIN, ALLOW]")
182-
{
183-
@Override
184-
public void setValue(WriteableAppProps writeable, String value)
185-
{
186-
writeable.setXFrameOption(value);
187-
}
188-
},
189181
navAccessOpen("Always include inaccessible parent folders in project menu when child folder is accessible")
190182
{
191183
@Override

api/src/org/labkey/api/settings/WriteableAppProps.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,6 @@ public void setAllowSessionKeys(boolean b)
225225
storeBooleanValue(allowSessionKeys, b);
226226
}
227227

228-
public void setXFrameOption(String option)
229-
{
230-
storeStringValue(XFrameOption, option);
231-
}
232-
233228
public void setExternalRedirectHosts(@NotNull Collection<String> externalRedirectHosts)
234229
{
235230
setAllowList(externalRedirectHostURLs, externalRedirectHosts);

api/src/org/labkey/api/util/ExceptionUtil.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,6 @@ static ActionURL handleException(@NotNull HttpServletRequest request, @NotNull H
833833
ContentSecurityPolicyFilter.ContentSecurityPolicyType.Enforce.getHeaderName(),
834834
ContentSecurityPolicyFilter.ContentSecurityPolicyType.Report.getHeaderName(),
835835
AuthFilter.STRICT_TRANSPORT_SECURITY_HEADER_NAME,
836-
AuthFilter.X_FRAME_OPTIONS_HEADER_NAME,
837836
AuthFilter.X_CONTENT_TYPE_OPTIONS_HEADER_NAME,
838837
AuthFilter.REFERRER_POLICY_HEADER_NAME,
839838
AuthFilter.SERVER_HEADER_NAME))
@@ -1106,7 +1105,6 @@ private static void renderErrorPage(Throwable ex, int responseStatus, String mes
11061105
else
11071106
{
11081107
pageConfig.setTemplate(originalConfig.getTemplate());
1109-
pageConfig.setFrameOption(originalConfig.getFrameOption());
11101108
}
11111109

11121110
HttpView<?> errorView = null;

api/src/org/labkey/api/util/PageFlowUtil.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2696,6 +2696,16 @@ private static boolean shouldEscapeForExport(@NotNull String value)
26962696
return StringUtils.containsAny(value,",\"");
26972697
}
26982698

2699+
/// Generate one row of tab-delimited output using RFC 4180 quoting rules.
2700+
/// Fields containing tabs, newlines, or double quotes are enclosed in double quotes,
2701+
/// with embedded double quotes escaped by doubling.
2702+
public static String joinValuesWithTabs4180(@NotNull List<String> values)
2703+
{
2704+
return values.stream()
2705+
.map(value -> null == value ? "" : StringUtils.containsAny(value, "\t\n\r\"") ? "\"" + Strings.CS.replace(value, "\"", "\"\"") + "\"" : value)
2706+
.collect(Collectors.joining("\t"));
2707+
}
2708+
26992709

27002710
static final String FIELD_ENCODED_PREFIX = "%_";
27012711

0 commit comments

Comments
 (0)