diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeAPIImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeAPIImpl.java index adec005bc5d7..65e492e2785f 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeAPIImpl.java @@ -389,7 +389,7 @@ public Optional> find(final List varNames, final Strin contentTypeList = this.contentTypeFactory.find(lowercaseVarNames, filter.toLowerCase(), offset, limit, orderBy); } else if (offset > 0 || limit > 0) { int adjustedLimit = offset + limit; - adjustedLimit = adjustedLimit > lowercaseVarNames.size() ? lowercaseVarNames.size() : limit; + adjustedLimit = adjustedLimit > lowercaseVarNames.size() ? lowercaseVarNames.size() : adjustedLimit; final List varNamesSubList = lowercaseVarNames.subList(offset, adjustedLimit); contentTypeList = this.contentTypeFactory.find(varNamesSubList, null, internalOffset, internalLimit, orderBy); } else { diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactoryImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactoryImpl.java index ca1c23b58492..3e0f18ac0a5c 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactoryImpl.java @@ -178,7 +178,7 @@ public List find(final Collection varNames, final String fi final DotConnect dc = new DotConnect(); String sql = UtilMethods.isSet(filter) ? ContentTypeSql.SELECT_BY_VAR_NAMES_FILTERED : ContentTypeSql.SELECT_BY_VAR_NAMES; sql = String.format(sql, String.join(COMMA, Collections.nCopies(varNames.size(), "?"))); - if (UtilMethods.isSet(orderBy)) { + if (UtilMethods.isSet(orderBy) && !orderBy.contains(SQLUtil.DOT_NOT_SORT)) { // DOT_NOT_SORT is used to indicate no order by wanted sql = UtilMethods.isSet(orderBy) ? sql + ContentTypeSql.ORDER_BY : sql; final String sanitizedOrderBy = SQLUtil.sanitizeSortBy(orderBy); sql = String.format(sql, sanitizedOrderBy); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java index e1eeb368a4e5..4b973551a9c7 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java @@ -13,6 +13,7 @@ import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.contenttype.transform.contenttype.ContentTypeInternationalization; import com.dotcms.exception.ExceptionUtil; +import com.dotcms.rendering.velocity.services.PageRenderUtil; import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.rest.InitDataObject; import com.dotcms.rest.ResponseEntityView; @@ -23,20 +24,26 @@ import com.dotcms.rest.exception.BadRequestException; import com.dotcms.rest.exception.ForbiddenException; import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; +import com.dotcms.util.ConversionUtils; import com.dotcms.util.PaginationUtil; import com.dotcms.util.diff.DiffItem; import com.dotcms.util.diff.DiffResult; import com.dotcms.util.pagination.ContentTypesPaginator; import com.dotcms.util.pagination.OrderDirection; import com.dotcms.workflow.helper.WorkflowHelper; +import com.dotmarketing.beans.ContainerStructure; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.DotStateException; import com.dotmarketing.business.PermissionAPI; +import com.dotmarketing.common.util.SQLUtil; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.contentlet.model.ContentletVersionInfo; +import com.dotmarketing.portlets.htmlpageasset.model.IHTMLPage; import com.dotmarketing.portlets.workflows.business.WorkflowAPI; import com.dotmarketing.portlets.workflows.model.SystemActionWorkflowActionMapping; +import com.dotmarketing.util.Config; import com.dotmarketing.util.IdentifierValidator; import com.dotmarketing.util.Logger; import com.dotmarketing.util.PageMode; @@ -45,6 +52,7 @@ import com.dotmarketing.util.json.JSONException; import com.dotmarketing.util.json.JSONObject; import com.google.common.collect.ImmutableMap; +import com.liferay.portal.language.LanguageUtil; import com.liferay.portal.model.User; import com.liferay.util.StringPool; import io.swagger.v3.oas.annotations.ExternalDocumentation; @@ -56,6 +64,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import io.vavr.Lazy; import io.vavr.Tuple; import io.vavr.Tuple2; import io.vavr.control.Try; @@ -81,10 +90,17 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import static com.dotcms.util.DotPreconditions.checkNotEmpty; @@ -114,6 +130,8 @@ public class ContentTypeResource implements Serializable { private final WorkflowHelper workflowHelper; private final PermissionAPI permissionAPI; + private final Lazy> contentPaletteHiddenTypes = Lazy.of(()->Set.of(Config.getStringArrayProperty("CONTENT_PALETTE_HIDDEN_CONTENT_TYPES", new String[]{}))); + public ContentTypeResource() { this(ContentTypeHelper.getInstance(), new WebResource(), new PaginationUtil(new ContentTypesPaginator()), @@ -1593,4 +1611,293 @@ private T getFilterValue(final FilteredContentTypesForm form, final String p return UtilMethods.isSet(form.getFilter().get(param)) ? (T) form.getFilter().get(param) : defaultValue; } + /** + * Returns a list of {@link ContentType} objects based on the page containers/types on the layout + *
{@code
+	 * GET http://localhost:8080/api/v1/contenttype/page?pagePathOrId=48190c8c-42c4-46af-8d1a-0cd5db894797
+	 * }
+ *

If you want results composed of 10 items per page and you want the third page, and you + * don't have the Site's Identifier, you can call this URL:

+ *
{@code
+	 * GET http://localhost:8080/api/v1/contenttype/page?pagePathOrId=48190c8c-42c4-46af-8d1a-0cd5db894797
+	 * }
+ * + * @param httpRequest The current instance of the {@link HttpServletRequest}. + * @param httpResponse The current instance of the {@link HttpServletResponse}. + * @param filter Filtering parameter used to pass down the Content Types name, Velocity + * Variable Name, or Inode. You can pass down part of the characters. + * @param page The selected results page, for pagination purposes. + * @param perPage The number of results to return per page, for pagination purposes. + * @param orderByParam The column name that will be used to sort the paginated results. For + * reference, please check + * {@link com.dotmarketing.common.util.SQLUtil#ORDERBY_WHITELIST}. + * @param direction The direction of the sorting. It can be either "ASC" or "DESC". + * @param type The Velocity variable name of the Content Type to retrieve. + * @param siteId The identifier of the Site where the requested Content Types live. + * @param sites A comma-separated list of Site identifiers or Site Keys where the + * requested Content Types live. + * + * @return A JSON response with the paginated list of Content Types. + * + * @throws DotDataException An error occurred when retrieving information from the database. + */ + @GET + @Path("/page") + @JSONP + @NoCache + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + @Tag(name = "getPagesContentTypes", description = "Returns the content types valid for a page based on the container/types on the layout") + @Operation( + operationId = "getPagesContentTypes", + summary = "Retrieves a list of content types for a page", + description = "Returns a list of content type objects based on the filtering criteria.", + tags = {"Content Type"}, + responses = { + @ApiResponse(responseCode = "200", description = "Content types retrieved successfully", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + value = "{\n" + + " \"entity\": [\n" + + " {\n" + + " \"baseType\": \"string\",\n" + + " \"clazz\": \"string\",\n" + + " \"defaultType\": true,\n" + + " \"description\": \"string\",\n" + + " \"fixed\": true,\n" + + " \"folder\": \"string\",\n" + + " \"folderPath\": \"string\",\n" + + " \"host\": \"string\",\n" + + " \"iDate\": 0,\n" + + " \"icon\": \"string\",\n" + + " \"id\": \"string\",\n" + + " \"layout\": [],\n" + + " \"metadata\": {},\n" + + " \"modDate\": 0,\n" + + " \"multilingualable\": true,\n" + + " \"nEntries\": 0,\n" + + " \"name\": \"string\",\n" + + " \"siteName\": \"string\",\n" + + " \"sortOrder\": 0,\n" + + " \"system\": true,\n" + + " \"variable\": \"string\",\n" + + " \"versionable\": true,\n" + + " \"workflows\": []\n" + + " }\n" + + " ],\n" + + " \"errors\": [],\n" + + " \"i18nMessagesMap\": {},\n" + + " \"messages\": [],\n" + + " \"pagination\": {\n" + + " \"currentPage\": 0,\n" + + " \"perPage\": 0,\n" + + " \"totalEntries\": 0\n" + + " },\n" + + " \"permissions\": []\n" + + "}\n" + ) + } + ) + ), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "500", description = "Internal Server Error") + } + ) + public final Response getPagesContentTypes(@Context final HttpServletRequest httpRequest, + @Context final HttpServletResponse httpResponse, + + @QueryParam("pagePathOrId") @Parameter(schema = @Schema(type = "string"), + description = "The URL or Identifier of the page to filter content types for the palette" + ) final String pagePathOrId, + @DefaultValue("-1") @QueryParam("language") @Parameter( + schema = @Schema(type = "string"), + description = "Optional Language id" + ) String language, + @QueryParam(PaginationUtil.FILTER) @Parameter(schema = @Schema(type = "string"), + description = "String to filter/search for specific content types; leave blank to return all." + ) final String filter, + @DefaultValue("1") @QueryParam(PaginationUtil.PAGE) @Parameter(schema = @Schema(type = "integer"), + description = "Page number in response pagination.\n\nDefault: `1`" + ) final int page, + @DefaultValue("10") @QueryParam(PaginationUtil.PER_PAGE) @Parameter(schema = @Schema(type = "integer"), + description = "Number of results per page for pagination.\n\nDefault: `10`" + ) final int perPage, + @DefaultValue("usage") @QueryParam(PaginationUtil.ORDER_BY) @Parameter( + schema = @Schema(type = "string"), + description = "Column(s) to sort the results. Multiple columns can be " + + "combined in a comma-separated list. Column names can also be set " + + "within a SQL string function, such as `upper()`.\n\n" + + "Some possible values:\n\n" + + "`name`, `velocity_var_name`, `mod_date`, `sort_order`\n\n" + + "`description`, `structuretype`, `category`, `inode`" + ) String orderByParam, + @DefaultValue("ASC") @QueryParam(PaginationUtil.DIRECTION) @Parameter( + schema = @Schema( + type = "string", + allowableValues = {"ASC", "DESC"}, + defaultValue = "ASC", + required = true + ), + description = "Sort direction: choose between ascending or descending." + ) String direction, + @QueryParam(ContentTypesPaginator.HOST_PARAMETER_ID) @Parameter(schema = @Schema(type = "string"), + description = "Filter by site identifier." + ) final String siteId) throws DotDataException, DotSecurityException { + + final User user = new WebResource.InitBuilder(this.webResource) + .requestAndResponse(httpRequest, httpResponse) + .rejectWhenNoUser(true) + .init().getUser(); + + if (Objects.isNull(pagePathOrId)) { + + throw new BadRequestException("The 'pagePathOrId' parameter is required."); + } + + Logger.debug(this, ()-> "Getting Content Types for page: " + pagePathOrId); + + final Map extraParams = new HashMap<>(); + final PageMode pageMode = PageMode.get(httpRequest); + final long languageId = getLanguageId(language); + final Host site = getSite(siteId, user, pageMode.respectAnonPerms); // wondering if this should be current or default + final String orderBy = this.getOrderByRealName(orderByParam); + List typeVarNames = findPageContainersContentTypesVarnamesByPathOrIdAndFilter(pagePathOrId, site, + languageId, pageMode, user, filter); + final boolean isUsage = "usage".equalsIgnoreCase(orderBy); + + if (isUsage) { + + typeVarNames = doUsage(page, perPage, direction, user, typeVarNames, extraParams); + } + + if (null != siteId) { + extraParams.put(ContentTypesPaginator.HOST_PARAMETER_ID,siteId); + } + + if (UtilMethods.isSet(typeVarNames)) { + + Logger.debug(this, "Found Content Types for page: " + pagePathOrId + + " in site: " + (Objects.nonNull(site) ? site.getHostname() : "null") + + " with languageId: " + languageId + " and pageMode: " + pageMode + + " with types: " + typeVarNames); + extraParams.put(ContentTypesPaginator.TYPES_PARAMETER_NAME, typeVarNames); + } + + final PaginationUtil paginationUtil = new PaginationUtil(new ContentTypesPaginator(APILocator.getContentTypeAPI(user))); + return isUsage? + paginationUtil.getPage(httpRequest, user, filter, PaginationUtil.FIRST_PAGE_INDEX, // we already paginate the results, so we start at page 1. + perPage, SQLUtil.DOT_NOT_SORT , // if usage is set, I do not want sort on the db, so use dotNONE. + OrderDirection.valueOf(direction), extraParams): + paginationUtil.getPage(httpRequest, user, filter, page, perPage, orderBy, + OrderDirection.valueOf(direction), extraParams); + } // getPagesContentTypes. + + private static long getLanguageId(final String language) { + + final long userLanguageId = LanguageUtil.getLanguageId(language); + final long languageId = userLanguageId > 0 ? userLanguageId : APILocator.getLanguageAPI().getDefaultLanguage().getId(); + return languageId; + } + + private static Host getSite(final String siteId, final User user, final boolean respectAnonPerms) throws DotDataException, DotSecurityException { + return UtilMethods.isSet(siteId) ? + APILocator.getHostAPI().find(siteId, user, respectAnonPerms) : + APILocator.getHostAPI().findDefaultHost(user, respectAnonPerms); + } + + private List doUsage(final int page, + final int perPage, + final String direction, + final User user, List typeVarNames, + final Map extraParams) throws DotDataException { + + final boolean isAscending = OrderDirection.ASC.name().equalsIgnoreCase(direction); + final Map entriesByContentTypes = APILocator.getContentTypeAPI + (user, true).getEntriesByContentTypes(); + // here are filtered and sorted, but we need to paginate them. + this.sort(typeVarNames, isAscending, user, entriesByContentTypes); + typeVarNames = this.paginate(typeVarNames, page, perPage); + extraParams.put(ContentTypesPaginator.ENTRIES_BY_CONTENT_TYPES, entriesByContentTypes); + final Comparator> comparator = Comparator + .comparing((Map contentTypeMap) -> + ConversionUtils.toLong(contentTypeMap.getOrDefault(ContentTypesPaginator.N_ENTRIES_FIELD_NAME, -1l),-1l)); + extraParams.put(ContentTypesPaginator.COMPARATOR, isAscending?comparator:comparator.reversed()); + return typeVarNames; + } + + public List paginate(final List typeVarNames, final int page, final int perPage) { + + if (typeVarNames == null || typeVarNames.isEmpty() || perPage <= 0 || page <= 0) { + return Collections.emptyList(); + } + + int total = typeVarNames.size(); + int fromIndex = Math.min((page - 1) * perPage, total); + int toIndex = Math.min(fromIndex + perPage, total); + + return typeVarNames.subList(fromIndex, toIndex); + } + + private void sort(final List typeVarNames, final boolean ascending, + final User user, final Map entriesByContentTypes) { + + final Comparator comparator = Comparator + .comparing((String contentTypeVarName) -> + entriesByContentTypes.getOrDefault(contentTypeVarName.toLowerCase(), -1l)); + + typeVarNames.sort(ascending ? comparator : comparator.reversed()); + } + + /* + * This methods retrieves the page by path or ID, then extracts the content types from the containers + * Matching the filter criteria and removing the repeated ones and the ones from the blacklist. + */ + private List findPageContainersContentTypesVarnamesByPathOrIdAndFilter(final String pagePathOrId, + final Host site, + final long languageId, + final PageMode pageMode, + final User user, + final String filter) throws DotDataException, DotSecurityException { + + Logger.debug(this, ()-> "Getting Content Types for page: " + pagePathOrId + + " in site: " + (Objects.nonNull(site) ? site.getHostname() : "null") + + " with languageId: " + languageId + " and pageMode: " + pageMode); + + IHTMLPage htmlPage = Try.of(()->APILocator.getHTMLPageAssetAPI().getPageByPath( + pagePathOrId, site, languageId, pageMode.showLive)).getOrNull(); + + if (Objects.isNull(htmlPage)) { // try fallback by identifier + + final Optional contentletVersionInfoOpt = APILocator.getVersionableAPI().getContentletVersionInfo(pagePathOrId, languageId); + if (contentletVersionInfoOpt.isPresent()) { + htmlPage = APILocator.getHTMLPageAssetAPI().findPage(pageMode.showLive? + contentletVersionInfoOpt.get().getLiveInode(): contentletVersionInfoOpt.get().getWorkingInode(), + user, pageMode.respectAnonPerms); + } else { + // try as an inode. + htmlPage = APILocator.getHTMLPageAssetAPI().findPage(pagePathOrId, user, pageMode.respectAnonPerms); + } + } + + if (Objects.isNull(htmlPage)) { // still null, so the page does not exist + + throw new BadRequestException( + String.format("Page with path or ID '%s' was not found", pagePathOrId)); + } + + final Set repeatedTypes = new HashSet<>(); + + // Retrieves the containers associated to the page, then extracts the content types for each container filtering the ones do not allowed + return new PageRenderUtil(htmlPage, user, pageMode, languageId, site) + .getContainersRaw().stream().map(containerRaw -> containerRaw.getContainerStructures()) + .flatMap(Collection::stream) + .map(ContainerStructure::getContentTypeVar) + .filter(Objects::nonNull) + .filter(Predicate.not(this.contentPaletteHiddenTypes.get()::contains)) + .filter(repeatedTypes::add) + .filter(varname -> filter == null || varname.toLowerCase().contains(filter.toLowerCase())) + .collect(Collectors.toList()); + } // findPageContainersContentTypesVarnamesByPathOrIdAndFilter } diff --git a/dotCMS/src/main/java/com/dotcms/util/pagination/ContentTypesPaginator.java b/dotCMS/src/main/java/com/dotcms/util/pagination/ContentTypesPaginator.java index 7ec15b6f98d5..2f85759191dd 100644 --- a/dotCMS/src/main/java/com/dotcms/util/pagination/ContentTypesPaginator.java +++ b/dotCMS/src/main/java/com/dotcms/util/pagination/ContentTypesPaginator.java @@ -19,12 +19,15 @@ import com.dotmarketing.util.PaginatedArrayList; import com.dotmarketing.util.UtilMethods; import com.liferay.portal.model.User; +import com.liferay.util.StringPool; import com.rainerhahnekamp.sneakythrow.Sneaky; import io.vavr.control.Try; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -39,11 +42,17 @@ */ public class ContentTypesPaginator implements PaginatorOrdered>{ - private static final String N_ENTRIES_FIELD_NAME = "nEntries"; + public static final String N_ENTRIES_FIELD_NAME = "nEntries"; public static final String TYPE_PARAMETER_NAME = "type"; public static final String TYPES_PARAMETER_NAME = "types"; public static final String HOST_PARAMETER_ID = "host"; public static final String SITES_PARAMETER_NAME = "sites"; + public static final String COMPARATOR = "comparator"; + public static final String ENTRIES_BY_CONTENT_TYPES = "entriesByContentTypes"; + public static final String VARIABLE = "variable"; + public static final String WORKFLOWS = "workflows"; + public static final String SYSTEM_ACTION_MAPPINGS = "systemActionMappings"; + private final ContentTypeAPI contentTypeAPI; private final WorkflowAPI workflowAPI; @@ -102,8 +111,12 @@ public PaginatedArrayList> getItems(final User user, final S final List> contentTypesTransform = transformContentTypesToMap(contentTypes); setEntriesAttribute(user, contentTypesTransform, this.workflowAPI.findSchemesMapForContentType(contentTypes), - this.workflowAPI.findSystemActionsMapByContentType(contentTypes, user)); - result.addAll(contentTypesTransform); + this.workflowAPI.findSystemActionsMapByContentType(contentTypes, user), + extraParams); + + result.addAll(Objects.nonNull(extraParams) && extraParams.containsKey(COMPARATOR)? + contentTypesTransform.stream().sorted((Comparator>) extraParams.get(COMPARATOR)).collect(Collectors.toList()) + :contentTypesTransform); return result; } catch (final DotDataException | DotSecurityException e) { final String errorMsg = String.format("An error occurred when retrieving paginated Content Types: " + @@ -127,13 +140,15 @@ public PaginatedArrayList> getItems(final User user, final S */ private void setEntriesAttribute(final User user, final List> contentTypesTransform, final Map> workflowSchemes, - final Map> systemActionMappings) { + final Map> systemActionMappings, + final Map extraParams) { Map entriesByContentTypes = null; try { - entriesByContentTypes = APILocator.getContentTypeAPI - (user, true).getEntriesByContentTypes(); + entriesByContentTypes = Objects.nonNull(extraParams) && extraParams.containsKey(ENTRIES_BY_CONTENT_TYPES)? + (Map)extraParams.get(ENTRIES_BY_CONTENT_TYPES): + APILocator.getContentTypeAPI(user, true).getEntriesByContentTypes(); } catch (final DotStateException | DotDataException e) { final String errorMsg = String.format("Error trying to retrieve total entries by Content Type: %s", e.getMessage()); Logger.error(ContentTypesPaginator.class, errorMsg, e); @@ -142,7 +157,7 @@ private void setEntriesAttribute(final User user, final List for (final Map contentTypeEntry : contentTypesTransform) { - final String variable = (String) contentTypeEntry.get("variable"); + final String variable = (String) contentTypeEntry.get(VARIABLE); if (entriesByContentTypes != null) { final String key = variable.toLowerCase(); @@ -150,17 +165,17 @@ private void setEntriesAttribute(final User user, final List entriesByContentTypes.get(key); contentTypeEntry.put(N_ENTRIES_FIELD_NAME, contentTypeEntriesNumber); } else { - contentTypeEntry.put(N_ENTRIES_FIELD_NAME, "N/A"); + contentTypeEntry.put(N_ENTRIES_FIELD_NAME, StringPool.NA); } if (workflowSchemes.containsKey(variable)) { - contentTypeEntry.put("workflows", workflowSchemes.get(variable)); + contentTypeEntry.put(WORKFLOWS, workflowSchemes.get(variable)); } if (systemActionMappings.containsKey(variable)) { - contentTypeEntry.put("systemActionMappings", workflowSchemes.get(variable)); + contentTypeEntry.put(SYSTEM_ACTION_MAPPINGS, systemActionMappings.get(variable)); } } } diff --git a/dotCMS/src/main/java/com/dotcms/util/pagination/PaginatorOrdered.java b/dotCMS/src/main/java/com/dotcms/util/pagination/PaginatorOrdered.java index 255340b71dff..d9ac41e1ebcf 100644 --- a/dotCMS/src/main/java/com/dotcms/util/pagination/PaginatorOrdered.java +++ b/dotCMS/src/main/java/com/dotcms/util/pagination/PaginatorOrdered.java @@ -3,6 +3,7 @@ import com.dotmarketing.util.PaginatedArrayList; import com.liferay.portal.model.User; +import java.util.Collections; import java.util.Map; /** @@ -102,7 +103,7 @@ default PaginatedArrayList getItems(final User user, final String filter, fin * @return A {@link PaginatedArrayList} of items matching the specified search criteria. */ default PaginatedArrayList getItems(final User user, final String filter, final int limit, final int offset, final String orderBy, final OrderDirection direction) { - return getItems(user, filter, limit, offset, orderBy, direction, null); + return getItems(user, filter, limit, offset, orderBy, direction, Collections.emptyMap()); } } diff --git a/dotCMS/src/main/java/com/dotmarketing/common/util/SQLUtil.java b/dotCMS/src/main/java/com/dotmarketing/common/util/SQLUtil.java index b25124211c0d..05cc8642177a 100644 --- a/dotCMS/src/main/java/com/dotmarketing/common/util/SQLUtil.java +++ b/dotCMS/src/main/java/com/dotmarketing/common/util/SQLUtil.java @@ -53,6 +53,8 @@ public class SQLUtil { private static final SecurityLoggerServiceAPI securityLoggerServiceAPI = APILocator.getSecurityLogger(); + // When you need to send a sort but do not want to actually sort by anything + public static final String DOT_NOT_SORT = "dotnosort"; public static final String ASC = "asc"; public static final String DESC = "desc"; public static final String _ASC = " " + ASC ; diff --git a/dotCMS/src/main/java/com/liferay/util/StringPool.java b/dotCMS/src/main/java/com/liferay/util/StringPool.java index 7aae9e980f25..6043d2095573 100644 --- a/dotCMS/src/main/java/com/liferay/util/StringPool.java +++ b/dotCMS/src/main/java/com/liferay/util/StringPool.java @@ -93,5 +93,5 @@ public class StringPool { public static final String UNKNOWN = "UNKNOWN"; - + public static final String NA = "N/A"; } diff --git a/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml b/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml index ef170b8c8e70..632b7d31237e 100644 --- a/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml +++ b/dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml @@ -42,6 +42,9 @@ tags: name: Containers - description: Endpoints for managing content and contentlets name: Content +- description: Returns the content types valid for a page based on the container/types + on the layout + name: getPagesContentTypes - description: Endpoints for managing folder structure and organization name: Folders - description: Form management and processing @@ -6713,6 +6716,122 @@ paths: summary: Updates a content type tags: - Content Type + /v1/contenttype/page: + get: + description: Returns a list of content type objects based on the filtering criteria. + operationId: getPagesContentTypes + parameters: + - description: The URL or Identifier of the page to filter content types for + the palette + in: query + name: pagePathOrId + schema: + type: string + - description: Optional Language id + in: query + name: language + schema: + type: string + default: "-1" + - description: String to filter/search for specific content types; leave blank + to return all. + in: query + name: filter + schema: + type: string + - description: |- + Page number in response pagination. + + Default: `1` + in: query + name: page + schema: + type: integer + format: int64 + default: 1 + - description: |- + Number of results per page for pagination. + + Default: `10` + in: query + name: per_page + schema: + type: integer + format: int64 + default: 10 + - description: |- + Column(s) to sort the results. Multiple columns can be combined in a comma-separated list. Column names can also be set within a SQL string function, such as `upper()`. + + Some possible values: + + `name`, `velocity_var_name`, `mod_date`, `sort_order` + + `description`, `structuretype`, `category`, `inode` + in: query + name: orderby + schema: + type: string + default: usage + - description: "Sort direction: choose between ascending or descending." + in: query + name: direction + schema: + type: string + default: ASC + enum: + - ASC + - DESC + - description: Filter by site identifier. + in: query + name: host + schema: + type: string + responses: + "200": + content: + application/json: + example: + entity: + - baseType: string + clazz: string + defaultType: true + description: string + fixed: true + folder: string + folderPath: string + host: string + iDate: 0 + icon: string + id: string + layout: [] + metadata: {} + modDate: 0 + multilingualable: true + nEntries: 0 + name: string + siteName: string + sortOrder: 0 + system: true + variable: string + versionable: true + workflows: [] + errors: [] + i18nMessagesMap: {} + messages: [] + pagination: + currentPage: 0 + perPage: 0 + totalEntries: 0 + permissions: [] + description: Content types retrieved successfully + "403": + description: Forbidden + "500": + description: Internal Server Error + summary: Retrieves a list of content types for a page + tags: + - getPagesContentTypes + - Content Type /v1/contenttype/{baseVariableName}/_copy: post: description: |- diff --git a/dotcms-postman/src/main/resources/postman/ContentTypePages.postman_collection.json b/dotcms-postman/src/main/resources/postman/ContentTypePages.postman_collection.json new file mode 100644 index 000000000000..34a70f2dc85b --- /dev/null +++ b/dotcms-postman/src/main/resources/postman/ContentTypePages.postman_collection.json @@ -0,0 +1,498 @@ +{ + "info": { + "_postman_id": "eb368c08-8543-40cf-b207-fc4ec18294cd", + "name": "ContentTypePagesResource", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "781456" + }, + "item": [ + { + "name": "ContentTypeForPages", + "item": [ + { + "name": "Logout", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/logout", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "logout" + ] + }, + "description": "Logout before try with invalid user" + }, + "response": [] + }, + { + "name": "GetCurrentSite", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 \", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Storing workflow data\", function () {", + " var jsonData = pm.response.json();", + " pm.collectionVariables.set(\"currentSiteHost\", jsonData.entity.hostname);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/site/currentSite", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "site", + "currentSite" + ] + } + }, + "response": [] + }, + { + "name": "GetFirstPage", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 \", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "var jsonData = pm.response.json();", + "pm.collectionVariables.set(\"currentPageId\", jsonData.contentlets[0].identifier);", + "pm.collectionVariables.set(\"currentPageUrl\", jsonData.contentlets[0].url);", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/content/render/false/query/+contentType:htmlpageasset", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "content", + "render", + "false", + "query", + "+contentType:htmlpageasset" + ] + } + }, + "response": [] + }, + { + "name": "CheckContentTypeForPages", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 \", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Storing workflow data\", function () {", + " let jsonData = pm.response.json();", + " let length = jsonData.entity.length;", + " let currentPage = jsonData.pagination.currentPage;", + " let perPage = jsonData.pagination.perPage;", + " let totalEntries = jsonData.pagination.totalEntries;", + " ", + " pm.expect(length).to.greaterThan(0)", + " pm.expect(totalEntries).to.greaterThan(0)", + " pm.expect(perPage).to.eql(10)", + " pm.expect(currentPage).to.eql(1)", + "", + "});", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/contenttype/page?pagePathOrId={{currentPageId}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "contenttype", + "page" + ], + "query": [ + { + "key": "pagePathOrId", + "value": "{{currentPageId}}" + } + ] + } + }, + "response": [] + }, + { + "name": "CheckContentTypeForPagesByUrl", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 \", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Checking Content Type Information\", function () {", + " let jsonData = pm.response.json();", + " let length = jsonData.entity.length;", + " let currentPage = jsonData.pagination.currentPage;", + " let perPage = jsonData.pagination.perPage;", + " let totalEntries = jsonData.pagination.totalEntries;", + " ", + " pm.expect(length).to.greaterThan(0)", + " pm.expect(totalEntries).to.greaterThan(0)", + " pm.expect(perPage).to.eql(10)", + " pm.expect(currentPage).to.eql(1)", + "", + "});", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/contenttype/page?pagePathOrId={{currentPageUrl}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "contenttype", + "page" + ], + "query": [ + { + "key": "pagePathOrId", + "value": "{{currentPageUrl}}" + } + ] + } + }, + "response": [] + }, + { + "name": "CheckContentTypeForPagesBadRequest", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400 \", function () {", + " pm.response.to.have.status(400);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/contenttype/page?pagePathOrId=NONEXIST", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "contenttype", + "page" + ], + "query": [ + { + "key": "pagePathOrId", + "value": "NONEXIST" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "if (!pm.environment.get('jwt')) {", + " console.log(\"generating....\")", + " const serverURL = pm.environment.get('serverURL'); // Get the server URL from the environment variable", + " const apiUrl = `${serverURL}/api/v1/apitoken`; // Construct the full API URL", + "", + " if (!pm.environment.get('jwt')) {", + " const username = pm.environment.get(\"user\");", + " const password = pm.environment.get(\"password\");", + " const basicAuth = Buffer.from(`${username}:${password}`).toString('base64');", + "", + " const requestOptions = {", + " url: apiUrl,", + " method: \"POST\",", + " header: {", + " \"accept\": \"*/*\",", + " \"content-type\": \"application/json\",", + " \"Authorization\": `Basic ${basicAuth}`", + " },", + " body: {", + " mode: \"raw\",", + " raw: JSON.stringify({", + " \"expirationSeconds\": 7200,", + " \"userId\": \"dotcms.org.1\",", + " \"network\": \"0.0.0.0/0\",", + " \"claims\": {\"label\": \"postman-tests\"}", + " })", + " }", + " };", + "", + " pm.sendRequest(requestOptions, function (err, response) {", + " if (err) {", + " console.log(err);", + " } else {", + " const jwt = response.json().entity.jwt;", + " pm.environment.set('jwt', jwt);", + " console.log(jwt);", + " }", + " });", + " }", + "}", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "if (!pm.environment.get('jwt')) {", + " console.log(\"generating....\")", + " const serverURL = pm.environment.get('serverURL'); // Get the server URL from the environment variable", + " const apiUrl = `${serverURL}/api/v1/apitoken`; // Construct the full API URL", + "", + " if (!pm.environment.get('jwt')) {", + " const username = pm.environment.get(\"user\");", + " const password = pm.environment.get(\"password\");", + " const basicAuth = Buffer.from(`${username}:${password}`).toString('base64');", + "", + " const requestOptions = {", + " url: apiUrl,", + " method: \"POST\",", + " header: {", + " \"accept\": \"*/*\",", + " \"content-type\": \"application/json\",", + " \"Authorization\": `Basic ${basicAuth}`", + " },", + " body: {", + " mode: \"raw\",", + " raw: JSON.stringify({", + " \"expirationSeconds\": 7200,", + " \"userId\": \"dotcms.org.1\",", + " \"network\": \"0.0.0.0/0\",", + " \"claims\": {\"label\": \"postman-tests\"}", + " })", + " }", + " };", + "", + " pm.sendRequest(requestOptions, function (err, response) {", + " if (err) {", + " console.log(err);", + " } else {", + " const jwt = response.json().entity.jwt;", + " pm.environment.set('jwt', jwt);", + " console.log(jwt);", + " }", + " });", + " }", + "}", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "currentSiteJost", + "value": "" + }, + { + "key": "currentSiteHost", + "value": "" + }, + { + "key": "currentPageId", + "value": "" + }, + { + "key": "currentPageUrl", + "value": "" + } + ] +} \ No newline at end of file