Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#30900 draft for first changes on force unlock #30906

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
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 @@ -5,15 +5,20 @@
import com.dotcms.contenttype.model.field.RelationshipField;
import com.dotcms.mock.response.MockHttpResponse;
import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils;
import com.dotcms.repackage.org.apache.commons.httpclient.HttpStatus;
import com.dotcms.rest.AnonymousAccess;
import com.dotcms.rest.CountView;
import com.dotcms.rest.InitDataObject;
import com.dotcms.rest.MapToContentletPopulator;
import com.dotcms.rest.RESTParams;
import com.dotcms.rest.ResourceResponse;
import com.dotcms.rest.ResponseEntityBooleanView;
import com.dotcms.rest.ResponseEntityContentletView;
import com.dotcms.rest.ResponseEntityCountView;
import com.dotcms.rest.ResponseEntityView;
import com.dotcms.rest.WebResource;
import com.dotcms.rest.annotation.NoCache;
import com.dotcms.rest.exception.ForbiddenException;
import com.dotcms.util.DotPreconditions;
import com.dotcms.uuid.shorty.ShortType;
import com.dotcms.uuid.shorty.ShortyId;
Expand Down Expand Up @@ -48,6 +53,7 @@
import com.dotmarketing.util.PageMode;
import com.dotmarketing.util.UtilMethods;
import com.dotmarketing.util.json.JSONException;
import com.dotmarketing.util.json.JSONObject;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.liferay.portal.language.LanguageUtil;
Expand Down Expand Up @@ -394,27 +400,63 @@ public ResponseEntityView<List<ContentReferenceView>> getContentletReferences(
return new ResponseEntityView<>(contentReferenceViews);
}

