• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CeON / dataverse / 1369

19 Jun 2024 01:24PM UTC coverage: 25.507% (-0.003%) from 25.51%
1369

push

jenkins

web-flow
Closes #2467: Add button to users dashboard to download csv with list of users (#2495)

* Closes #2478: API to download CSV file with all registered users

* Re-use existing superuser fetching method

* Closes #2467: Add button to users dashboard to download csv with list of users

4 of 14 new or added lines in 3 files covered. (28.57%)

1 existing line in 1 file now uncovered.

17782 of 69713 relevant lines covered (25.51%)

0.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.05
/dataverse-webapp/src/main/java/edu/harvard/iq/dataverse/api/Access.java
1
package edu.harvard.iq.dataverse.api;
2

3
import com.amazonaws.services.glacier.model.MissingParameterValueException;
4
import edu.harvard.iq.dataverse.DataFileServiceBean;
5
import edu.harvard.iq.dataverse.DataverseDao;
6
import edu.harvard.iq.dataverse.DataverseRequestServiceBean;
7
import edu.harvard.iq.dataverse.DataverseRoleServiceBean;
8
import edu.harvard.iq.dataverse.DataverseSession;
9
import edu.harvard.iq.dataverse.PermissionServiceBean;
10
import edu.harvard.iq.dataverse.RoleAssigneeServiceBean;
11
import edu.harvard.iq.dataverse.api.annotations.ApiWriteOperation;
12
import edu.harvard.iq.dataverse.api.dto.AuthenticatedUserDTO;
13
import edu.harvard.iq.dataverse.citation.CitationFactory;
14
import edu.harvard.iq.dataverse.common.BundleUtil;
15
import edu.harvard.iq.dataverse.dataaccess.DataAccess;
16
import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter;
17
import edu.harvard.iq.dataverse.dataaccess.OptionalAccessService;
18
import edu.harvard.iq.dataverse.dataaccess.StorageIO;
19
import edu.harvard.iq.dataverse.dataaccess.StorageIOConstants;
20
import edu.harvard.iq.dataverse.datafile.FilePermissionsService;
21
import edu.harvard.iq.dataverse.datafile.page.WholeDatasetDownloadLogger;
22
import edu.harvard.iq.dataverse.dataset.EmbargoAccessService;
23
import edu.harvard.iq.dataverse.dataset.datasetversion.DatasetVersionServiceBean;
24
import edu.harvard.iq.dataverse.datavariable.VariableServiceBean;
25
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
26
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
27
import edu.harvard.iq.dataverse.engine.command.impl.AssignRoleCommand;
28
import edu.harvard.iq.dataverse.engine.command.impl.RequestAccessCommand;
29
import edu.harvard.iq.dataverse.engine.command.impl.RevokeRoleCommand;
30
import edu.harvard.iq.dataverse.export.DDIExportServiceBean;
31
import edu.harvard.iq.dataverse.guestbook.GuestbookResponseServiceBean;
32
import edu.harvard.iq.dataverse.notification.NotificationObjectType;
33
import edu.harvard.iq.dataverse.notification.UserNotificationService;
34
import edu.harvard.iq.dataverse.persistence.datafile.DataFile;
35
import edu.harvard.iq.dataverse.persistence.datafile.FileMetadata;
36
import edu.harvard.iq.dataverse.persistence.datafile.datavariable.DataVariable;
37
import edu.harvard.iq.dataverse.persistence.datafile.license.FileTermsOfUse.TermsOfUseType;
38
import edu.harvard.iq.dataverse.persistence.dataset.Dataset;
39
import edu.harvard.iq.dataverse.persistence.dataset.DatasetVersion;
40
import edu.harvard.iq.dataverse.persistence.dataverse.Dataverse;
41
import edu.harvard.iq.dataverse.persistence.dataverse.DataverseTheme;
42
import edu.harvard.iq.dataverse.persistence.guestbook.GuestbookResponse;
43
import edu.harvard.iq.dataverse.persistence.user.AuthenticatedUser;
44
import edu.harvard.iq.dataverse.persistence.user.DataverseRole;
45
import edu.harvard.iq.dataverse.persistence.user.DataverseRole.BuiltInRole;
46
import edu.harvard.iq.dataverse.persistence.user.GuestUser;
47
import edu.harvard.iq.dataverse.persistence.user.NotificationType;
48
import edu.harvard.iq.dataverse.persistence.user.Permission;
49
import edu.harvard.iq.dataverse.persistence.user.RoleAssignee;
50
import edu.harvard.iq.dataverse.persistence.user.RoleAssignment;
51
import edu.harvard.iq.dataverse.persistence.user.User;
52
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
53
import edu.harvard.iq.dataverse.util.FileUtil;
54
import edu.harvard.iq.dataverse.util.StringUtil;
55
import edu.harvard.iq.dataverse.worldmapauth.WorldMapTokenServiceBean;
56
import io.vavr.control.Option;
57
import io.vavr.control.Try;
58
import org.apache.commons.lang.StringUtils;
59

60
import javax.ejb.EJB;
61
import javax.inject.Inject;
62
import javax.persistence.TypedQuery;
63
import javax.servlet.http.HttpServletResponse;
64
import javax.ws.rs.BadRequestException;
65
import javax.ws.rs.DELETE;
66
import javax.ws.rs.ForbiddenException;
67
import javax.ws.rs.GET;
68
import javax.ws.rs.NotFoundException;
69
import javax.ws.rs.PUT;
70
import javax.ws.rs.Path;
71
import javax.ws.rs.PathParam;
72
import javax.ws.rs.Produces;
73
import javax.ws.rs.QueryParam;
74
import javax.ws.rs.ServiceUnavailableException;
75
import javax.ws.rs.WebApplicationException;
76
import javax.ws.rs.core.Context;
77
import javax.ws.rs.core.MultivaluedMap;
78
import javax.ws.rs.core.Response;
79
import javax.ws.rs.core.StreamingOutput;
80
import javax.ws.rs.core.UriInfo;
81
import java.io.ByteArrayOutputStream;
82
import java.io.File;
83
import java.io.FileInputStream;
84
import java.io.IOException;
85
import java.io.InputStream;
86
import java.io.OutputStream;
87
import java.sql.Timestamp;
88
import java.util.ArrayList;
89
import java.util.Arrays;
90
import java.util.Collections;
91
import java.util.Date;
92
import java.util.List;
93
import java.util.Optional;
94
import java.util.Properties;
95
import java.util.function.Supplier;
96
import java.util.logging.Level;
97
import java.util.logging.Logger;
98
import java.util.stream.Collectors;
99

100
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
101

102

103
/**
104
 * @author Leonid Andreev
105
 * <p>
106
 * The data (file) access API is based on the DVN access API v.1.0 (that came
107
 * with the v.3.* of the DVN app) and extended for DVN 4.0 to include some
108
 * extra fancy functionality, such as subsetting individual columns in tabular
109
 * data files and more.
110
 */
