diff --git a/dotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java b/dotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java index df6d58202b3b..ae8e8c83e83d 100644 --- a/dotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java @@ -121,6 +121,13 @@ public class BrowserAPIImpl implements BrowserAPI { (ContentletJsonAPI.CONTENTLET_AS_JSON).append(", '$.fields.").append("fileName.").append("value')" + " "); + /** + * Synthetic MIME Type that dotCMS assigns to HTML Pages at display time. It is never persisted to the + * {@code contentlet_as_json} column, so it can only be resolved through the HTMLPAGE base type. Legacy display + * code declares its own copies of this value; they are intentionally left alone. + */ + private static final String DOTPAGE_MIME_TYPE = "application/dotpage"; + private static final StringBuilder ASSET_NAME_LIKE = new StringBuilder().append("LOWER(%s) LIKE ? "); private static final StringBuilder ASSET_NAME_EQ = new StringBuilder().append("LOWER(%s) = ? "); @@ -2433,14 +2440,32 @@ private void appendOrderByQuery(StringBuilder sqlQuery, boolean orderByDesc) { } /** - * Appends the specified MIME Types to the main SQL query. + * Appends the specified MIME Types to the main SQL query. Every requested MIME Type is routed to the only + * condition that can actually match it, and the resulting conditions are OR'ed together: + * + * The match on the synthetic value is exact on purpose. A MIME Type that merely starts with it -- say, + * {@code application/dotpage-foo} -- must still go through the metadata check. + *

The {@code struc} table is already joined by {@link #buildSelectBaseQuery(BrowserQuery, String)}, so the + * base type condition needs no extra join and no bound parameter. * * @param sqlQuery The main SQL query. * @param mimeTypes The list of MIME Types specified by the client. */ private void appendMIMETypeQuery(final StringBuilder sqlQuery, final List mimeTypes) { final String mimeTypesFilter = String.format(" AND (%s)", mimeTypes.stream() - .map(mimeType -> String.format("jsonb_path_exists(c.contentlet_as_json,'$.fields.**.metadata ? (@.contentType like_regex \".*%s.*\")')", mimeType)) + .map(mimeType -> { + if (DOTPAGE_MIME_TYPE.equals(mimeType)) { + return String.format("struc.structuretype = %d", BaseContentType.HTMLPAGE.getType()); + } + return String.format("jsonb_path_exists(c.contentlet_as_json,'$.fields.**.metadata ? (@.contentType like_regex \".*%s.*\")')", mimeType); + }) .collect(Collectors.joining(" OR "))); sqlQuery.append(mimeTypesFilter); } diff --git a/dotcms-integration/src/test/java/com/dotcms/browser/BrowserAPITest.java b/dotcms-integration/src/test/java/com/dotcms/browser/BrowserAPITest.java index 3519877b9c0f..fa4286a91be8 100644 --- a/dotcms-integration/src/test/java/com/dotcms/browser/BrowserAPITest.java +++ b/dotcms-integration/src/test/java/com/dotcms/browser/BrowserAPITest.java @@ -22,6 +22,7 @@ import com.dotcms.rest.api.v1.content.search.strategies.GlobalSearchAttributeStrategy; import com.dotcms.datagen.RoleDataGen; import com.dotcms.datagen.SiteDataGen; +import com.dotcms.datagen.TemplateDataGen; import com.dotcms.datagen.TestDataUtils; import com.dotcms.datagen.TestUserUtils; import com.dotcms.datagen.UserDataGen; @@ -95,6 +96,14 @@ public class BrowserAPITest extends IntegrationTestBase { static Link testlink; + // Fixture for the MIME-type routing tests -- https://github.com/dotCMS/core/issues/36916 + static final String DOTPAGE_MIME_TYPE = "application/dotpage"; + static Host mimeHost; + static Folder mimeFolder, mimeSubFolder; + static HTMLPageAsset mimePage, mimePageAltLanguage, mimeSubFolderPage; + static FileAsset mimeJpgFile, mimePdfFile, mimeTxtFile, mimeSubFolderJpgFile; + static Link mimeLink; + @BeforeClass public static void prepare() throws Exception { //Setting web app environment @@ -138,6 +147,92 @@ public static void prepare() throws Exception { testPage = APILocator.getHTMLPageAssetAPI().fromContentlet(HTMLPageDataGen.checkin(page, IndexPolicy.FORCE)); testlink = new LinkDataGen().hostId(testHost.getIdentifier()).title("testLink").parent(testFolder).target("https://google.com").linkType("EXTERNAL").nextPersisted(); + + seedMimeTypeFixture(); + } + + /** + * Seeds a dedicated Site and folder tree for the MIME-type routing tests of + * issue #36916. It is kept apart from the + * fixture above so the assertions can be exact about which items a MIME-filtered browse returns. + *

+     * mimeFolder
+     *      |_ mimePage             HTMLPAGE,  default language
+     *      |_ mimePageAltLanguage  HTMLPAGE,  testLanguage
+     *      |_ mimeJpgFile          FILEASSET, image/jpeg
+     *      |_ mimePdfFile          FILEASSET, application/pdf
+     *      |_ mimeTxtFile          FILEASSET, text/plain
+     *      |_ mimeLink             LINK
+     *      |_ mimeSubFolder
+     *          |_ mimeSubFolderPage      HTMLPAGE,  default language
+     *          |_ mimeSubFolderJpgFile   FILEASSET, image/jpeg
+     * 
+ */ + private static void seedMimeTypeFixture() throws Exception { + mimeHost = new SiteDataGen().nextPersisted(); + mimeFolder = new FolderDataGen().name("mimeFolder").site(mimeHost).nextPersisted(); + mimeSubFolder = new FolderDataGen().name("mimeSubFolder").parent(mimeFolder).nextPersisted(); + + final Template template = new TemplateDataGen().host(mimeHost).nextPersisted(); + + mimePage = new HTMLPageDataGen(mimeFolder, template).title("mimePage").pageURL("mime-page") + .nextPersisted(); + mimePageAltLanguage = new HTMLPageDataGen(mimeFolder, template).title("mimePageAltLanguage") + .pageURL("mime-page-alt-language").languageId(testLanguage.getId()).nextPersisted(); + mimeSubFolderPage = new HTMLPageDataGen(mimeSubFolder, template).title("mimeSubFolderPage") + .pageURL("mime-sub-folder-page").nextPersisted(); + + mimeJpgFile = persistFileAsset(mimeFolder, copyTestResource("/images/test.jpg", "mimeJpgFile", ".jpg")); + mimePdfFile = persistFileAsset(mimeFolder, copyTestResource( + "/com/dotmarketing/portlets/contentlet/business/test_files/test.pdf", "mimePdfFile", ".pdf")); + mimeTxtFile = persistFileAsset(mimeFolder, + FileUtil.createTemporaryFile("mimeTxtFile", ".txt", "this is a test!")); + mimeSubFolderJpgFile = persistFileAsset(mimeSubFolder, + copyTestResource("/images/test.jpg", "mimeSubFolderJpgFile", ".jpg")); + + mimeLink = new LinkDataGen().hostId(mimeHost.getIdentifier()).title("mimeLink").parent(mimeFolder) + .target("https://google.com").linkType("EXTERNAL").nextPersisted(); + } + + /** + * Copies a classpath test resource into a temporary file so that a File Asset can be generated from it. The + * file extension matters here: the asset metadata -- and therefore the {@code contentType} the SQL MIME + * predicate looks at -- is derived from the actual file contents on check-in. + */ + private static File copyTestResource(final String resourcePath, final String prefix, final String suffix) + throws IOException { + final URL url = BrowserAPITest.class.getResource(resourcePath); + assertNotNull("Test resource must exist in the classpath: " + resourcePath, url); + final File tempFile = File.createTempFile(prefix, suffix); + FileUtils.copyFile(new File(url.getFile()), tempFile); + return tempFile; + } + + private static FileAsset persistFileAsset(final Folder folder, final File file) + throws DotDataException, DotSecurityException { + return APILocator.getFileAssetAPI().fromContentlet(new FileAssetDataGen(file).folder(folder) + .setPolicy(IndexPolicy.WAIT_FOR).nextPersisted()); + } + + /** + * Runs a browse against the MIME-type fixture folder through {@link BrowserAPI#getFolderContentList(BrowserQuery)} + * and returns the Identifiers that came back. This entry point deliberately has no in-memory MIME filter + * (see research R2), so what it returns is what the SQL predicate itself selected. + */ + private Set browseIdentifiers(final Folder folder, final List mimeTypes) + throws DotSecurityException, DotDataException { + return browserAPI.getFolderContentList(BrowserQuery.builder() + .withUser(APILocator.systemUser()) + .withHostOrFolderId(folder.getIdentifier()) + .showPages(true) + .showFiles(true) + .showDotAssets(true) + .showFolders(false) + .showWorking(true) + .showMimeTypes(mimeTypes) + .build()).stream() + .map(Treeable::getIdentifier) + .collect(Collectors.toSet()); } /** @@ -2125,4 +2220,227 @@ public void test_getPaginatedContents_scanLimitStopsLoop() throws Exception { BrowserAPIImpl.BROWSER_DB_MAX_SCAN_ROWS_DEFAULT); } } + + /** + * + * Contract case C1 -- AC-001 and AC-006 of #36916. + */ + @Test + public void test_getFolderContentList_dotPageMimeType_returnsPages() throws Exception { + final Set identifiers = browseIdentifiers(mimeFolder, List.of(DOTPAGE_MIME_TYPE)); + + assertTrue("Pages must be returned for an '" + DOTPAGE_MIME_TYPE + "' browse, but got: " + identifiers, + identifiers.containsAll( + Set.of(mimePage.getIdentifier(), mimePageAltLanguage.getIdentifier()))); + assertFalse("The JPG File Asset must not be returned for an '" + DOTPAGE_MIME_TYPE + "' browse", + identifiers.contains(mimeJpgFile.getIdentifier())); + assertFalse("The PDF File Asset must not be returned for an '" + DOTPAGE_MIME_TYPE + "' browse", + identifiers.contains(mimePdfFile.getIdentifier())); + assertFalse("The TXT File Asset must not be returned for an '" + DOTPAGE_MIME_TYPE + "' browse", + identifiers.contains(mimeTxtFile.getIdentifier())); + } + + /** + *
    + *
  • Method to test: {@link BrowserAPI#getFolderContentList(BrowserQuery)}
  • + *
  • Given Scenario: The same folder is browsed filtering by a real MIME type, {@code image/jpeg}, + * with Pages enabled.
  • + *
  • Expected Result: Only the matching File Asset comes back. Pages must be dropped by the SQL + * predicate itself, since two of the three consumers of the query have no in-memory MIME filter.
  • + *
+ * Contract case C2, invariant I2 -- AC-005 and AC-007 of + * #36916. + */ + @Test + public void test_getFolderContentList_realMimeType_returnsMatchingFilesAndNoPages() throws Exception { + final Set identifiers = browseIdentifiers(mimeFolder, List.of("image/jpeg")); + + assertTrue("The JPG File Asset must be returned for an 'image/jpeg' browse, but got: " + identifiers, + identifiers.contains(mimeJpgFile.getIdentifier())); + assertFalse("The PDF File Asset must not match 'image/jpeg'", + identifiers.contains(mimePdfFile.getIdentifier())); + assertFalse("The TXT File Asset must not match 'image/jpeg'", + identifiers.contains(mimeTxtFile.getIdentifier())); + assertFalse("Pages must never leak into a real MIME type browse", + identifiers.contains(mimePage.getIdentifier())); + assertFalse("Pages must never leak into a real MIME type browse", + identifiers.contains(mimePageAltLanguage.getIdentifier())); + } + + /** + *
    + *
  • Method to test: {@link BrowserAPI#getFolderContentList(BrowserQuery)}
  • + *
  • Given Scenario: The folder is browsed filtering by two real MIME types at once.
  • + *
  • Expected Result: Both matching File Assets come back and nothing else. This is the pure + * regression guard for the MIME filtering added by PR #34217, and it must behave identically before and + * after the fix.
  • + *
+ * Contract case C4, invariant I1 -- AC-005 of + * #36916. + */ + @Test + public void test_getFolderContentList_multipleRealMimeTypes_areUnchanged() throws Exception { + final Set identifiers = browseIdentifiers(mimeFolder, List.of("image/jpeg", "application/pdf")); + + assertTrue("The JPG File Asset must be returned, but got: " + identifiers, + identifiers.contains(mimeJpgFile.getIdentifier())); + assertTrue("The PDF File Asset must be returned, but got: " + identifiers, + identifiers.contains(mimePdfFile.getIdentifier())); + assertFalse("The TXT File Asset matches neither MIME type", + identifiers.contains(mimeTxtFile.getIdentifier())); + assertFalse("Pages must never leak into a real MIME type browse", + identifiers.contains(mimePage.getIdentifier())); + } + + /** + *
    + *
  • Method to test: {@link BrowserAPI#getFolderContentList(BrowserQuery)}
  • + *
  • Given Scenario: The folder is browsed with a MIME type that merely starts with the synthetic + * one, {@code application/dotpage-foo}.
  • + *
  • Expected Result: No Page comes back. Only the exact string {@code application/dotpage} may be + * routed to the base type branch; anything else goes through the asset metadata check.
  • + *
+ * Invariant I3 of #36916. + */ + @Test + public void test_getFolderContentList_dotPageMimeTypePrefix_isNotRoutedToBaseType() throws Exception { + final Set identifiers = browseIdentifiers(mimeFolder, List.of(DOTPAGE_MIME_TYPE + "-foo")); + + assertFalse("Only the exact '" + DOTPAGE_MIME_TYPE + "' may resolve Pages by base type", + identifiers.contains(mimePage.getIdentifier())); + assertFalse("Only the exact '" + DOTPAGE_MIME_TYPE + "' may resolve Pages by base type", + identifiers.contains(mimePageAltLanguage.getIdentifier())); + } + + /** + *
    + *
  • Method to test: {@link BrowserAPI#getFolderContentList(BrowserQuery)}
  • + *
  • Given Scenario: The folder is browsed asking for the synthetic Page MIME type and a real one + * at the same time -- a folder holding a mix of Pages, File Assets and a Link.
  • + *
  • Expected Result: Both the Pages and the matching File Asset come back; the File Assets that + * match neither requested MIME type do not.
  • + *
+ * Contract case C3 -- AC-004 of #36916. + */ + @Test + public void test_getFolderContentList_mixedMimeTypes_returnsPagesAndMatchingFiles() throws Exception { + final Set identifiers = + browseIdentifiers(mimeFolder, List.of(DOTPAGE_MIME_TYPE, "image/jpeg")); + + assertTrue("Pages must be returned for a mixed browse, but got: " + identifiers, + identifiers.containsAll( + Set.of(mimePage.getIdentifier(), mimePageAltLanguage.getIdentifier()))); + assertTrue("The JPG File Asset must be returned for a mixed browse, but got: " + identifiers, + identifiers.contains(mimeJpgFile.getIdentifier())); + assertFalse("The PDF File Asset matches neither requested MIME type", + identifiers.contains(mimePdfFile.getIdentifier())); + assertFalse("The TXT File Asset matches neither requested MIME type", + identifiers.contains(mimeTxtFile.getIdentifier())); + } + + /** + *
    + *
  • Method to test: {@link BrowserAPI#getFolderContentList(BrowserQuery)}
  • + *
  • Given Scenario: The same mixed browse is requested twice with the MIME types in opposite + * order.
  • + *
  • Expected Result: Both requests select exactly the same rows -- the requested MIME types are + * an unordered set as far as the generated predicate is concerned.
  • + *
+ * Invariant I5 of #36916. + */ + @Test + public void test_getFolderContentList_mimeTypeOrder_doesNotChangeResults() throws Exception { + final Set pageFirst = + browseIdentifiers(mimeFolder, List.of(DOTPAGE_MIME_TYPE, "image/jpeg")); + final Set imageFirst = + browseIdentifiers(mimeFolder, List.of("image/jpeg", DOTPAGE_MIME_TYPE)); + + assertEquals("The order of the requested MIME types must not change the result set", pageFirst, + imageFirst); + } + + /** + *
    + *
  • Method to test: {@link BrowserAPI#getFolderContentList(BrowserQuery)}
  • + *
  • Given Scenario: A sub-folder holding a Page and a JPG File Asset is browsed, first by the + * synthetic Page MIME type and then by a real one.
  • + *
  • Expected Result: Each browse returns only its own kind, and neither returns items from the + * parent folder. Folders themselves are listed separately by the Browser API and are out of scope here.
  • + *
+ * AC-004 of #36916. + */ + @Test + public void test_getFolderContentList_subFolder_filtersEachTypeAsExpected() throws Exception { + final Set pages = browseIdentifiers(mimeSubFolder, List.of(DOTPAGE_MIME_TYPE)); + + assertTrue("The sub-folder Page must be returned, but got: " + pages, + pages.contains(mimeSubFolderPage.getIdentifier())); + assertFalse("The sub-folder JPG File Asset must not match '" + DOTPAGE_MIME_TYPE + "'", + pages.contains(mimeSubFolderJpgFile.getIdentifier())); + assertFalse("A sub-folder browse must not return items from its parent folder", + pages.contains(mimePage.getIdentifier())); + + final Set images = browseIdentifiers(mimeSubFolder, List.of("image/jpeg")); + + assertTrue("The sub-folder JPG File Asset must be returned, but got: " + images, + images.contains(mimeSubFolderJpgFile.getIdentifier())); + assertFalse("Pages must never leak into a real MIME type browse", + images.contains(mimeSubFolderPage.getIdentifier())); + } + + /** + *
    + *
  • Method to test: {@link BrowserAPI#getFolderContentList(BrowserQuery)}
  • + *
  • Given Scenario: Pages are browsed by the synthetic MIME type under a specific language, with + * and without the default language fallback the legacy dialog turns on.
  • + *
  • Expected Result: With the fallback on, both the Page in the requested language and the one in + * the default language come back; with it off, only the Page in the requested language does. Language + * resolution is unchanged by the MIME routing.
  • + *
+ * AC-008 of #36916. + */ + @Test + public void test_getFolderContentList_dotPageMimeType_honorsLanguageFallback() throws Exception { + final Set withFallback = browseIdentifiersByLanguage(testLanguage.getId(), true); + + assertTrue("The Page in the requested language must be returned, but got: " + withFallback, + withFallback.contains(mimePageAltLanguage.getIdentifier())); + assertTrue("The default language Page must be returned when the fallback is on, but got: " + withFallback, + withFallback.contains(mimePage.getIdentifier())); + + final Set withoutFallback = browseIdentifiersByLanguage(testLanguage.getId(), false); + + assertTrue("The Page in the requested language must be returned, but got: " + withoutFallback, + withoutFallback.contains(mimePageAltLanguage.getIdentifier())); + assertFalse("The default language Page must not be returned when the fallback is off", + withoutFallback.contains(mimePage.getIdentifier())); + } + + /** + * Same browse as {@link #browseIdentifiers(Folder, List)} but scoped to a language, mirroring how the legacy + * dialog resolves content -- see {@code BrowserAjax.getFolderContentWithDotAssets}. + */ + private Set browseIdentifiersByLanguage(final long languageId, final boolean showDefaultLangItems) + throws DotSecurityException, DotDataException { + return browserAPI.getFolderContentList(BrowserQuery.builder() + .withUser(APILocator.systemUser()) + .withHostOrFolderId(mimeFolder.getIdentifier()) + .showPages(true) + .showFiles(true) + .showFolders(false) + .showWorking(true) + .withLanguageId(languageId) + .showDefaultLangItems(showDefaultLangItems) + .showMimeTypes(List.of(DOTPAGE_MIME_TYPE)) + .build()).stream() + .map(Treeable::getIdentifier) + .collect(Collectors.toSet()); + } } diff --git a/dotcms-integration/src/test/java/com/dotmarketing/portlets/browser/ajax/BrowserAjaxTest.java b/dotcms-integration/src/test/java/com/dotmarketing/portlets/browser/ajax/BrowserAjaxTest.java index 31e9d8abb194..1f94703ac969 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/portlets/browser/ajax/BrowserAjaxTest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/portlets/browser/ajax/BrowserAjaxTest.java @@ -49,6 +49,9 @@ */ public class BrowserAjaxTest { + /** Synthetic MIME type the legacy browser dialogs send for Pages. See {@code PageViewStrategy}. */ + private static final String DOTPAGE_MIME_TYPE = "application/dotpage"; + private static Host testSite = null; private static Folder parentFolderOne = null; private static Folder parentFolderTwo = null; @@ -361,4 +364,38 @@ public void test_getHosts_ShoudlRetrieveSystemHosts() throws DotDataException, D assertTrue(hosts.stream().anyMatch(host -> host.get("identifier").equals(APILocator.systemHost().getIdentifier())) && loggedInUser.getFirstName().equals(user.getFirstName()) ); setUpDwrContext(APILocator.getUserAPI().getSystemUser()); } + + /** + *
    + *
  • Method to test: {@link BrowserAjax#getFolderContentWithDotAssets(String, int, int, String, + * List, List, boolean, boolean, boolean, String, boolean, boolean, boolean)}
  • + *
  • Given Scenario: The legacy "Select a file" / "Select link" dialog browses a folder that + * contains a Page, passing the synthetic {@code application/dotpage} MIME type. This is the exact call the + * redirect target picker makes from a Page's Properties.
  • + *
  • Expected Result: The Page is listed and {@code total} is greater than zero -- instead of the + * {@code {total: 0, list: []}} the dialog reports today.
  • + *
+ * AC-001 and AC-003 of #36916. + */ + @Test + public void test_getFolderContentWithDotAssets_dotPageMimeType_returnsPages() throws Exception { + setUpDwrContext(APILocator.getUserAPI().getSystemUser()); + + final Host host = new SiteDataGen().nextPersisted(); + final Folder folder = new FolderDataGen().site(host).nextPersisted(); + final Template template = new TemplateDataGen().host(host).nextPersisted(); + final HTMLPageAsset page = new HTMLPageDataGen(folder, template).nextPersisted(); + + final BrowserAjax browserAjax = new BrowserAjax(); + final Map results = browserAjax.getFolderContentWithDotAssets(folder.getIdentifier(), 0, + 100, "", List.of(DOTPAGE_MIME_TYPE), List.of(), false, true, false, "moddate", true, true, false); + + assertNotNull("The dialog must always get a result map back", results); + final List> contentList = (List>) results.get("list"); + assertNotNull("The dialog result must carry a list", contentList); + assertTrue("The dialog must report at least one item for a folder that contains a Page, but reported: " + + results.get("total"), ((Integer) results.get("total")) > 0); + assertTrue("The Page must be listed for an '" + DOTPAGE_MIME_TYPE + "' browse", + contentList.stream().anyMatch(item -> page.getIdentifier().equals(item.get("identifier")))); + } } diff --git a/dotcms-postman/src/main/resources/postman/Browser_Resource.postman_collection.json b/dotcms-postman/src/main/resources/postman/Browser_Resource.postman_collection.json index 33865a6e5081..bc4aad775e13 100644 --- a/dotcms-postman/src/main/resources/postman/Browser_Resource.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/Browser_Resource.postman_collection.json @@ -362,6 +362,136 @@ }, "response": [] }, + { + "name": "GetFiles - dotpage MIME type returns Pages only", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code should be 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "// https://github.com/dotCMS/core/issues/36916 - 'application/dotpage' is a synthetic MIME", + "// type that is never persisted, so it must resolve Pages by their HTMLPAGE base type.", + "pm.test(\"Only Pages come back for an application/dotpage browse\", function () {", + " const list = pm.response.json().entity.list || [];", + " const nonPages = list.filter(item => item.mimeType && item.mimeType !== \"application/dotpage\");", + " pm.expect(nonPages, \"Non-Page items leaked into the browse: \" + JSON.stringify(nonPages)).to.be.empty;", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\t\"showLinks\":false,\n\t\"showDotAssets\":true,\n\t\"showPages\":true,\n\t\"showFiles\":true,\n\t\"showWorking\":true,\n\t\"mimeTypes\":[\"application/dotpage\"]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/browser", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "browser" + ] + }, + "description": "Regression guard for issue #36916: a browse filtered by the synthetic 'application/dotpage' MIME type must resolve Pages by base type instead of by asset metadata." + }, + "response": [] + }, + { + "name": "GetFiles - real MIME type excludes Pages", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code should be 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "// https://github.com/dotCMS/core/issues/36916 - Pages must be dropped by the SQL predicate", + "// itself for a real MIME type browse, not by any downstream in-memory filter.", + "pm.test(\"Pages never leak into a real MIME type browse\", function () {", + " const list = pm.response.json().entity.list || [];", + " const pages = list.filter(item => item.mimeType === \"application/dotpage\");", + " pm.expect(pages, \"Pages leaked into an image/jpeg browse: \" + JSON.stringify(pages)).to.be.empty;", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\t\"showLinks\":false,\n\t\"showDotAssets\":true,\n\t\"showPages\":true,\n\t\"showFiles\":true,\n\t\"showWorking\":true,\n\t\"mimeTypes\":[\"image/jpeg\"]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/browser", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "browser" + ] + }, + "description": "Regression guard for issue #36916 / PR #34217: filtering by a real MIME type must keep excluding Pages." + }, + "response": [] + }, { "name": "Get Select Folder 404", "event": [