/**
* Checks whether a given contentlet can be locked by the current user.
*
* @param request the HTTP servlet request, containing information about the client request.
* @param response the HTTP servlet response, used for setting response parameters.
* @param inodeOrIdentifier the inode or identifier of the contentlet to be checked.
* @param language the language ID of the contentlet (optional, defaults to -1 for fallback).
* @return a ResponseEntityView containing:
* <ul>
* <li>"canLock": boolean indicating if the contentlet can be locked by the user.</li>
* <li>"locked": boolean indicating if the contentlet is currently locked.</li>
* <li>"lockedBy": (optional) user ID of the user who locked the contentlet, if locked.</li>
* <li>"lockedOn": (optional) timestamp when the contentlet was locked, if locked.</li>
* <li>"lockedByName": (optional) name of the user who locked the contentlet, if locked.</li>
* <li>"inode": the inode of the contentlet.</li>
* <li>"id": the identifier of the contentlet.</li>
* </ul>
* @throws DotDataException if there is a data access issue.
* @throws DotSecurityException if the user does not have the required permissions.
* @throws DoesNotExistException if the contentlet does not exist.
*/
@GET
@Path("/_canlock/{inodeOrIdentifier}")
@Produces(MediaType.APPLICATION_JSON)
public Response canLockContent(@Context HttpServletRequest request, @Context final HttpServletResponse response,
@PathParam("inodeOrIdentifier") final String inodeOrIdentifier,
@DefaultValue("-1") @QueryParam("language") final String language)
throws DotDataException, JSONException, DotSecurityException {
@Operation(operationId = "canLockContent", summary = "Check if a contentlet can be locked",
description = "Checks if the contentlet specified by its inode or identifier can be locked by the current user.",
tags = {"Content"},
responses = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved lock status",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ResponseEntityView.class)
)
),
@ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\`
@ApiResponse(responseCode = "401", description = "Invalid User"), // not logged in
@ApiResponse(responseCode = "403", description = "Forbidden"), // no permission
@ApiResponse(responseCode = "404", description = "Content not found"),
@ApiResponse(responseCode = "500", description = "Internal Server Error")
}
)
public ResponseEntityView<Map<String, Object>> canLockContent(@Context HttpServletRequest request,
@Context final HttpServletResponse response,
@PathParam("inodeOrIdentifier") final String inodeOrIdentifier,
@DefaultValue("-1") @QueryParam("language") final String language)
throws DotDataException, DotSecurityException {

final User user =
new WebResource.InitBuilder(this.webResource)
.requestAndResponse(request, response)
.requiredAnonAccess(AnonymousAccess.READ)
.init().getUser();

Logger.debug(this, () -> "Checking if the contentlet can be locked: " + inodeOrIdentifier);
final PageMode mode = PageMode.get(request);
final long testLangId = LanguageUtil.getLanguageId(language);
final long languageId = testLangId <=0 ? this.languageWebAPI.getLanguage(request).getId() : testLangId;

final Contentlet contentlet = this.resolveContentletOrFallBack(inodeOrIdentifier, mode, languageId, user);


final Map<String, Object> resultMap = new HashMap<>();

if(UtilMethods.isEmpty(contentlet::getIdentifier)) {
Expand All @@ -438,7 +480,118 @@ public Response canLockContent(@Context HttpServletRequest request, @Context fin
resultMap.put("inode", contentlet.getInode());
resultMap.put("id", contentlet.getIdentifier());

return Response.ok(new ResponseEntityView<>(resultMap)).build();
return new ResponseEntityView<>(resultMap);
}

/**
* Unlock a given contentlet by the current user.
*
* @param request the HTTP servlet request, containing information about the client request.
* @param response the HTTP servlet response, used for setting response parameters.
* @param inodeOrIdentifier the inode or identifier of the contentlet to be checked.
* @param language the language ID of the contentlet (optional, defaults to -1 for fallback).
* @return a ResponseEntityBooleanView true if the unlock was sucessful.
*
* @throws DotDataException if there is a data access issue.
* @throws DotSecurityException if the user does not have the required permissions.
* @throws DoesNotExistException if the contentlet does not exist.
*/
@PUT
@Path("/_unlock/{inodeOrIdentifier}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "unlockContent", summary = "Unlock a given contentlet by the current user",
description = "If the user is allowed to unlock the contentlet specified by its inode or identifier, " +
"the contentlet will be unlocked.",
tags = {"Content"},
responses = {
@ApiResponse(responseCode = "200", description = "Successfully unlocked contentlet",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ResponseEntityBooleanView.class)
)
),
@ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\`
@ApiResponse(responseCode = "401", description = "Invalid User"), // not logged in
@ApiResponse(responseCode = "403", description = "Forbidden"), // no permission
@ApiResponse(responseCode = "404", description = "Content not found"),
@ApiResponse(responseCode = "500", description = "Internal Server Error")
}
)
public ResponseEntityBooleanView unlockContent(@Context HttpServletRequest request,
@Context final HttpServletResponse response,
@PathParam("inodeOrIdentifier") final String inodeOrIdentifier,
@DefaultValue("-1") @QueryParam("language") final String language)
throws DotDataException, DotSecurityException {

final User user =
new WebResource.InitBuilder(this.webResource)
.requestAndResponse(request, response)
.requiredAnonAccess(AnonymousAccess.WRITE)
.init().getUser();

Logger.debug(this, () -> "Unlocking the contentlet: " + inodeOrIdentifier);
final PageMode mode = PageMode.get(request);
final long testLangId = LanguageUtil.getLanguageId(language);
final long languageId = testLangId <=0 ? this.languageWebAPI.getLanguage(request).getId() : testLangId;
final Contentlet contentlet = this.resolveContentletOrFallBack(inodeOrIdentifier, mode, languageId, user);

APILocator.getContentletAPI().unlock(contentlet, user, mode.respectAnonPerms);

return new ResponseEntityBooleanView(true);
}

/**
* Lock a given contentlet by the current user.
*
* @param request the HTTP servlet request, containing information about the client request.
* @param response the HTTP servlet response, used for setting response parameters.
* @param inodeOrIdentifier the inode or identifier of the contentlet to be checked.
* @param language the language ID of the contentlet (optional, defaults to -1 for fallback).
* @return a ResponseEntityBooleanView true if the lock was sucessful.
*
* @throws DotDataException if there is a data access issue.
* @throws DotSecurityException if the user does not have the required permissions.
* @throws DoesNotExistException if the contentlet does not exist.
*/
@PUT
@Path("/_lock/{inodeOrIdentifier}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "lockContent", summary = "Lock a given contentlet by the current user",
description = "If the user is allowed to lock the contentlet specified by its inode or identifier, " +
"the contentlet will be locked.",
tags = {"Content"},
responses = {
@ApiResponse(responseCode = "200", description = "Successfully locked contentlet",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ResponseEntityBooleanView.class)
)
),
@ApiResponse(responseCode = "400", description = "Bad request"), // invalid param string like `\`
@ApiResponse(responseCode = "401", description = "Invalid User"), // not logged in
@ApiResponse(responseCode = "403", description = "Forbidden"), // no permission
@ApiResponse(responseCode = "404", description = "Content not found"),
@ApiResponse(responseCode = "500", description = "Internal Server Error")
}
)
public ResponseEntityBooleanView lockContent(@Context HttpServletRequest request,
@Context HttpServletResponse response,
@PathParam("inodeOrIdentifier") final String inodeOrIdentifier,
@DefaultValue("-1") @QueryParam("language") final String language)
throws DotDataException, DotSecurityException {

final User user =
new WebResource.InitBuilder(this.webResource)
.requestAndResponse(request, response)
.requiredAnonAccess(AnonymousAccess.WRITE)
.init().getUser();

Logger.debug(this, () -> "Locking the contentlet: " + inodeOrIdentifier);
final PageMode mode = PageMode.get(request);
final long testLangId = LanguageUtil.getLanguageId(language);
final long languageId = testLangId <=0 ? this.languageWebAPI.getLanguage(request).getId() : testLangId;
final Contentlet contentlet = this.resolveContentletOrFallBack(inodeOrIdentifier, mode, languageId, user);

APILocator.getContentletAPI().lock(contentlet, user, mode.respectAnonPerms);
return new ResponseEntityBooleanView(true);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package com.dotmarketing.portlets.workflows.actionlet;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.dotcms.util.ConversionUtils;
import com.dotcms.util.DotPreconditions;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.portlets.contentlet.model.Contentlet;
import com.dotmarketing.portlets.workflows.model.CheckboxWorkflowActionletParameter;
import com.dotmarketing.portlets.workflows.model.WorkflowActionClassParameter;
import com.dotmarketing.portlets.workflows.model.WorkflowActionFailureException;
import com.dotmarketing.portlets.workflows.model.WorkflowActionletParameter;
import com.dotmarketing.portlets.workflows.model.WorkflowProcessor;
import com.dotmarketing.portlets.workflows.model.WorkflowStep;
import com.dotmarketing.util.Logger;
import com.liferay.portal.model.User;
import com.liferay.util.StringPool;
import io.vavr.control.Try;

/**
* {@link WorkFlowActionlet} that unlock a {@link Contentlet}
Expand All @@ -21,7 +27,7 @@
*/
public class CheckinContentActionlet extends WorkFlowActionlet {


public static final String FORCE_UNLOCK_ALLOWED = "force-unlock";

/**
*
Expand All @@ -37,15 +43,27 @@ public String getHowTo() {
return "This actionlet will unlock the content.";
}

public void executeAction(WorkflowProcessor processor,Map<String,WorkflowActionClassParameter> params) throws WorkflowActionFailureException {
public void executeAction(final WorkflowProcessor processor,
final Map<String,WorkflowActionClassParameter> params) throws WorkflowActionFailureException {
try {

final Contentlet contentlet = processor.getContentlet();
User user = processor.getUser();
final boolean forceUnlock = ConversionUtils.toBoolean(params.get(FORCE_UNLOCK_ALLOWED).getValue(), false);
DotPreconditions.checkNotNull(contentlet);

if (!contentlet.isNew() && contentlet.isLocked()) {

contentlet.setProperty(Contentlet.WORKFLOW_IN_PROGRESS, Boolean.TRUE);
APILocator.getContentletAPI().unlock(contentlet, processor.getUser(),
if (forceUnlock) {

final User finalUser = user;
user = user.isAdmin()
|| Try.of(()->APILocator.getContentletAPI().canLock(contentlet, finalUser)).getOrElse(false)?
user:APILocator.systemUser();
}

APILocator.getContentletAPI().unlock(contentlet, user,
processor.getContentletDependencies() != null
&& processor.getContentletDependencies().isRespectAnonymousPermissions());
}
Expand All @@ -65,6 +83,10 @@ public WorkflowStep getNextStep() {
@Override
public List<WorkflowActionletParameter> getParameters() {

return null;
final List<WorkflowActionletParameter> workflowActionletParameters = new ArrayList<>();

workflowActionletParameters.add(new CheckboxWorkflowActionletParameter(FORCE_UNLOCK_ALLOWED, "Force Unlock", "false", false));

return workflowActionletParameters;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1787,7 +1787,8 @@ private List<WorkflowAction> doFilterActions(final ImmutableList.Builder<Workflo
for (final WorkflowAction workflowAction : unfilteredActions) {

if (this.workflowStatusFilter.filter(workflowAction,
new ContentletStateOptions(isNew, isPublished, isArchived, canLock, isLocked, renderMode))) {
new ContentletStateOptions(isNew, isPublished, isArchived,
canLock(canLock, workflowAction), isLocked, renderMode))) {

actions.add(workflowAction);
}
Expand All @@ -1796,6 +1797,33 @@ private List<WorkflowAction> doFilterActions(final ImmutableList.Builder<Workflo
return actions.build();
}

/**
* Figure out the can lock, if the can lock is false, check if the workflow has
* 1) the lock action
* 2) if forze lock is enabled
* if both conditions are meet, then the can lock is true
* @param canLock
* @param workflowAction
* @return boolean
*/
private boolean canLock(final boolean canLock, final WorkflowAction workflowAction) {

if (!canLock && Objects.nonNull(workflowAction)) {

final List<WorkflowActionClass> actionClasses = Try.of(()->this.findActionClasses(workflowAction)).getOrNull();
if(Objects.nonNull(actionClasses)) {

return actionClasses.stream().filter(actionClass -> actionClass.getClazz().equals(CheckinContentActionlet.class.getName()))
.map(actionClass -> Try.of(()->this.findParamsForActionClass(actionClass)).getOrNull())
.filter(workflowActionClassParameterMap -> Objects.nonNull(workflowActionClassParameterMap) &&
workflowActionClassParameterMap.containsKey(CheckinContentActionlet.FORCE_UNLOCK_ALLOWED))
.anyMatch(workflowActionClassParameterMap -> "true".equalsIgnoreCase(
workflowActionClassParameterMap.get(CheckinContentActionlet.FORCE_UNLOCK_ALLOWED).getValue()));
}
}

return canLock;
}


/**
Expand Down Expand Up @@ -2657,21 +2685,10 @@ public List<WorkflowAction> findAvailableActions(final Contentlet contentlet, fi
return Collections.emptyList();
}


boolean canLock = false;
boolean isLocked = false;
boolean isPublish = false;
boolean isArchived = false;

try {

canLock = APILocator.getContentletAPI().canLock(contentlet, user);
isLocked = isNew? true : APILocator.getVersionableAPI().isLocked(contentlet);
isPublish = isNew? false: APILocator.getVersionableAPI().hasLiveVersion(contentlet);
isArchived = isNew? false: APILocator.getVersionableAPI().isDeleted(contentlet);
} catch(Exception e) {

}
final boolean canLock = Try.of(()->APILocator.getContentletAPI().canLock(contentlet, user)).getOrElse(false);
final boolean isLocked = isNew? true : Try.of(()->APILocator.getVersionableAPI().isLocked(contentlet)).getOrElse(false);
final boolean isPublish = isNew? false: Try.of(()->APILocator.getVersionableAPI().hasLiveVersion(contentlet)).getOrElse(false);
final boolean isArchived = isNew? false: Try.of(()->APILocator.getVersionableAPI().isDeleted(contentlet)).getOrElse(false);

final List<WorkflowStep> steps = findStepsByContentlet(contentlet, false);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dotmarketing.portlets.workflows.model;

/**
* This represents a single checkbox parameter
* @author jsanca
*/
public class CheckboxWorkflowActionletParameter extends WorkflowActionletParameter {

public CheckboxWorkflowActionletParameter(final String key, final String displayName,
final String defaultValue, final boolean isRequired) {
super(key, displayName, defaultValue, isRequired);
}

@Override
public String toString() {
return "CheckboxWorkflowActionletParameter [key=" + getKey() + "]";
}

}
Loading
Loading