111

112
@Path("access")
113
public class Access extends AbstractApiBean {
1✔
114
    private static final Logger logger = Logger.getLogger(Access.class.getCanonicalName());
1✔
115

116
    @EJB
117
    DataFileServiceBean dataFileService;
118
    @EJB
119
    DatasetVersionServiceBean versionService;
120
    @EJB
121
    DataverseDao dataverseDao;
122
    @EJB
123
    VariableServiceBean variableService;
124
    @Inject
125
    SettingsServiceBean settingsService;
126
    @EJB
127
    DDIExportServiceBean ddiExportService;
128
    @EJB
129
    PermissionServiceBean permissionService;
130
    @EJB
131
    WorldMapTokenServiceBean worldMapTokenServiceBean;
132
    @Inject
133
    DataverseRequestServiceBean dvRequestService;
134
    @EJB
135
    GuestbookResponseServiceBean guestbookResponseService;
136
    @EJB
137
    DataverseRoleServiceBean roleService;
138
    @EJB
139
    UserNotificationService userNotificationService;
140
    @EJB
141
    private FilePermissionsService filePermissionsService;
142
    @Inject
143
    private EmbargoAccessService embargoAccessService;
144
    @Inject
145
    private WholeDatasetDownloadLogger wholeDatasetDownloadLogger;
146
    @Inject
147
    private ImageThumbConverter imageThumbConverter;
148
    @Inject
149
    private CitationFactory citationFactory;
150
    @Inject
151
    private RoleAssigneeServiceBean roleAssigneeSvc;
152

153

154
    // TODO:
155
    // versions? -- L.A. 4.0 beta 10
156
    @Path("datafile/bundle/{fileId}")
157
    @GET
158
    @Produces({"application/zip"})
159
    public BundleDownloadInstance datafileBundle(@PathParam("fileId") String fileId, @QueryParam("gbrecs") Boolean gbrecs) {
160

161

162
        GuestbookResponse gbr = null;
×
163

164
        DataFile datafile = findDataFileOrDieWrapper(fileId);
×
165

166
        // This will throw a ForbiddenException if access isn't authorized:
167
        checkAuthorization(datafile);
×
168

169
        if (gbrecs == null && datafile.isReleased()) {
×
170
            // Write Guestbook record if not done previously and file is released
171
            User apiTokenUser = getApiTokenUserWithGuestFallbackOnInvalidToken();
×
172
            gbr = guestbookResponseService.initAPIGuestbookResponse(datafile.getOwner(), datafile, session, apiTokenUser);
×
173
            guestbookResponseService.save(gbr);
×
174
        }
175

176
        DownloadInfo dInfo = new DownloadInfo(datafile);
×
177
        BundleDownloadInstance downloadInstance = new BundleDownloadInstance(dInfo);
×
178

179
        FileMetadata fileMetadata = datafile.getFileMetadata();
×
180

181
        downloadInstance.setFileCitationEndNote(citationFactory.create(fileMetadata).toEndNoteString());
×
182
        downloadInstance.setFileCitationRIS(citationFactory.create(fileMetadata).toRISString());
×
183
        downloadInstance.setFileCitationBibtex(citationFactory.create(fileMetadata).toBibtexString());
×
184

185
        ByteArrayOutputStream outStream = null;
×
186
        outStream = new ByteArrayOutputStream();
×
187
        Long dfId = datafile.getId();
×
188
        try {
189
            ddiExportService.exportDataFile(dfId, outStream, null, null);
×
190
            downloadInstance.setFileDDIXML(outStream.toString());
×
191
        } catch (Exception ex) {
×
192
            // if we can't generate the DDI, it's ok, we'll just generate the bundle without it.
193
            logger.log(Level.WARNING,"Exception during DDI generation.", ex);
×
194
        }
×
195

196
        wholeDatasetDownloadLogger.incrementLogIfDownloadingWholeDataset(Collections.singletonList(datafile));
×
197
        return downloadInstance;
×
198

199
    }
200

201
    //Added a wrapper method since the original method throws a wrapped response
202
    //the access methods return files instead of responses so we convert to a WebApplicationException
203

204
    private DataFile findDataFileOrDieWrapper(String fileId) {
205

206
        DataFile df = null;
×
207

208
        try {
209
            df = findDataFileOrDie(fileId);
×
210
        } catch (WrappedResponse ex) {
×
211
            logger.warning("Access: datafile service could not locate a DataFile object for id " + fileId + "!");
×
212
            throw new NotFoundException();
×
213
        }
×
214
        return df;
×
215
    }
216

217

218
    @Path("datafile/{fileId}")
219
    @GET
220
    @Produces({"application/xml"})
221
    public DownloadInstance datafile(@PathParam("fileId") String fileId, @QueryParam("gbrecs") Boolean gbrecs, @Context UriInfo uriInfo, @Context HttpServletResponse response) {
222

223
        DataFile df = findDataFileOrDieWrapper(fileId);
×
224
        GuestbookResponse gbr = null;
×
225

226
        if (df.isHarvested()) {
×
227
            String errorMessage = "Datafile " + fileId + " is a harvested file that cannot be accessed in this Dataverse";
×
228
            throw new NotFoundException(errorMessage);
×
229
            // (nobody should ever be using this API on a harvested DataFile)!
230
        }
231

232
        if (gbrecs == null && df.isReleased()) {
×
233
            // Write Guestbook record if not done previously and file is released
234
            User apiTokenUser = getApiTokenUserWithGuestFallbackOnInvalidToken();
×
235
            gbr = guestbookResponseService.initAPIGuestbookResponse(df.getOwner(), df, session, apiTokenUser);
×
236
        }
237

238
        // This will throw a ForbiddenException if access isn't authorized:
239
        checkAuthorization(df);
×
240

241
        DownloadInfo dInfo = new DownloadInfo(df);
×
242

243
        logger.fine("checking if thumbnails are supported on this file.");
×
244
        if (FileUtil.isThumbnailSupported(df)) {
×
245
            dInfo.addServiceAvailable(new OptionalAccessService("thumbnail", "image/png", "imageThumb=true", "Image Thumbnail (64x64)"));
×
246
        }
247

248
        if (df.isTabularData()) {
×
249
            String originalMimeType = df.getDataTable().getOriginalFileFormat();
×
250
            dInfo.addServiceAvailable(new OptionalAccessService("original", originalMimeType, "format=original", "Saved original (" + originalMimeType + ")"));
×
251

252
            dInfo.addServiceAvailable(new OptionalAccessService("R", "application/x-rlang-transport", "format=RData", "Data in R format"));
×
253
            dInfo.addServiceAvailable(new OptionalAccessService("preprocessed", "application/json", "format=prep", "Preprocessed data in JSON"));
×
254
            dInfo.addServiceAvailable(new OptionalAccessService("subset", "text/tab-separated-values", "variables=&lt;LIST&gt;", "Column-wise Subsetting"));
×
255
        }
256
        DownloadInstance downloadInstance = new DownloadInstance(dInfo);
×
257

258
        if (gbr != null) {
×
259
            downloadInstance.setGbr(gbr);
×
260
            downloadInstance.setDataverseRequestService(dvRequestService);
×
261
            downloadInstance.setCommand(engineSvc);
×
262
        }
263
        for (String key : uriInfo.getQueryParameters().keySet()) {
×
264
            String value = uriInfo.getQueryParameters().getFirst(key);
×
265
            logger.fine("is download service supported? key=" + key + ", value=" + value);
×
266

267
            if (downloadInstance.checkIfServiceSupportedAndSetConverter(key, value)) {
×
268
                // this automatically sets the conversion parameters in
269
                // the download instance to key and value;
270
                // TODO: I should probably set these explicitly instead.
271
                logger.fine("yes!");
×
272

273
                if (downloadInstance.getConversionParam().equals("subset")) {
×
274
                    String subsetParam = downloadInstance.getConversionParamValue();
×
275
                    String[] variableIdParams = subsetParam.split(",");
×
276
                    if (variableIdParams != null && variableIdParams.length > 0) {
×
277
                        logger.fine(variableIdParams.length + " tokens;");
×
278
                        for (int i = 0; i < variableIdParams.length; i++) {
×
279
                            logger.fine("token: " + variableIdParams[i]);
×
280
                            String token = variableIdParams[i].replaceFirst("^v", "");
×
281
                            Long variableId = null;
×
282
                            try {
283
                                variableId = new Long(token);
×
284
                            } catch (NumberFormatException nfe) {
×
285
                                variableId = null;
×
286
                            }
×
287
                            if (variableId != null) {
×
288
                                logger.fine("attempting to look up variable id " + variableId);
×
289
                                if (variableService != null) {
×
290
                                    DataVariable variable = variableService.find(variableId);
×
291
                                    if (variable != null) {
×
292
                                        if (downloadInstance.getExtraArguments() == null) {
×
293
                                            downloadInstance.setExtraArguments(new ArrayList<Object>());
×
294
                                        }
295
                                        logger.fine("putting variable id " + variable.getId() + " on the parameters list of the download instance.");
×
296
                                        downloadInstance.getExtraArguments().add(variable);
×
297

298
                                        //if (!variable.getDataTable().getDataFile().getId().equals(sf.getId())) {
299
                                        //variableList.add(variable);
300
                                        //}
301
                                    }
302
                                } else {
×
303
                                    logger.fine("variable service is null.");
×
304
                                }
305
                            }
306
                        }
307
                    }
308
                }
309

310
                logger.fine("downloadInstance: " + downloadInstance.getConversionParam() + "," + downloadInstance.getConversionParamValue());
×
311

312
                break;
×
313
            } else {
314
                // Service unknown/not supported/bad arguments, etc.:
315
                // TODO: throw new ServiceUnavailableException();
316
            }
317
        }
×
318

319
        /*
320
         * Provide "Access-Control-Allow-Origin" header:
321
         */
322
        response.setHeader("Access-Control-Allow-Origin", "*");
×
323

324
        /*
325
         * Provide some browser-friendly headers: (?)
326
         */
327
        //return retValue;
328
        return downloadInstance;
×
329
    }
330

331

332
    /*
333
     * Variants of the Access API calls for retrieving datafile-level
334
     * Metadata.
335
     */
336

337

338
    // Metadata format defaults to DDI:
339
    @Path("datafile/{fileId}/metadata")
340
    @GET
341
    @Produces({"text/xml"})
342
    public String tabularDatafileMetadata(@PathParam("fileId") String fileId, @QueryParam("exclude") String exclude, @QueryParam("include") String include, @Context HttpServletResponse response) throws NotFoundException, ServiceUnavailableException {
343
        return tabularDatafileMetadataDDI(fileId, exclude, include, response);
×
344
    }
345

346
    /*
347
     * This has been moved here, under /api/access, from the /api/meta hierarchy
348
     * which we are going to retire.
349
     */
350
    @Path("datafile/{fileId}/metadata/ddi")
351
    @GET
352
    @Produces({"text/xml"})
353
    public String tabularDatafileMetadataDDI(@PathParam("fileId") String fileId, @QueryParam("exclude") String exclude, @QueryParam("include") String include, @Context HttpServletResponse response) throws NotFoundException, ServiceUnavailableException {
354
        String retValue = "";
×
355

356
        DataFile dataFile;
357

358
        dataFile = findDataFileOrDieWrapper(fileId);
×
359

360
        if (!dataFile.isTabularData()) {
×
361
            throw new BadRequestException("tabular data required");
×
362
        }
363

364
        if (dataFile.isHarvested()) {
×
365
            String errorMessage = "Datafile " + fileId + " is a harvested file that cannot be accessed in this Dataverse";
×
366
            throw new NotFoundException(errorMessage);
×
367
        }
368

369
        // This will throw a ForbiddenException if access isn't authorized:
370
        checkAuthorization(dataFile);
×
371

372
        response.setHeader("Content-disposition", "attachment; filename=\"dataverse_files.zip\"");
×
373

374
        String fileName = dataFile.getFileMetadata().getLabel().replaceAll("\\.tab$", "-ddi.xml");
×
375
        response.setHeader("Content-disposition", "attachment; filename=\"" + fileName + "\"");
×
376
        response.setHeader("Content-Type", "application/xml; name=\"" + fileName + "\"");
×
377

378
        ByteArrayOutputStream outStream = null;
×
379
        outStream = new ByteArrayOutputStream();
×
380
        Long dataFileId = dataFile.getId();
×
381
        try {
382
            ddiExportService.exportDataFile(
×
383
                    dataFileId,
384
                    outStream,
385
                    exclude,
386
                    include);
387

388
            retValue = outStream.toString();
×
389

390
        } catch (Exception e) {
×
391
            // For whatever reason we've failed to generate a partial
392
            // metadata record requested.
393
            // We return Service Unavailable.
394
            throw new ServiceUnavailableException();
×
395
        }
×
396

397
        response.setHeader("Access-Control-Allow-Origin", "*");
×
398

399
        return retValue;
×
400
    }
401

402
    @Path("variable/{varId}/metadata/ddi")
403
    @GET
404
    @Produces({"application/xml"})
405
    public String dataVariableMetadataDDI(@PathParam("varId") Long varId, @QueryParam("exclude") String exclude, @QueryParam("include") String include, @Context HttpServletResponse response) {
406
        String retValue = "";
×
407

408
        getDatasetFromDataVariable(varId)
×
409
                .filter(dt -> embargoAccessService.isRestrictedByEmbargo(dt))
×
410
                .ifPresent(dt -> { throw new ForbiddenException(); });
×
411

412
        ByteArrayOutputStream outStream = null;
×
413
        try {
414
            outStream = new ByteArrayOutputStream();
×
415

416
            ddiExportService.exportDataVariable(
×
417
                    varId,
418
                    outStream,
419
                    exclude,
420
                    include);
421
        } catch (Exception e) {
×
422
            // For whatever reason we've failed to generate a partial
423
            // metadata record requested. We simply return an empty string.
424
            return retValue;
×
425
        }
×
426

427
        retValue = outStream.toString();
×
428

429
        response.setHeader("Access-Control-Allow-Origin", "*");
×
430

431
        return retValue;
×
432
    }
433

434
    /*
435
     * "Preprocessed data" metadata format:
436
     * (this was previously provided as a "format conversion" option of the
437
     * file download form of the access API call)
438
     */
439

440
    @Path("datafile/{fileId}/metadata/preprocessed")
441
    @GET
442
    @Produces({"text/xml"})
443
    public DownloadInstance tabularDatafileMetadataPreprocessed(@PathParam("fileId") String fileId, @Context HttpServletResponse response) throws ServiceUnavailableException {
444

445
        DataFile df = findDataFileOrDieWrapper(fileId);
×
446

447
        // This will throw a ForbiddenException if access isn't authorized:
448
        checkAuthorization(df);
×
449
        DownloadInfo dInfo = new DownloadInfo(df);
×
450

451
        if (df.isTabularData()) {
×
452
            dInfo.addServiceAvailable(new OptionalAccessService("preprocessed", "application/json", "format=prep", "Preprocessed data in JSON"));
×
453
        } else {
454
            throw new BadRequestException("tabular data required");
×
455
        }
456
        DownloadInstance downloadInstance = new DownloadInstance(dInfo);
×
457
        if (downloadInstance.checkIfServiceSupportedAndSetConverter("format", "prep")) {
×
458
            logger.fine("Preprocessed data for tabular file " + fileId);
×
459
        }
460

461
        response.setHeader("Access-Control-Allow-Origin", "*");
×
462

463
        return downloadInstance;
×
464
    }
465

466
    /*
467
     * API method for downloading zipped bundles of multiple files:
468
     */
469

470
    // TODO: Rather than only supporting looking up files by their database IDs, consider supporting persistent identifiers.
471
    @Path("datafiles/{fileIds}")
472
    @GET
473
    @Produces({"application/zip"})
474
    public Response datafiles(@PathParam("fileIds") String fileIds, @QueryParam("gbrecs") Boolean gbrecs,
475
                              @Context UriInfo uriInfo, @Context HttpServletResponse response) throws WebApplicationException {
476
        assertOrThrowBadRequest(() -> StringUtils.isNotBlank(fileIds));
×
477

478
        final long zipDownloadSizeLimit = determineDownloadSizeLimit();
×
479
        logger.fine("setting zip download size limit to " + zipDownloadSizeLimit + " bytes.");
×
480

481
        User apiTokenUser = getApiTokenUserWithGuestFallbackOnInvalidToken(); //for use in adding gb records if necessary
×
482

483
        final boolean sendOriginalFormat = isOriginalFormatRequested(uriInfo.getQueryParameters());
×
484

485
        StreamingOutput stream = (OutputStream outputStream) -> {
×
486
            String[] fileIdParams = fileIds.split(",");
×
487
            assertOrThrowBadRequest(() -> fileIdParams.length > 0);
×
488
            logger.fine(fileIdParams.length + " tokens;");
×
489

490
            ZipperWrapper zipperWrapper = new ZipperWrapper();
×
491
            long sizeTotal = 0L;
×
492
            List<DataFile> filesToDownload = new ArrayList<>();
×
493

494
            for (String fileIdParam : fileIdParams) {
×
495
                logger.fine("token: " + fileIdParam);
×
496

497
                Long fileId;
498
                try {
499
                    fileId = new Long(fileIdParam);
×
500
                } catch (NumberFormatException nfe) {
×
501
                    logger.log(Level.WARNING, "Cannot parse file id [{0}]. Skipped.", fileIdParam);
×
502
                    continue;
×
503
                }
×
504

505
                logger.fine("attempting to look up file id " + fileId);
×
506
                DataFile file = dataFileService.find(fileId);
×
507
                if (file == null) {
×
508
                    continue;
×
509
                }
510

511
                if (isAccessAuthorized(file)) {
×
512
                    logger.fine("adding datafile (id=" + file.getId() + ") to the download list of the ZippedDownloadInstance.");
×
513
                    if (gbrecs == null && file.isReleased()) {
×
514
                        GuestbookResponse gbr = guestbookResponseService.initAPIGuestbookResponse(file.getOwner(), file, session, apiTokenUser);
×
515
                        guestbookResponseService.save(gbr);
×
516
                    }
517

518
                    if (zipperWrapper.isEmpty()) {
×
519
                        // This is the first file we can serve - so we now know that we are going to be able
520
                        // to produce some output.
521
                        zipperWrapper.init(outputStream);
×
522
                        response.setHeader("Content-disposition", "attachment; filename=\"dataverse_files.zip\"");
×
523
                        response.setHeader("Content-Type", "application/zip; name=\"dataverse_files.zip\"");
×
524
                    }
525

526
                    long size = computeFileSize(file, sendOriginalFormat);
×
527
                    if (size < (zipDownloadSizeLimit - sizeTotal)) {
×
528
                        sizeTotal += zipperWrapper.getZipper().addFileToZipStream(file, sendOriginalFormat);
×
529
                        filesToDownload.add(file);
×
530
                    } else {
531
                        String fileName = file.getFileMetadata().getLabel();
×
532
                        String mimeType = file.getContentType();
×
533
                        zipperWrapper.addToManifest(fileName + " (" + mimeType +
×
534
                                ") skipped because the total size of the download bundle exceeded the limit of "
535
                                + zipDownloadSizeLimit + " bytes.\r\n");
536
                    }
537
                } else if (embargoAccessService.isRestrictedByEmbargo(file.getOwner())) {
×
538
                    Supplier<MissingParameterValueException> exception = () ->
×
539
                            new MissingParameterValueException("[Couldn't retrive embargo date for file id=]" + file.getId());
×
540
                    zipperWrapper.addToManifest("File with id=" + file.getId() + " IS EMBARGOED UNTIL "
×
541
                            + file.getOwner().getEmbargoDate().getOrElseThrow(exception).toInstant() + "\r\n");
×
542
                } else if (file.getLatestFileMetadata().getTermsOfUse().getTermsOfUseType() == TermsOfUseType.RESTRICTED) {
×
543
                    zipperWrapper.addToManifest(file.getFileMetadata().getLabel() + " IS RESTRICTED AND CANNOT BE DOWNLOADED\r\n");
×
544
                } else {
545
                    // As of now this errors out. This is bad because the user ends up with a broken zip and manifest
546
                    // This is good in that the zip ends early so the user does not wait for the results
547
                    String errorMessage = "Datafile " + fileId + ": no such object available";
×
548
                    throw new NotFoundException(errorMessage);
×
549
                }
550
            }
551

552
            if (zipperWrapper.isEmpty()) {
×
553
                // If the DataFileZipper object is still NULL, it means that there were file ids supplied - but none of
554
                // the corresponding files were accessible for this user.
555
                // In which case we don't bother generating any output, and just give them a 403:
556
                throw new ForbiddenException();
×
557
            }
558

559
            // Check whether some subset of downloaded files is equal to some whole
560
            // set of files of some version and log if so
561
            wholeDatasetDownloadLogger.incrementLogIfDownloadingWholeDataset(filesToDownload);
×
562

563
            // This will add the generated File Manifest to the zipped output, then flush and close the stream:
564
            zipperWrapper.getZipper().finalizeZipStream();
×
565
        };
×
566
        return Response.ok(stream).build();
×
567
    }
568

569
    private long determineDownloadSizeLimit() {
570
        long limit = settingsService.getValueForKeyAsLong(SettingsServiceBean.Key.ZipDownloadLimit);
×
571
        if (limit == -1) {
×
572
            throw new BadRequestException("Download zipped bundles of multiple files option is disabled in this installation");
×
573
        }
574
        return limit != 0 ? limit : Long.MAX_VALUE;
×
575
    }
576

577
    private void assertOrThrowBadRequest(Supplier<Boolean> constraint) {
578
        if (!constraint.get()) {
×
579
            throw new BadRequestException();
×
580
        }
581
    }
×
582

583
    private boolean isOriginalFormatRequested(MultivaluedMap<String, String> queryParameters) {
584
        return queryParameters
×
585
                .keySet().stream()
×
586
                .filter("format"::equals)
×
587
                .map(queryParameters::getFirst)
×
588
                .anyMatch("original"::equals);
×
589
    }
590

591
    private long computeFileSize(DataFile file, boolean getOriginal) throws IOException {
592
        long size;
593
        // is the original format requested, and is this a tabular datafile, with a preserved original?
594
        if (getOriginal && file.isTabularData() && StringUtil.nonEmpty(file.getDataTable().getOriginalFileFormat())) {
×
595

596
            // We now store the size of the original file in the database (in DataTable), so we get it for free.
597
            // However, there may still be legacy datatables for which the size is not saved. so the "inefficient" code
598
            // is kept, below, as a fallback solution.
599
            // -- L.A., 4.10
600

601
            if (file.getDataTable().getOriginalFileSize() != null) {
×
602
                size = file.getDataTable().getOriginalFileSize();
×
603
            } else {
604
                StorageIO<DataFile> storageIO = DataAccess.dataAccess().getStorageIO(file);
×
605
                storageIO.open();
×
606
                size = storageIO.getAuxObjectSize(StorageIOConstants.SAVED_ORIGINAL_FILENAME_EXTENSION);
×
607

608
                // save it permanently:
609
                file.getDataTable().setOriginalFileSize(size);
×
610
                fileService.saveDataTable(file.getDataTable());
×
611
            }
612
            if (size == 0L) {
×
613
                throw new IOException("Invalid file size or accessObject when checking limits of zip file");
×
614
            }
615
        } else {
616
            size = file.getFilesize();
×
617
        }
618
        return size;
×
619
    }
620

621

622
    // TODO: Rather than only supporting looking up files by their database IDs, consider supporting persistent identifiers.
623
    @Path("fileCardImage/{fileId}")
624
    @GET
625
    @Produces({"image/png"})
626
    public InputStream fileCardImage(@PathParam("fileId") Long fileId) {
627

628

629
        DataFile df = dataFileService.find(fileId);
×
630

631
        if (df == null) {
×
632
            logger.warning("Preview: datafile service could not locate a DataFile object for id " + fileId + "!");
×
633
            return null;
×
634
        }
635

636
        if(embargoAccessService.isRestrictedByEmbargo(df.getOwner())) {
×
637
            logger.warning("Preview: datafile id[" + fileId + "] is restricted by embargo");
×
638
            return null;
×
639
        }
640

641
        StorageIO<DataFile> thumbnailDataAccess = null;
×
642

643
        try {
644
            if ("application/pdf".equalsIgnoreCase(df.getContentType())
×
645
                    || df.isImage()
×
646
                    || "application/zipped-shapefile".equalsIgnoreCase(df.getContentType())) {
×
647

648
                thumbnailDataAccess = imageThumbConverter.getImageThumbnailAsInputStream(df, 48);
×
649
                if (thumbnailDataAccess != null && thumbnailDataAccess.getInputStream() != null) {
×
650
                    return thumbnailDataAccess.getInputStream();
×
651
                }
652
            }
653
        } catch (IOException ioEx) {
×
654
            return null;
×
655
        }
×
656

657
        return null;
×
658
    }
659

660
    // Note:
661
    // the Dataverse page is no longer using this method.
662
    @Path("dsCardImage/{versionId}")
663
    @GET
664
    @Produces({"image/png"})
665
    public InputStream dsCardImage(@PathParam("versionId") Long versionId) {
666

667

668
        DatasetVersion datasetVersion = versionService.getById(versionId);
×
669

670
        if (datasetVersion == null) {
×
671
            logger.warning("Preview: Version service could not locate a DatasetVersion object for id " + versionId + "!");
×
672
            return null;
×
673
        }
674

675
        //String imageThumbFileName = null;
676
        StorageIO thumbnailDataAccess = null;
×
677

678
        // First, check if this dataset has a designated thumbnail image:
679

680
        if (datasetVersion.getDataset() != null) {
×
681

682
            DataFile logoDataFile = datasetVersion.getDataset().getThumbnailFile();
×
683
            if (logoDataFile != null) {
×
684

685
                try {
686
                    thumbnailDataAccess = imageThumbConverter.getImageThumbnailAsInputStream(logoDataFile, 48);
×
687
                    if (thumbnailDataAccess != null && thumbnailDataAccess.getInputStream() != null) {
×
688
                        return thumbnailDataAccess.getInputStream();
×
689
                    }
690
                } catch (IOException ioEx) {
×
691
                    thumbnailDataAccess = null;
×
692
                }
×
693
            }
694

695

696
            // If not, we'll try to use one of the files in this dataset version:
697
            /*
698
            if (thumbnailDataAccess == null) {
699

700
                if (!datasetVersion.getDataset().isHarvested()) {
701
                    thumbnailDataAccess = getThumbnailForDatasetVersion(datasetVersion);
702
                }
703
            }*/
704

705
        }
706

707
        return null;
×
708
    }
709

710
    @Path("dvCardImage/{dataverseId}")
711
    @GET
712
    @Produces({"image/png"})
713
    public InputStream dvCardImage(@PathParam("dataverseId") Long dataverseId) {
714
        logger.fine("entering dvCardImage");
×
715

716
        Dataverse dataverse = dataverseDao.find(dataverseId);
×
717

718
        if (dataverse == null) {
×
719
            logger.warning("Preview: Version service could not locate a DatasetVersion object for id " + dataverseId + "!");
×
720
            return null;
×
721
        }
722

723
        // First, check if the dataverse has a defined logo:
724

725
        if (dataverse.getDataverseTheme() != null && dataverse.getDataverseTheme().getLogo() != null && !dataverse.getDataverseTheme().getLogo().equals("")) {
×
726
            File dataverseLogoFile = getLogo(dataverse);
×
727
            if (dataverseLogoFile != null) {
×
728
                logger.fine("dvCardImage: logo file found");
×
729
                InputStream in = null;
×
730

731
                try {
732
                    if (dataverseLogoFile.exists()) {
×
733
                        String logoThumbNailPath = dataverseDao.getDataverseLogoThumbnailFilePath(dataverse.getId());
×
734
                        if (logoThumbNailPath != null) {
×
735
                            in = new FileInputStream(logoThumbNailPath);
×
736
                        }
737
                    }
738
                } catch (Exception ex) {
×
739
                    in = null;
×
740
                }
×
741
                if (in != null) {
×
742
                    logger.fine("dvCardImage: successfully obtained thumbnail for dataverse logo.");
×
743
                    return in;
×
744
                }
745
            }
746
        }
747

748
        return null;
×
749
    }
750

751
    // TODO:
752
    // put this method into the dataverseservice; use it there
753
    // -- L.A. 4.0 beta14
754

755
    private File getLogo(Dataverse dataverse) {
756
        if (dataverse.getId() == null) {
×
757
            return null;
×
758
        }
759

760
        DataverseTheme theme = dataverse.getDataverseTheme();
×
761
        if (theme != null && theme.getLogo() != null && !theme.getLogo().equals("")) {
×
762
            Properties p = System.getProperties();
×
763
            String domainRoot = p.getProperty("com.sun.aas.instanceRoot");
×
764

765
            if (domainRoot != null && !"".equals(domainRoot)) {
×
766
                return new File(domainRoot + File.separator +
×
767
                                        "docroot" + File.separator +
768
                                        "logos" + File.separator +
769
                                        dataverse.getLogoOwnerId() + File.separator +
×
770
                                        theme.getLogo());
×
771
            }
772
        }
773

774
        return null;
×
775
    }
776

777
    /**
778
     * Request Access to Restricted File
779
     *
780
     * @param fileToRequestAccessId
781
     * @param headers
782
     * @return
783
     * @author sekmiller
784
     */
785
    @PUT
786
    @ApiWriteOperation
787
    @Path("/datafile/{id}/requestAccess")
788
    public Response requestFileAccess(@PathParam("id") String fileToRequestAccessId) {
789

790
        DataverseRequest dataverseRequest;
791
        DataFile dataFile;
792

793
        try {
794
            dataFile = findDataFileOrDie(fileToRequestAccessId);
×
795
        } catch (WrappedResponse ex) {
×
796
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.fileNotFound", fileToRequestAccessId));
×
797
        }
×
798

799
        AuthenticatedUser requestor;
800

801
        try {
802
            requestor = findAuthenticatedUserOrDie();
×
803
            dataverseRequest = createDataverseRequest(requestor);
×
804
        } catch (WrappedResponse wr) {
×
805
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", wr.getLocalizedMessage()));
×
806
        }
×
807

808
        if (isAccessAuthorized(dataFile)) {
×
809
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.failure.invalidRequest"));
×
810
        }
811

812
        if (dataFile.getFileAccessRequesters().contains(requestor)) {
×
813
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.failure.requestExists"));
×
814
        }
815

816
        try {
817
            engineSvc.submit(new RequestAccessCommand(dataverseRequest, dataFile));
×
818

819
            filePermissionsService.sendRequestFileAccessNotification(dataFile.getOwner(), dataFile.getId(), requestor);
×
820
        } catch (CommandException ex) {
×
821
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.failure.commandError", dataFile.getDisplayName(), ex.getLocalizedMessage()));
×
822
        }
×
823

824
        return ok(BundleUtil.getStringFromBundle("access.api.requestAccess.success.for.single.file", dataFile.getDisplayName()));
×
825

826
    }
827

828
    /*
829
     * List Reqeusts to restricted file
830
     *
831
     * @author sekmiller
832
     *
833
     * @param fileToRequestAccessId
834
     * @param apiToken
835
     * @param headers
836
     * @return
837
     */
838
    @GET
839
    @Path("/datafile/{id}/listRequests")
840
    public Response listFileAccessRequests(@PathParam("id") String fileToRequestAccessId) {
841

842
        DataverseRequest dataverseRequest;
843

844
        DataFile dataFile;
845
        try {
846
            dataFile = findDataFileOrDie(fileToRequestAccessId);
×
847
        } catch (WrappedResponse ex) {
×
848
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestList.fileNotFound", fileToRequestAccessId));
×
849
        }
×
850

851
        try {
852
            dataverseRequest = createDataverseRequest(findUserOrDie());
×
853
        } catch (WrappedResponse wr) {
×
854
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", wr.getLocalizedMessage()));
×
855
        }
×
856

857
        if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() ||
×
858
                permissionService.requestOn(dataverseRequest, dataFile.getOwner()).has(Permission.ManageDatasetPermissions) ||
×
859
                permissionService.requestOn(dataverseRequest, dataFile.getOwner()).has(Permission.ManageMinorDatasetPermissions))) {
×
860
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions"));
×
861
        }
