Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -45,8 +46,10 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;

import org.apache.commons.lang3.StringUtils;
import org.json.JSONException;
Expand Down Expand Up @@ -527,34 +530,75 @@ protected String importSteps(Authentication auth, @RequestBody ObjectNode object
toProjectAssetsUrl);

if (newFileName != null && !fileName.equals(newFileName)) {
// the file name was changed so we need to update the step content by replacing
// all instances of the file name with the new file name
steps = steps.replaceAll(fileName, newFileName);
steps = replaceAssetFileName(steps, fileName, newFileName);
}
}

// send back the steps string which may have been modified if we needed to change a file name
return steps;
}

protected String replaceAssetFileName(String steps, String fileName, String newFileName) {
// getAssetFileNames found the name after decoding %20, so the encoded form is replaced too
return steps.replace(fileName, newFileName)
.replace(fileName.replace(" ", "%20"), newFileName.replace(" ", "%20"));
}

// Same name class as ProjectService.replaceAssetPaths in WISE-Client. Widening it lets a match
// start at an HTML attribute's closing quote and swallow the next src.
private static final Pattern EMBEDDED_ASSET = Pattern.compile(
"('|\"|\\\\'|\\\\\")([^:][^/]?[^/]?[a-zA-Z0-9\\u4e00-\\u9fa5@%&;\\._\\/\\s\\-']*[.]"
+ "(png|jpe?g|pdf|gif|mov|mp4|mp3|wav|swf|css|txt|json|xlsx?|doc|html.*?|js)).*?"
+ "('|\"|\\\\'|\\\\\")",
Pattern.CASE_INSENSITIVE);

private static final Pattern RELATIVE_ASSET_FILE_NAME = Pattern.compile(
"^(?!/)[a-zA-Z0-9@%&;()._/ \\-"
+ "\\u3008-\\u3011\\u3014-\\u301b\\u3040-\\u30ff\\u3400-\\u4dbf\\u4e00-\\u9fff\\uac00-\\ud7af"
+ "\\uff08\\uff09\\uff10-\\uff19\\uff21-\\uff3a\\uff3b\\uff3d\\uff41-\\uff5a]+"
+ "\\.(png|jpe?g|pdf|gif|mov|mp4|mp3|wav|swf|css|txt|json|xlsx?|doc|html?|js)$",
Pattern.CASE_INSENSITIVE);

protected List<String> getAssetFileNames(String stepsText) {
// Replace %20 with a space so that file names with a space will be properly found. Sometimes
// html content that contains a file name with a space will replace the space with %20.
String stepsTextModified = stepsText.replaceAll("%20", " ");

// Regex string to match asset file references in the step/component content. e.g. carbon.png
String patternString = """
('|"|\\\\'|\\\\")([^:][^/]?[^/]?[a-zA-Z0-9@\\._\\/\\s\\-]*[.]\
(png|PNG|jpe?g|JPE?G|pdf|PDF|gif|GIF|mov|MOV|mp4|MP4|mp3|MP3|wav|WAV|swf|SWF|css|CSS\
|txt|TXT|json|JSON|xlsx?|XLSX?|doc|DOC|html.*?|HTML.*?|js|JS)).*?('|"|\\\\'|\\\\")\
""";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(stepsTextModified);
HashSet<String> fileNames = new HashSet<String>();
JsonNode steps = null;
try {
steps = new ObjectMapper().readTree(stepsText);
} catch (JsonProcessingException e) {
// not JSON, e.g. an HTML fragment: search it as one text
}
if (steps != null && steps.isContainerNode()) {
collectAssetFileNames(steps, fileNames);
} else {
addEmbeddedAssetFileNames(stepsText, fileNames);
}
return new ArrayList<String>(fileNames);
}

