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

UFAL/share submission by email #780

Merged
merged 14 commits into from
Nov 12, 2024
Merged
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
11 changes: 11 additions & 0 deletions dspace-api/src/main/java/org/dspace/content/WorkspaceItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ public class WorkspaceItem
@Column(name = "page_reached")
private Integer pageReached = -1;

@Column(name = "share_token")
private String shareToken = null;

/**
* Protected constructor, create object using:
* {@link org.dspace.content.service.WorkspaceItemService#create(Context, Collection, boolean)}
Expand Down Expand Up @@ -131,6 +134,14 @@ public void setPageReached(int v) {
pageReached = v;
}

public String getShareToken() {
return shareToken;
}

public void setShareToken(String shareToken) {
this.shareToken = shareToken;
}

/**
* Decide if this WorkspaceItem is equal to another
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ public WorkspaceItem findByItem(Context context, Item item) throws SQLException
return workspaceItemDAO.findByItem(context, item);
}

@Override
public List<WorkspaceItem> findByShareToken(Context context, String shareToken) throws SQLException {
return workspaceItemDAO.findByShareToken(context, shareToken);
}

@Override
public List<WorkspaceItem> findAll(Context context) throws SQLException {
return workspaceItemDAO.findAll(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public List<WorkspaceItem> findByEPerson(Context context, EPerson ep, Integer li

public WorkspaceItem findByItem(Context context, Item i) throws SQLException;

public List<WorkspaceItem> findByShareToken(Context context, String shareToken) throws SQLException;

public List<WorkspaceItem> findAll(Context context) throws SQLException;

public List<WorkspaceItem> findAll(Context context, Integer limit, Integer offset) throws SQLException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ public WorkspaceItem findByItem(Context context, Item i) throws SQLException {
return uniqueResult(context, criteriaQuery, false, WorkspaceItem.class);
}

@Override
public List<WorkspaceItem> findByShareToken(Context context, String shareToken) throws SQLException {
Query query = createQuery(context,
"from WorkspaceItem ws where ws.shareToken = :shareToken");
query.setParameter("shareToken", shareToken);
return list(query);
}

@Override
public List<WorkspaceItem> findAll(Context context) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ public List<WorkspaceItem> findByCollection(Context context, Collection collecti
public WorkspaceItem findByItem(Context context, Item item)
throws SQLException;

/**
* Find the workspace item by the share token.
* @param context the DSpace context object
* @param shareToken the share token
* @return the List of workspace items or null
* @throws SQLException if database error
*/
public List<WorkspaceItem> findByShareToken(Context context, String shareToken)
throws SQLException;

