diff --git a/api/src/org/labkey/api/action/BaseApiAction.java b/api/src/org/labkey/api/action/BaseApiAction.java index 821fcc25316..ff0ebe377b8 100644 --- a/api/src/org/labkey/api/action/BaseApiAction.java +++ b/api/src/org/labkey/api/action/BaseApiAction.java @@ -39,6 +39,7 @@ import org.labkey.api.util.MimeMap; import org.labkey.api.util.Pair; import org.labkey.api.util.ResponseHelper; +import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.view.BadRequestException; import org.labkey.api.view.NotFoundException; import org.labkey.api.view.UnauthorizedException; @@ -52,6 +53,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.InputStreamReader; import java.io.Reader; import java.net.SocketTimeoutException; import java.time.Duration; @@ -511,12 +513,23 @@ protected long getMaximumJsonInputLength() private @Nullable JSONObject getJsonObject() throws IOException { + HttpServletRequest request = getViewContext().getRequest(); + if (request == null) + return null; + + String characterEncoding = request.getCharacterEncoding(); + if (characterEncoding == null) + characterEncoding = StringUtilsLabKey.DEFAULT_CHARSET.name(); + long maxLength = getMaximumJsonInputLength(); - try (Reader r = getViewContext().getRequest().getReader(); - Reader jsonReader = maxLength > 0 ? new StrictBoundedReader(r, maxLength) : r) + + // Issue 53699: Use request.getInputStream() instead of request.getReader() to + // avoid BufferUnderflowException when processing multibyte character JSON payloads. + try (Reader streamReader = new InputStreamReader(request.getInputStream(), characterEncoding); + Reader jsonReader = maxLength > 0 ? new StrictBoundedReader(streamReader, maxLength) : streamReader) { - JSONTokener tokener = new JSONTokener(r); - return tokener.more() ? new JSONObject(new JSONTokener(jsonReader)) : null; + JSONTokener tokener = new JSONTokener(jsonReader); + return tokener.more() ? new JSONObject(tokener) : null; } } diff --git a/api/src/org/labkey/api/data/validator/LengthValidator.java b/api/src/org/labkey/api/data/validator/LengthValidator.java index 2aace6b1984..0b93cdf96de 100644 --- a/api/src/org/labkey/api/data/validator/LengthValidator.java +++ b/api/src/org/labkey/api/data/validator/LengthValidator.java @@ -35,8 +35,9 @@ public String _validate(int rowNum, Object value) { if (value instanceof String s) { - if (s.length() > scale) - return "Value is too long for column '" + _columnName + "', a maximum length of " + scale + " is allowed. The supplied value, '" + StringUtils.abbreviateMiddle(s, "...", 50) + "', was " + s.length() + " characters long."; + int charLength = Character.codePointCount(s, 0, s.length()); + if (charLength > scale) + return "Value is too long for column '" + _columnName + "', a maximum length of " + scale + " is allowed. The supplied value, '" + StringUtils.abbreviateMiddle(s, "...", 50) + "', was " + charLength + " characters long."; } return null; diff --git a/api/src/org/labkey/api/dataiterator/ExistingRecordDataIterator.java b/api/src/org/labkey/api/dataiterator/ExistingRecordDataIterator.java index 66372a4d971..333813d0c89 100644 --- a/api/src/org/labkey/api/dataiterator/ExistingRecordDataIterator.java +++ b/api/src/org/labkey/api/dataiterator/ExistingRecordDataIterator.java @@ -172,6 +172,9 @@ public Object getConstantValue(int i) @Override public boolean next() throws BatchValidationException { + if (_context.getErrors().hasErrors()) + return false; + // NOTE: we have to call mark() before we call next() if we want the 'next' row to be cached if (useMark) _unwrapped.mark(); // unwrapped _delegate diff --git a/assay/src/org/labkey/assay/plate/PlateImpl.java b/assay/src/org/labkey/assay/plate/PlateImpl.java index 89f4579c9cf..b0071a0c912 100644 --- a/assay/src/org/labkey/assay/plate/PlateImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateImpl.java @@ -23,7 +23,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateCustomField; import org.labkey.api.assay.plate.PlateService; @@ -42,6 +46,8 @@ import org.labkey.api.security.User; import org.labkey.api.security.UserManager; import org.labkey.api.util.GUID; +import org.labkey.api.util.JunitUtil; +import org.labkey.api.util.TestContext; import org.labkey.api.util.UnexpectedException; import org.labkey.api.view.ActionURL; import org.labkey.assay.PlateController; @@ -58,9 +64,7 @@ import java.util.List; import java.util.Map; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - +@RunWith(Enclosed.class) @JsonInclude(JsonInclude.Include.NON_NULL) public class PlateImpl extends PropertySetImpl implements Plate, Cloneable { @@ -160,9 +164,9 @@ public static PlateImpl from(PlateBean bean) Container container = ContainerManager.getForId(bean.getContainerId()); plate.setContainer(container); plate.setCreated(bean.getCreated()); - plate.setCreatedBy(UserManager.getUser(bean.getCreatedBy())); + plate.setCreatedBy(bean.getCreatedBy()); plate.setModified(bean.getModified()); - plate.setModifiedBy(UserManager.getUser(bean.getModifiedBy())); + plate.setModifiedBy(bean.getModifiedBy()); // plate type and plate set objects PlateType plateType = PlateManager.get().getPlateType(bean.getPlateType()); @@ -436,9 +440,9 @@ public JSONObject getCreatedBy() return user != null ? user.getUserProps() : null; } - public void setCreatedBy(User createdBy) + public void setCreatedBy(int createdBy) { - _createdBy = createdBy.getUserId(); + _createdBy = createdBy; } public Date getModified() @@ -458,9 +462,9 @@ public JSONObject getModifiedBy() return user != null ? user.getUserProps() : null; } - public void setModifiedBy(User modifiedBy) + public void setModifiedBy(int modifiedBy) { - _modifiedBy = modifiedBy.getUserId(); + _modifiedBy = modifiedBy; } public String getDataFileId() @@ -758,8 +762,17 @@ public boolean isNew() return _rowId == null || _rowId <= 0; } - public static final class TestCase + public static class TestCase extends Assert { + private PlateSetImpl _plateSet; + + @Before + public void setup() throws Exception + { + PlateSetImpl plateSet = new PlateSetImpl(); + _plateSet = PlateManager.get().createPlateSet(JunitUtil.getTestContainer(), TestContext.get().getUser(), plateSet, null, null); + } + @Test public void testIdentifierMatch() { @@ -781,5 +794,54 @@ public void testIdentifierMatch() assertFalse("Expected plate to not match invalid name", plate.isIdentifierMatch("")); assertFalse("Expected plate to not match invalid name", plate.isIdentifierMatch(null)); } + + @Test + public void testFromInitializer() + { + String testContainerId = JunitUtil.getTestContainer().getId(); + int testUserId = TestContext.get().getUser().getUserId(); + Long plateTypeId = PlateManager.get().getPlateTypes().stream().findFirst().orElseThrow().getRowId(); + Long plateSetId = _plateSet.getRowId(); + + PlateBean bean = new PlateBean(); + Long expectedRowId = 1L; + String expectedName = "Test Name"; + Boolean archived = null; + bean.setPlateType(plateTypeId); + bean.setPlateSet(plateSetId); + bean.setContainerId(testContainerId); + bean.setCreatedBy(testUserId); + bean.setModifiedBy(testUserId); + bean.setRowId(expectedRowId); + bean.setPlateId("test-id"); + bean.setName(expectedName); + bean.setArchived(archived); + + PlateImpl plate = PlateImpl.from(bean); + assertNotNull(plate.getCreatedBy()); + assertEquals(testUserId, plate.getCreatedBy().getInt("id")); + assertNotNull(plate.getModifiedBy()); + assertEquals(testUserId, plate.getModifiedBy().getInt("id")); + assertEquals(expectedName, plate.getName()); + assertEquals(expectedRowId, plate.getRowId()); + assertFalse(plate.isArchived()); + assertFalse(plate.isNew()); + + // Issue 53633: Verify an invalid user does not cause an error initializing from bean + { + bean = new PlateBean(); + bean.setPlateType(plateTypeId); + bean.setPlateSet(plateSetId); + bean.setContainerId(testContainerId); + bean.setCreatedBy(-1); + bean.setModifiedBy(-1); + + plate = PlateImpl.from(bean); + assertNull(plate.getCreatedBy()); + assertNull(plate.getModifiedBy()); + assertFalse(plate.isArchived()); + assertTrue(plate.isNew()); + } + } } } diff --git a/study/test/src/org/labkey/test/tests/study/AutoLinkToStudyTest.java b/study/test/src/org/labkey/test/tests/study/AutoLinkToStudyTest.java index 0b8ac910ce1..a631b12183e 100644 --- a/study/test/src/org/labkey/test/tests/study/AutoLinkToStudyTest.java +++ b/study/test/src/org/labkey/test/tests/study/AutoLinkToStudyTest.java @@ -13,15 +13,11 @@ import org.labkey.test.categories.Daily; import org.labkey.test.components.CustomizeView; import org.labkey.test.components.assay.AssayConstants; -import org.labkey.test.components.ext4.Window; import org.labkey.test.pages.query.ExecuteQueryPage; import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionTable; -import org.labkey.test.util.Ext4Helper; import org.labkey.test.util.PermissionsHelper; import org.labkey.test.util.StudyHelper; -import org.openqa.selenium.Keys; -import org.openqa.selenium.WebElement; import java.io.File; import java.util.List; @@ -300,20 +296,8 @@ private void importAssayRun(File runFile, String assayName, String runName) private void createDatasetCategory(String projectName, String name) { goToProjectHome(projectName); - goToManageViews(); - Locator.linkWithText("Manage Categories").findElement(getDriver()).click(); - _extHelper.waitForExtDialog("Manage Categories"); - Window categoryWindow = new Window.WindowFinder(getDriver()).withTitle("Manage Categories").waitFor(); - categoryWindow.clickButton("New Category", 0); - WebElement newCategoryField = Locator.input("label").withAttributeContaining("id", "textfield").notHidden().waitForElement(getDriver(), WAIT_FOR_JAVASCRIPT); - actionClear(newCategoryField); - sleep(500); - newCategoryField.sendKeys(name); - sleep(500); - newCategoryField.sendKeys(Keys.ENTER); - waitForElement(Ext4Helper.Locators.window("Manage Categories").append("//div").withText(name)); - clickButton("Done", 0); - _extHelper.waitForExtDialogToDisappear("Manage Categories"); + goToManageViews() + .createCategory(name); } private String getCategory(String projectName, String datasetName)