862

863
        List<AuthenticatedUser> requesters = dataFile.getFileAccessRequesters();
×
864

865
        if (requesters == null || requesters.isEmpty()) {
×
866
            List<String> args = Arrays.asList(dataFile.getDisplayName());
×
867
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestList.noRequestsFound"));
×
868
        }
869

870
        AuthenticatedUserDTO.Converter converter = new AuthenticatedUserDTO.Converter();
×
871
        return ok(requesters.stream()
×
872
                .map(converter::convert)
×
873
                .collect(Collectors.toList()));
×
874

875
    }
876

877
    /**
878
     * Grant Access to Restricted File
879
     *
880
     * @param fileToRequestAccessId
881
     * @param identifier
882
     * @return
883
     * @author sekmiller
884
     */
885
    @PUT
886
    @ApiWriteOperation
887
    @Path("/datafile/{id}/grantAccess/{identifier}")
888
    public Response grantFileAccess(@PathParam("id") String fileToRequestAccessId, @PathParam("identifier") String identifier) {
889

890
        DataverseRequest dataverseRequest;
891
        DataFile dataFile;
892

893
        try {
894
            dataFile = findDataFileOrDie(fileToRequestAccessId);
×
895
        } catch (WrappedResponse ex) {
×
896
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.fileNotFound", fileToRequestAccessId));
×
897
        }
×
898

899
        RoleAssignee ra = roleAssigneeSvc.getRoleAssignee(identifier);
×
900

901
        if (ra == null) {
×
902
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.grantAccess.noAssigneeFound", identifier));
×
903
        }
904

905
        try {
906
            dataverseRequest = createDataverseRequest(findUserOrDie());
×
907
        } catch (WrappedResponse wr) {
×
908
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", identifier));
×
909
        }
×
910

911
        DataverseRole fileDownloaderRole = roleService.findBuiltinRoleByAlias(BuiltInRole.FILE_DOWNLOADER);
×
912

913
        try {
914
            engineSvc.submit(new AssignRoleCommand(ra, fileDownloaderRole, dataFile, dataverseRequest, null));
×
915
            if (dataFile.getFileAccessRequesters().remove(ra)) {
×
916
                dataFileService.save(dataFile);
×
917
            }
918

919
        } catch (CommandException ex) {
×
920
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.grantAccess.failure.commandError", dataFile.getDisplayName(), ex.getLocalizedMessage()));
×
921
        }
×
922

923
        try {
924
            AuthenticatedUser au = (AuthenticatedUser) ra;
×
925
            userNotificationService.sendNotificationWithEmail(au, new Timestamp(new Date().getTime()), NotificationType.GRANTFILEACCESS,
×
926
                                                              dataFile.getOwner().getId(), NotificationObjectType.AUTHENTICATED_USER);
×
927
        } catch (ClassCastException e) {
×
928
            //nothing to do here - can only send a notification to an authenticated user
929
        }
×
930

931
        return ok(BundleUtil.getStringFromBundle("access.api.grantAccess.success.for.single.file", dataFile.getDisplayName()));
×
932

933
    }