/**
* Get all workspace items in the whole system
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--
-- The contents of this file are subject to the license and copyright
-- detailed in the LICENSE and NOTICE files at the root of the source
-- tree and available online at
--
-- http://www.dspace.org/license/
--

ALTER TABLE workspaceitem ADD share_token varchar(32);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--
-- The contents of this file are subject to the license and copyright
-- detailed in the LICENSE and NOTICE files at the root of the source
-- tree and available online at
--
-- http://www.dspace.org/license/
--

ALTER TABLE workspaceitem ADD share_token varchar(32);
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,13 @@ public WorkspaceItemBuilder withMetadata(final String schema, final String eleme
final String value) {
return addMetadataValue(schema, element, qualifier, value);
}

public WorkspaceItemBuilder withShareToken(String shareToken) {
try {
workspaceItem.setShareToken(shareToken);
} catch (Exception e) {
handleException(e);
}
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;

/**
* This class represents a DTO that will be used to share a submission link. It will be used to return the share link
* to the user in the UI.
*
* @author Milan Majchrak (dspace at dataquest.sk)
*/
public class ShareSubmissionLinkDTO {

private String shareLink;

public ShareSubmissionLinkDTO() { }

public String getShareLink() {
return shareLink;
}

public void setShareLink(String shareLink) {
this.shareLink = shareLink;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.repository;

import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.BadRequestException;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.converter.ConverterService;
import org.dspace.app.rest.model.RestAddressableModel;
import org.dspace.app.rest.model.ShareSubmissionLinkDTO;
import org.dspace.app.rest.model.WorkspaceItemRest;
import org.dspace.app.rest.utils.Utils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.Email;
import org.dspace.core.I18nUtil;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.dspace.web.ContextUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* This class' purpose is to provide an API for sharing an in-progress submission. It allows the user to generate
* a share link for a workspace item and to set the owner of the workspace item to the current user.
*
* @author Milan Majchrak (dspace at dataquest.sk)
*/
@RestController
@RequestMapping("/api/" + RestAddressableModel.SUBMISSION)
public class SubmissionController {
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(SubmissionController.class);

@Autowired
WorkspaceItemService workspaceItemService;

@Autowired
ConfigurationService configurationService;

@Autowired
protected Utils utils;

@Autowired
AuthorizeService authorizeService;

@Lazy
@Autowired
protected ConverterService converter;

@PreAuthorize("hasPermission(#wsoId, 'WORKSPACEITEM', 'WRITE')")
@RequestMapping(method = RequestMethod.GET, value = "share")
public ResponseEntity<ShareSubmissionLinkDTO> generateShareLink(@RequestParam(name = "workspaceitemid")
Integer wsoId, HttpServletRequest request)
throws SQLException, AuthorizeException {

Context context = ContextUtil.obtainContext(request);
// Check the context is not null
this.validateContext(context);

// Get workspace item from ID
WorkspaceItem wsi = workspaceItemService.find(context, wsoId);
// Check the wsi does exist
validateWorkspaceItem(wsi, wsoId, null);

// Generate a share link
String shareToken = generateShareToken();

// Update workspace item with share link
wsi.setShareToken(shareToken);
workspaceItemService.update(context, wsi);
// Without commit the changes are not persisted into the database
context.commit();

// Get submitter email
EPerson currentUser = context.getCurrentUser();
if (currentUser == null) {
String errorMessage = "The current user is not valid, it cannot be null.";
log.error(errorMessage);
throw new BadRequestException(errorMessage);
}

// Send email to submitter with share link
String shareLink = sendShareLinkEmail(context, wsi, currentUser);
if (StringUtils.isEmpty(shareLink)) {
String errorMessage = "The share link is empty.";
log.error(errorMessage);
throw new RuntimeException(errorMessage);
}

// Create a DTO with the share link for better processing in the FE
ShareSubmissionLinkDTO shareSubmissionLinkDTO = new ShareSubmissionLinkDTO();
shareSubmissionLinkDTO.setShareLink(shareLink);

// Send share link in response
return ResponseEntity.ok().body(shareSubmissionLinkDTO);
}

@PreAuthorize("hasPermission(#wsoId, 'WORKSPACEITEM', 'WRITE')")
@RequestMapping(method = RequestMethod.GET, value = "setOwner")
public WorkspaceItemRest setOwner(@RequestParam(name = "shareToken") String shareToken,
@RequestParam(name = "workspaceitemid") Integer wsoId,
HttpServletRequest request)
throws SQLException, AuthorizeException {

Context context = ContextUtil.obtainContext(request);
// Check the context is not null
this.validateContext(context);

// Get workspace by share token
List<WorkspaceItem> wsiList = workspaceItemService.findByShareToken(context, shareToken);
// Check the wsi does exist
if (CollectionUtils.isEmpty(wsiList)) {
String errorMessage = "The workspace item with share token:" + shareToken + " does not exist.";
log.error(errorMessage);
throw new BadRequestException(errorMessage);
}

// Get the first workspace item - the only one
WorkspaceItem wsi = wsiList.get(0);
// Check the wsi does exist
validateWorkspaceItem(wsi, null, shareToken);

if (!authorizeService.authorizeActionBoolean(context, wsi.getItem(), Constants.READ)) {
String errorMessage = "The current user does not have rights to view the WorkflowItem";
log.error(errorMessage);
throw new AccessDeniedException(errorMessage);
}

// Set the owner of the workspace item to the current user
EPerson currentUser = context.getCurrentUser();
vidiecan marked this conversation as resolved.
Show resolved Hide resolved
// If the current user is null, throw an exception
if (currentUser == null) {
String errorMessage = "The current user is not valid, it cannot be null.";
log.error(errorMessage);
throw new BadRequestException(errorMessage);
}

wsi.getItem().setSubmitter(currentUser);
workspaceItemService.update(context, wsi);
WorkspaceItemRest wsiRest = converter.toRest(wsi, utils.obtainProjection());

// Without commit the changes are not persisted into the database
context.commit();
return wsiRest;
}

private static String generateShareToken() {
// UUID generates a 36-char string with hyphens, so we can strip them to get a 32-char string
return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
vidiecan marked this conversation as resolved.
Show resolved Hide resolved
}

private String sendShareLinkEmail(Context context, WorkspaceItem wsi, EPerson currentUser) {
// Get the UI URL from the configuration
String uiUrl = configurationService.getProperty("dspace.ui.url");
// Get submitter email
String email = currentUser.getEmail();
// Compose the url with the share token. The user will be redirected to the UI.
String shareTokenUrl = uiUrl + "/share-submission/change-submitter?share_token=" + wsi.getShareToken() +
"&workspaceitemid=" + wsi.getID();
try {
Locale locale = context.getCurrentLocale();
Email bean = Email.getEmail(I18nUtil.getEmailFilename(locale, "share_submission"));
bean.addArgument(shareTokenUrl);
bean.addRecipient(email);
bean.send();
} catch (MessagingException | IOException e) {
String errorMessage = "Unable send the email because: " + e.getMessage();
log.error(errorMessage);
throw new RuntimeException(errorMessage);
}
return shareTokenUrl;
}

/**
* Check if the context is valid - not null. If not, throw an exception.
*/
private void validateContext(Context context) {
if (context == null) {
String errorMessage = "The current context is not valid, it cannot be null.";
log.error(errorMessage);
throw new RuntimeException(errorMessage);
}
}

/**
* Check if the workspace item is valid - not null. If not, throw an exception. The workspace item can be found by
* ID or the share token.
*/
private void validateWorkspaceItem(WorkspaceItem wsi, Integer wsoId, String shareToken) {
if (wsi == null) {
String identifier = wsoId != null ? wsoId.toString() : shareToken;
String identifierName = wsoId != null ? "ID" : "share token";
String errorMessage = "The workspace item with " + identifierName + ":" + identifier + " does not exist.";
log.error(errorMessage);
throw new BadRequestException(errorMessage);
}
}
}
Loading
Loading