// Searching the serialized JSON as a whole lets a match run from the closing quote of one string
// into the next, so each string value is searched on its own.
private void collectAssetFileNames(JsonNode node, Set<String> fileNames) {
if (node.isTextual()) {
String value = node.asText();
String decoded = value.replace("%20", " ");
if (RELATIVE_ASSET_FILE_NAME.matcher(decoded).matches()) {
fileNames.add(decoded);
} else {
addEmbeddedAssetFileNames(TextNode.valueOf(value).toString(), fileNames);
}
} else {
for (JsonNode child : node) {
collectAssetFileNames(child, fileNames);
}
}
}

private void addEmbeddedAssetFileNames(String text, Set<String> fileNames) {
// html content sometimes encodes the space in a file name as %20
Matcher matcher = EMBEDDED_ASSET.matcher(text.replace("%20", " "));
while (matcher.find()) {
fileNames.add(matcher.group(2));
}
return new ArrayList<String>(fileNames);
}

private void createNewProjectDirectory(String projectId) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.easymock.EasyMock.anyObject;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -305,4 +306,54 @@ public void getAssetFileNames_withASpace_shouldReturnFileNames() throws Exceptio
assertTrue(fileNames.contains("carbon dioxide.png"));
assertTrue(fileNames.contains("carbon monoxide.png"));
}

@Test
public void getAssetFileNames_withParenthesesAndCJK_shouldReturnEachFileName() throws Exception {
String stepsText = "{\"Stamps\":[\"示意圖 (chatgpt生成) 1.png\",\"細胞(核)【卵】.png\",\"b.png\"]}";
List<String> fileNames = authorAPIController.getAssetFileNames(stepsText);
assertEquals(3, fileNames.size());
assertTrue(fileNames.contains("示意圖 (chatgpt生成) 1.png"));
assertTrue(fileNames.contains("細胞(核)【卵】.png"));
assertTrue(fileNames.contains("b.png"));
}

@Test
public void getAssetFileNames_afterAnUnmatchedName_shouldNotRunIntoTheNextString()
throws Exception {
String stepsText = "[\"image#1.png\",\"b.png\"]";
List<String> fileNames = authorAPIController.getAssetFileNames(stepsText);
assertTrue(fileNames.contains("b.png"), fileNames.toString());
for (String fileName : fileNames) {
assertFalse(fileName.contains("\""), fileName);
}
}

@Test
public void getAssetFileNames_withParenthesesInHtmlText_shouldStillFindTheNextImage()
throws Exception {
String stepsText = "[{\"html\":\"<p title=\\\"x\\\">(see) a.png</p><img src=\\\"b.png\\\">\"}]";
List<String> fileNames = authorAPIController.getAssetFileNames(stepsText);
assertTrue(fileNames.contains("b.png"), fileNames.toString());
}

@Test
public void getAssetFileNames_withMixedCaseExtension_shouldReturnFileName() throws Exception {
String stepsText = "[{\"src\":\"Photo.Png\",\"html\":\"<img src=\\\"x.Jpg\\\">\"}]";
List<String> fileNames = authorAPIController.getAssetFileNames(stepsText);
assertTrue(fileNames.contains("Photo.Png"), fileNames.toString());
assertTrue(fileNames.contains("x.Jpg"), fileNames.toString());
}

@Test
public void replaceAssetFileName_withParentheses_shouldReplaceLiterally() {
String steps = "[\"a (1).png\",\"<img src=\\\"a%20(1).png\\\">\"]";
String result = authorAPIController.replaceAssetFileName(steps, "a (1).png", "a (1)-2.png");
assertEquals("[\"a (1)-2.png\",\"<img src=\\\"a%20(1)-2.png\\\">\"]", result);
}

@Test
public void replaceAssetFileName_withUnbalancedParenthesis_shouldNotThrow() {
String result = authorAPIController.replaceAssetFileName("[\"a(.png\"]", "a(.png", "a(-1.png");
assertEquals("[\"a(-1.png\"]", result);
}
}
Loading