934

935
    /**
936
     * Revoke Previously Granted Access to Restricted File
937
     *
938
     * @param fileToRequestAccessId
939
     * @param identifier
940
     * @return
941
     * @author sekmiller
942
     */
943
    @DELETE
944
    @ApiWriteOperation
945
    @Path("/datafile/{id}/revokeAccess/{identifier}")
946
    public Response revokeFileAccess(@PathParam("id") String fileToRequestAccessId, @PathParam("identifier") String identifier) {
947

948
        DataverseRequest dataverseRequest;
949
        DataFile dataFile;
950

951
        try {
952
            dataFile = findDataFileOrDie(fileToRequestAccessId);
×
953
        } catch (WrappedResponse ex) {
×
954
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.fileNotFound", fileToRequestAccessId));
×
955
        }
×
956

957
        try {
958
            dataverseRequest = createDataverseRequest(findUserOrDie());
×
959
        } catch (WrappedResponse wr) {
×
960
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", wr.getLocalizedMessage()));
×
961
        }
×
962

963
        if (identifier == null || identifier.equals("")) {
×
964
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.noKey"));
×
965
        }
966

967
        RoleAssignee ra = roleAssigneeSvc.getRoleAssignee(identifier);
×
968
        if (ra == null) {
×
969
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.grantAccess.noAssigneeFound", identifier));
×
970
        }
971

972
        DataverseRole fileDownloaderRole = roleService.findBuiltinRoleByAlias(BuiltInRole.FILE_DOWNLOADER);
×
973
        TypedQuery<RoleAssignment> query = em.createNamedQuery(
×
974
                "RoleAssignment.listByAssigneeIdentifier_DefinitionPointId_RoleId",
975
                RoleAssignment.class);
976
        query.setParameter("assigneeIdentifier", ra.getIdentifier());
×
977
        query.setParameter("definitionPointId", dataFile.getId());
×
978
        query.setParameter("roleId", fileDownloaderRole.getId());
×
979
        List<RoleAssignment> roles = query.getResultList();
×
980

981
        if (roles == null || roles.isEmpty()) {
×
982
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.revokeAccess.noRoleFound", identifier));
×
983
        }
984

985
        try {
986
            for (RoleAssignment role : roles) {
×
987
                execCommand(new RevokeRoleCommand(role, dataverseRequest));
×
988
            }
×
989
        } catch (WrappedResponse wr) {
×
990
            return wr.getResponse();
×
991
        }
×
992

993
        return ok(BundleUtil.getStringFromBundle("access.api.revokeAccess.success.for.single.file", ra.getIdentifier(), dataFile.getDisplayName()));
×
994

995
    }
996

997
    /**
998
     * Reject Access request to Restricted File
999
     *
1000
     * @param fileToRequestAccessId
1001
     * @param identifier
1002
     * @return
1003
     * @author sekmiller
1004
     */
1005
    @PUT
1006
    @ApiWriteOperation
1007
    @Path("/datafile/{id}/rejectAccess/{identifier}")
1008
    public Response rejectFileAccess(@PathParam("id") String fileToRequestAccessId, @PathParam("identifier") String identifier) {
1009

1010
        DataverseRequest dataverseRequest;
1011
        DataFile dataFile;
1012

1013
        try {
1014
            dataFile = findDataFileOrDie(fileToRequestAccessId);
×
1015
        } catch (WrappedResponse ex) {
×
1016
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.requestAccess.fileNotFound", fileToRequestAccessId));
×
1017
        }
×
1018

1019
        RoleAssignee ra = roleAssigneeSvc.getRoleAssignee(identifier);
×
1020

1021
        if (ra == null) {
×
1022
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.grantAccess.noAssigneeFound", identifier));
×
1023
        }
1024

1025
        try {
1026
            dataverseRequest = createDataverseRequest(findUserOrDie());
×
1027
        } catch (WrappedResponse wr) {
×
1028
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.fileAccess.failure.noUser", identifier));
×
1029
        }
×
1030

1031
        if (!(dataverseRequest.getAuthenticatedUser().isSuperuser() ||
×
1032
                permissionService.requestOn(dataverseRequest, dataFile.getOwner()).has(Permission.ManageDatasetPermissions) ||
×
1033
                permissionService.requestOn(dataverseRequest, dataFile.getOwner()).has(Permission.ManageMinorDatasetPermissions))) {
×
1034
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.rejectAccess.failure.noPermissions"));
×
1035
        }
1036

1037
        if (dataFile.getFileAccessRequesters().contains(ra)) {
×
1038
            dataFile.getFileAccessRequesters().remove(ra);
×
1039
            dataFileService.save(dataFile);
×
1040

1041
            try {
1042
                AuthenticatedUser au = (AuthenticatedUser) ra;
×
1043
                userNotificationService.sendNotificationWithEmail(au, new Timestamp(new Date().getTime()), NotificationType.REJECTFILEACCESS,
×
1044
                                                                  dataFile.getOwner().getId(), NotificationObjectType.AUTHENTICATED_USER);
×
1045
            } catch (ClassCastException e) {
×
1046
                //nothing to do here - can only send a notification to an authenticated user
1047
            }
×
1048

1049
            return ok(BundleUtil.getStringFromBundle("access.api.rejectAccess.success.for.single.file", dataFile.getDisplayName()));
×
1050

1051
        } else {
1052
            return error(BAD_REQUEST, BundleUtil.getStringFromBundle("access.api.fileAccess.rejectFailure.noRequest", dataFile.getDisplayName(), ra.getDisplayInfo().getTitle()));
×
1053
        }
1054
    }
1055

1056
    // checkAuthorization is a convenience method; it calls the boolean method
1057
    // isAccessAuthorized(), the actual workhorse, tand throws a 403 exception if not.
1058

1059
    private void checkAuthorization(DataFile df) throws WebApplicationException {
1060

1061
        if (!isAccessAuthorized(df)) {
×
1062
            throw new ForbiddenException();
×
1063
        }
1064
    }
×
1065

1066

1067
    protected boolean isAccessAuthorized(DataFile df) {
1068

1069
        boolean isRestrictedByEmbargo = embargoAccessService.isRestrictedByEmbargo(df.getOwner());
1✔
1070

1071
        if(isRestrictedByEmbargo) {
1✔
1072
            return false;
1✔
1073
        }
1074

1075
        boolean fileIsInsideAnyReleasedDsVersion = false;
1✔
1076

1077
        // We don't need to check permissions on files that are
1078
        // from released Dataset versions and not restricted:
1079
        for (FileMetadata fm : df.getFileMetadatas()) {
1✔
1080
            if (fm.getDatasetVersion().isReleased()) {
1✔
1081
                if (fm.getTermsOfUse().getTermsOfUseType() != TermsOfUseType.RESTRICTED) {
1✔
1082
                    return true;
1✔
1083
                }
1084
                fileIsInsideAnyReleasedDsVersion = true;
1✔
1085
            }
1086
        }
1✔
1087

1088
        // TODO: (IMPORTANT!)
1089
        // Business logic like this should NOT be maintained in individual
1090
        // application fragments.
1091
        // At the moment it is duplicated here, and inside the Dataset page.
1092
        // There are also stubs for file-level permission lookups and caching
1093
        // inside Gustavo's view-scoped PermissionsWrapper.
1094
        // All this logic needs to be moved to the PermissionServiceBean where it will be
1095
        // centrally maintained; with the PermissionsWrapper providing
1096
        // efficient cached lookups to the pages (that often need to make
1097
        // repeated lookups on the same files). Care will need to be taken
1098
        // to preserve the slight differences in logic utilized by the page and
1099
        // this Access call (the page checks the restriction flag on the
1100
        // filemetadata, not the datafile - as it needs to reflect the permission
1101
        // status of the file in the version history).
1102
        // I will open a 4.[34] ticket.
1103
        //
1104
        // -- L.A. 4.2.1
1105

1106
        User apiTokenUser = getApiTokenUserWithGuestFallbackOnInvalidToken();
1✔
1107
        User sessionUser = getSessionUserWithGuestFallback();
1✔
1108

1109
        if (!GuestUser.get().equals(apiTokenUser) && isAccessAuthorizedForUser(apiTokenUser, df, fileIsInsideAnyReleasedDsVersion)) {
1✔
1110
            logger.log(Level.FINE, "Token-based auth: user {0} has access rights on the datafile with id: {1}.",
×
1111
                    new Object[] { apiTokenUser.getIdentifier(), df.getId() });
×
1112
            return true;
×
1113
        } else if (!GuestUser.get().equals(sessionUser) && isAccessAuthorizedForUser(sessionUser, df, fileIsInsideAnyReleasedDsVersion)) {
1✔
1114
            logger.log(Level.FINE, "Session-based auth: user {0} has access rights on the datafile with id: {1}.",
1✔
1115
                    new Object[] { sessionUser.getIdentifier(), df.getId() });
1✔
1116
            return true;
1✔
1117
        } else if (isAccessAuthorizedForUser(GuestUser.get(), df, fileIsInsideAnyReleasedDsVersion)) {
1✔
1118
            logger.log(Level.FINE, "Guest user has access rights on the datafile with id: {1}.", df.getId());
×
1119
            return true;
×
1120
        }
1121

1122
        // And if all that failed, we'll still check if the download can be authorized based
1123
        // on the special WorldMap token:
1124

1125
        String apiToken = getRequestApiKey();
1✔
1126
        if ((apiToken != null) && (apiToken.length() == 64)) {
1✔
1127
            /*
1128
            WorldMap token check
1129
            - WorldMap tokens are 64 chars in length
1130

1131
            - Use the worldMapTokenServiceBean to verify token
1132
                and check permissions against the requested DataFile
1133
             */
1134
            boolean authorizedByWorldMapToken = worldMapTokenServiceBean.isWorldMapTokenAuthorizedForDataFileDownload(apiToken, df);
×
1135
            logger.fine("WorldMap token-based auth: Token is valid for the requested datafile");
×
1136
            return authorizedByWorldMapToken;
×
1137
        }
1138

1139
        if (!GuestUser.get().equals(sessionUser)) {
1✔
1140
            logger.log(Level.FINE, "Session-based auth: user {0} has NO access rights on the requested datafile.", sessionUser.getIdentifier());
1✔
1141
        } else if (!GuestUser.get().equals(apiTokenUser)) {
×
1142
            logger.log(Level.FINE, "Token-based auth: user {0} has NO access rights on the requested datafile.", apiTokenUser.getIdentifier());
×
1143
        } else {
1144
            logger.log(Level.FINE, "Guest user has NO access rights on the requested datafile.");
×
1145
        }
1146

1147
        return false;
1✔
1148
    }
1149

1150
    private boolean isAccessAuthorizedForUser(User user, DataFile datafile, boolean fileIsInsideAnyReleasedDsVersion) {
1151
        if (fileIsInsideAnyReleasedDsVersion) {
1✔
1152
            return permissionService.requestOn(createDataverseRequest(user), datafile).has(Permission.DownloadFile);
1✔
1153
        } else {
1154
            return permissionService.requestOn(createDataverseRequest(user), datafile.getOwner()).has(Permission.ViewUnpublishedDataset);
1✔
1155
        }
1156
    }
1157

1158
    private User getApiTokenUserWithGuestFallbackOnInvalidToken() {
1159
        return Try.of(this::findUserOrDie)
1✔
1160
                .onFailure(throwable -> logger.log(Level.FINE, "Failed finding user with apiToken", throwable))
1✔
1161
                .getOrElse(GuestUser.get());
1✔
1162
    }
1163

1164
    private Optional<Dataset> getDatasetFromDataVariable(Long dataVariableId) {
UNCOV
1165
        return Optional.ofNullable(variableService.find(dataVariableId).getDataTable().getDataFile().getOwner());
×
1166
    }
1167

1168
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc