From 4910dfd5273ccf46dceb97b993b988d69fbd4d9f Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Sun, 28 Oct 2018 17:03:21 +0100 Subject: [PATCH 001/129] Closes #252: route model permissions --- .../sismics/docs/core/dao/RouteModelDao.java | 7 ++++ .../core/dao/criteria/RouteModelCriteria.java | 15 +++++++ .../docs/core/dao/criteria/TagCriteria.java | 5 --- .../resources/db/update/dbupdate-015-0.sql | 2 + .../rest/resource/RouteModelResource.java | 32 +++++++++++---- .../docs/rest/resource/RouteResource.java | 5 +++ .../settings/SettingsWorkflowEdit.js | 41 ++++++++++--------- docs-web/src/main/webapp/src/locale/en.json | 3 +- .../partial/docs/settings.workflow.edit.html | 10 +++++ docs-web/src/main/webapp/src/style/main.less | 4 ++ .../docs/rest/TestRouteModelResource.java | 39 +++++++++++++++++- 11 files changed, 128 insertions(+), 35 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/RouteModelDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/RouteModelDao.java index 6f5f6736..af640875 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/RouteModelDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/RouteModelDao.java @@ -6,6 +6,7 @@ import com.sismics.docs.core.dao.criteria.RouteModelCriteria; import com.sismics.docs.core.dao.dto.RouteModelDto; import com.sismics.docs.core.model.jpa.RouteModel; import com.sismics.docs.core.util.AuditLogUtil; +import com.sismics.docs.core.util.SecurityUtil; import com.sismics.docs.core.util.jpa.QueryParam; import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; @@ -124,6 +125,12 @@ public class RouteModelDao { sb.append(" from T_ROUTE_MODEL rm "); // Add search criterias + if (criteria.getTargetIdList() != null && !SecurityUtil.skipAclCheck(criteria.getTargetIdList())) { + sb.append(" left join T_ACL a on a.ACL_TARGETID_C in (:targetIdList) and a.ACL_SOURCEID_C = rm.RTM_ID_C and a.ACL_PERM_C = 'READ' and a.ACL_DELETEDATE_D is null "); + criteriaList.add("a.ACL_ID_C is not null"); + parameterMap.put("targetIdList", criteria.getTargetIdList()); + } + criteriaList.add("rm.RTM_DELETEDATE_D is null"); if (!criteriaList.isEmpty()) { diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/RouteModelCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/RouteModelCriteria.java index 97223183..47bf1f06 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/RouteModelCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/RouteModelCriteria.java @@ -1,10 +1,25 @@ package com.sismics.docs.core.dao.criteria; +import java.util.List; + /** * Route model criteria. * * @author bgamard */ public class RouteModelCriteria { + /** + * ACL target ID list. + */ + private List targetIdList; + + public List getTargetIdList() { + return targetIdList; + } + + public RouteModelCriteria setTargetIdList(List targetIdList) { + this.targetIdList = targetIdList; + return this; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/TagCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/TagCriteria.java index a4f2ad33..e3ccbaad 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/TagCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/TagCriteria.java @@ -23,11 +23,6 @@ public class TagCriteria { */ private String documentId; - /** - * Tag name. - */ - private String name; - public String getId() { return id; } diff --git a/docs-core/src/main/resources/db/update/dbupdate-015-0.sql b/docs-core/src/main/resources/db/update/dbupdate-015-0.sql index fe67f85a..44435efe 100644 --- a/docs-core/src/main/resources/db/update/dbupdate-015-0.sql +++ b/docs-core/src/main/resources/db/update/dbupdate-015-0.sql @@ -7,5 +7,7 @@ alter table T_ROUTE_STEP add constraint FK_RTP_IDROUTE_C foreign key (RTP_IDROUT alter table T_ROUTE_STEP add constraint FK_RTP_IDVALIDATORUSER_C foreign key (RTP_IDVALIDATORUSER_C) references T_USER (USE_ID_C) on delete restrict on update restrict; insert into T_ROUTE_MODEL (RTM_ID_C, RTM_NAME_C, RTM_STEPS_C, RTM_CREATEDATE_D) values ('default-document-review', 'Document review', '[{"type":"VALIDATE","target":{"name":"administrators","type":"GROUP"},"name":"Check the document''s metadata"},{"type":"VALIDATE","target":{"name":"administrators","type":"GROUP"},"name":"Add relevant files to the document"},{"type":"APPROVE","target":{"name":"administrators","type":"GROUP"},"name":"Approve the document"}]', now()); +insert into T_ACL (ACL_ID_C, ACL_PERM_C, ACL_SOURCEID_C, ACL_TARGETID_C) values ('acl-admin-default-route-read', 'READ', 'default-document-review', 'administrators'); +insert into T_ACL (ACL_ID_C, ACL_PERM_C, ACL_SOURCEID_C, ACL_TARGETID_C) values ('acl-admin-default-route-write', 'WRITE', 'default-document-review', 'administrators'); update T_CONFIG set CFG_VALUE_C = '15' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java index 2214e408..35a7cf69 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java @@ -1,16 +1,14 @@ package com.sismics.docs.rest.resource; import com.google.common.collect.Lists; -import com.sismics.docs.core.constant.AclTargetType; -import com.sismics.docs.core.constant.ActionType; -import com.sismics.docs.core.constant.RouteStepTransition; -import com.sismics.docs.core.constant.RouteStepType; +import com.sismics.docs.core.constant.*; +import com.sismics.docs.core.dao.AclDao; import com.sismics.docs.core.dao.GroupDao; import com.sismics.docs.core.dao.RouteModelDao; -import com.sismics.docs.core.dao.TagDao; import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.dao.criteria.RouteModelCriteria; import com.sismics.docs.core.dao.dto.RouteModelDto; +import com.sismics.docs.core.model.jpa.Acl; import com.sismics.docs.core.model.jpa.Group; import com.sismics.docs.core.model.jpa.RouteModel; import com.sismics.docs.core.model.jpa.User; @@ -19,6 +17,7 @@ import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; +import com.sismics.rest.util.AclUtil; import com.sismics.rest.util.ValidationUtil; import javax.json.*; @@ -64,7 +63,7 @@ public class RouteModelResource extends BaseResource { SortCriteria sortCriteria = new SortCriteria(sortColumn, asc); RouteModelDao routeModelDao = new RouteModelDao(); - List routeModelDtoList = routeModelDao.findByCriteria(new RouteModelCriteria(), sortCriteria); + List routeModelDtoList = routeModelDao.findByCriteria(new RouteModelCriteria().setTargetIdList(getTargetIdList(null)), sortCriteria); for (RouteModelDto routeModelDto : routeModelDtoList) { routeModels.add(Json.createObjectBuilder() .add("id", routeModelDto.getId()) @@ -111,6 +110,23 @@ public class RouteModelResource extends BaseResource { .setName(name) .setSteps(steps), principal.getId()); + // Create read ACL + AclDao aclDao = new AclDao(); + Acl acl = new Acl(); + acl.setPerm(PermType.READ); + acl.setType(AclType.USER); + acl.setSourceId(id); + acl.setTargetId(principal.getId()); + aclDao.create(acl, principal.getId()); + + // Create write ACL + acl = new Acl(); + acl.setPerm(PermType.WRITE); + acl.setType(AclType.USER); + acl.setSourceId(id); + acl.setTargetId(principal.getId()); + aclDao.create(acl, principal.getId()); + // Always return OK JsonObjectBuilder response = Json.createObjectBuilder() .add("id", id); @@ -125,7 +141,6 @@ public class RouteModelResource extends BaseResource { private void validateRouteModelSteps(String steps) { UserDao userDao = new UserDao(); GroupDao groupDao = new GroupDao(); - TagDao tagDao = new TagDao(); try (JsonReader reader = Json.createReader(new StringReader(steps))) { JsonArray stepsJson = reader.readArray(); @@ -374,6 +389,9 @@ public class RouteModelResource extends BaseResource { .add("create_date", routeModel.getCreateDate().getTime()) .add("steps", routeModel.getSteps()); + // Add ACL + AclUtil.addAcls(response, id, getTargetIdList(null)); + return Response.ok().entity(response.build()).build(); } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java index e5aa515d..57b66b91 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java @@ -70,6 +70,11 @@ public class RouteResource extends BaseResource { throw new NotFoundException(); } + // Check permission on this route model + if (!aclDao.checkPermission(routeModelId, PermType.READ, getTargetIdList(null))) { + throw new ForbiddenClientException(); + } + // Avoid creating 2 running routes on the same document RouteStepDao routeStepDao = new RouteStepDao(); if (routeStepDao.getCurrentStep(documentId) != null) { diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js index 21b50447..a4a4dfa1 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js @@ -59,26 +59,6 @@ angular.module('docs').controller('SettingsWorkflowEdit', function($scope, $dial return $stateParams.id; }; - /** - * In edit mode, load the current workflow. - */ - if ($scope.isEdit()) { - Restangular.one('routemodel', $stateParams.id).get().then(function (data) { - $scope.workflow = data; - $scope.workflow.steps = JSON.parse(data.steps); - _.each($scope.workflow.steps, function (step) { - if (!step.transitions) { - // Patch for old route models - $scope.updateTransitions(step); - } - }); - }); - } else { - $scope.workflow = { - steps: [] - } - } - /** * Update the current workflow. */ @@ -188,4 +168,25 @@ angular.module('docs').controller('SettingsWorkflowEdit', function($scope, $dial Restangular.one('tag/list').get().then(function(data) { $scope.tags = data.tags; }); + + /** + * In edit mode, load the current workflow. + */ + if ($scope.isEdit()) { + Restangular.one('routemodel', $stateParams.id).get().then(function (data) { + $scope.workflow = data; + $scope.workflow.steps = JSON.parse(data.steps); + _.each($scope.workflow.steps, function (step) { + if (!step.transitions) { + // Patch for old route models + $scope.updateTransitions(step); + } + }); + }); + } else { + $scope.workflow = { + steps: [] + }; + $scope.addStep(); + } }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index e20d7514..b27ebd68 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -311,7 +311,8 @@ "target_help": "Approve: Accept or reject the review
Validate: Review and continue the workflow", "add_step": "Add a workflow step", "actions": "What happens after?", - "remove_action": "Remove action" + "remove_action": "Remove action", + "acl_info": "Only users and groups defined here will be able to start this workflow on a document" } }, "security": { diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html index dfdfeeef..1cc03eca 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html @@ -136,6 +136,16 @@ +
+
+

+ +
+
+
+ {{ 'settings.monitoring.reindexing_started' | translate }} +
+

From cee82f39c285b8510dd53d551c9cae3c0489e7e2 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 1 Nov 2018 16:27:35 +0100 Subject: [PATCH 006/129] #254: display documents in grid + concept of main file --- .../sismics/docs/core/dao/DocumentDao.java | 27 +++++++--- .../docs/core/dao/dto/DocumentDto.java | 16 +++++- .../async/DocumentUpdatedAsyncListener.java | 16 +++++- .../sismics/docs/core/model/jpa/Document.java | 15 ++++++ .../util/indexing/LuceneIndexingHandler.java | 3 +- .../src/main/resources/config.properties | 2 +- .../resources/db/update/dbupdate-021-0.sql | 4 ++ .../sismics/docs/rest/util/ClientUtil.java | 10 ++++ docs-web/src/dev/resources/config.properties | 2 +- .../docs/rest/resource/DocumentResource.java | 2 + .../docs/rest/resource/FileResource.java | 6 +++ .../app/docs/controller/document/Document.js | 10 +++- docs-web/src/main/webapp/src/locale/en.json | 2 + .../webapp/src/partial/docs/document.html | 51 ++++++++++++++++++- docs-web/src/main/webapp/src/style/main.less | 4 ++ docs-web/src/prod/resources/config.properties | 2 +- .../src/stress/resources/config.properties | 2 +- .../docs/rest/TestDocumentResource.java | 1 + 18 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 docs-core/src/main/resources/db/update/dbupdate-021-0.sql diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java index f3bb555c..34d170c7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java @@ -189,20 +189,35 @@ public class DocumentDao { } /** - * Update a document. + * Update a document and log the action. * * @param document Document to update * @param userId User ID * @return Updated document */ public Document update(Document document, String userId) { - EntityManager em = ThreadLocalContext.get().getEntityManager(); + Document documentDb = updateSilently(document); + // Create audit log + AuditLogUtil.create(documentDb, AuditLogType.UPDATE, userId); + + return documentDb; + } + + /** + * Update a document without audit log. + * + * @param document Document to update + * @return Updated document + */ + public Document updateSilently(Document document) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + // Get the document Query q = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null"); q.setParameter("id", document.getId()); Document documentDb = (Document) q.getSingleResult(); - + // Update the document documentDb.setTitle(document.getTitle()); documentDb.setDescription(document.getDescription()); @@ -216,11 +231,9 @@ public class DocumentDao { documentDb.setRights(document.getRights()); documentDb.setCreateDate(document.getCreateDate()); documentDb.setLanguage(document.getLanguage()); + documentDb.setFileId(document.getFileId()); documentDb.setUpdateDate(new Date()); - - // Create audit log - AuditLogUtil.create(documentDb, AuditLogType.UPDATE, userId); - + return documentDb; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentDto.java index 63aeb58f..48907984 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentDto.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentDto.java @@ -10,7 +10,12 @@ public class DocumentDto { * Document ID. */ private String id; - + + /** + * Main file ID. + */ + private String fileId; + /** * Title. */ @@ -114,6 +119,15 @@ public class DocumentDto { this.id = id; } + public String getFileId() { + return fileId; + } + + public DocumentDto setFileId(String fileId) { + this.fileId = fileId; + return this; + } + public String getTitle() { return title; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java index da0eb5b7..fb791d2a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentUpdatedAsyncListener.java @@ -4,10 +4,12 @@ import com.google.common.eventbus.AllowConcurrentEvents; import com.google.common.eventbus.Subscribe; import com.sismics.docs.core.dao.ContributorDao; import com.sismics.docs.core.dao.DocumentDao; +import com.sismics.docs.core.dao.FileDao; import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent; import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.jpa.Contributor; import com.sismics.docs.core.model.jpa.Document; +import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.TransactionUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,13 +40,25 @@ public class DocumentUpdatedAsyncListener { } TransactionUtil.handle(() -> { - // Update index + // Get the document DocumentDao documentDao = new DocumentDao(); Document document = documentDao.getById(event.getDocumentId()); if (document == null) { // Document deleted since event fired return; } + + // Set the main file + FileDao fileDao = new FileDao(); + List fileList = fileDao.getByDocumentId(null, event.getDocumentId()); + if (fileList.isEmpty()) { + document.setFileId(null); + } else { + document.setFileId(fileList.get(0).getId()); + } + + // Update database and index + documentDao.updateSilently(document); AppContext.getInstance().getIndexingHandler().updateDocument(document); // Update contributors list diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java index 626e631c..9fc76692 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java @@ -29,6 +29,12 @@ public class Document implements Loggable { @Column(name = "DOC_IDUSER_C", nullable = false, length = 36) private String userId; + /** + * Main file ID. + */ + @Column(name = "DOC_IDFILE_C", length = 36) + private String fileId; + /** * Language (ISO 639-9). */ @@ -137,6 +143,15 @@ public class Document implements Loggable { this.userId = userId; } + public String getFileId() { + return fileId; + } + + public Document setFileId(String fileId) { + this.fileId = fileId; + return this; + } + public String getTitle() { return title; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java index 6062557c..40f17127 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java @@ -219,7 +219,7 @@ public class LuceneIndexingHandler implements IndexingHandler { List criteriaList = new ArrayList<>(); Map documentSearchMap = Maps.newHashMap(); - StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C c0, d.DOC_TITLE_C c1, d.DOC_DESCRIPTION_C c2, d.DOC_CREATEDATE_D c3, d.DOC_LANGUAGE_C c4, "); + StringBuilder sb = new StringBuilder("select distinct d.DOC_ID_C c0, d.DOC_TITLE_C c1, d.DOC_DESCRIPTION_C c2, d.DOC_CREATEDATE_D c3, d.DOC_LANGUAGE_C c4, d.DOC_IDFILE_C, "); sb.append(" s.count c5, "); sb.append(" f.count c6, "); sb.append(" rs2.RTP_ID_C c7, rs2.RTP_NAME_C, d.DOC_UPDATEDATE_D c8 "); @@ -323,6 +323,7 @@ public class LuceneIndexingHandler implements IndexingHandler { documentDto.setDescription((String) o[i++]); documentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); documentDto.setLanguage((String) o[i++]); + documentDto.setFileId((String) o[i++]); Number shareCount = (Number) o[i++]; documentDto.setShared(shareCount != null && shareCount.intValue() > 0); Number fileCount = (Number) o[i++]; diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index 4d9e12ea..eb3e256a 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=20 \ No newline at end of file +db.version=21 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-021-0.sql b/docs-core/src/main/resources/db/update/dbupdate-021-0.sql new file mode 100644 index 00000000..5564fcf4 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-021-0.sql @@ -0,0 +1,4 @@ +alter table T_DOCUMENT add column DOC_IDFILE_C varchar(36); +alter table T_DOCUMENT add constraint FK_DOC_IDFILE_C foreign key (DOC_IDFILE_C) references T_FILE (FIL_ID_C) on delete restrict on update restrict; + +update T_CONFIG set CFG_VALUE_C = '21' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java index a45254e2..2213160e 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java @@ -154,6 +154,16 @@ public class ClientUtil { return authToken; } + /** + * Add a file to a document. + * + * @param file File path + * @param filename Filename + * @param token Authentication token + * @param documentId Document ID + * @return File ID + * @throws IOException e + */ public String addFileToDocument(String file, String filename, String token, String documentId) throws IOException { try (InputStream is = Resources.getResource(file).openStream()) { StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, filename); diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 1c8df450..e55e3dbc 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=20 \ No newline at end of file +db.version=21 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index 9e5fb928..2c7380b7 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -326,6 +326,7 @@ public class DocumentResource extends BaseResource { * @apiSuccess {Object[]} documents List of documents * @apiSuccess {String} documents.id ID * @apiSuccess {String} documents.highlight Search highlight (for fulltext search) + * @apiSuccess {String} documents.file_id Main file ID * @apiSuccess {String} documents.title Title * @apiSuccess {String} documents.description Description * @apiSuccess {Number} documents.create_date Create date (timestamp) @@ -395,6 +396,7 @@ public class DocumentResource extends BaseResource { documents.add(Json.createObjectBuilder() .add("id", documentDto.getId()) .add("highlight", JsonUtil.nullable(documentDto.getHighlight())) + .add("file_id", JsonUtil.nullable(documentDto.getFileId())) .add("title", documentDto.getTitle()) .add("description", JsonUtil.nullable(documentDto.getDescription())) .add("create_date", documentDto.getCreateTimestamp()) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index e972d197..4f03f156 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -367,6 +367,12 @@ public class FileResource extends BaseResource { file.setOrder(order); } } + + // Raise a document updated event + DocumentUpdatedAsyncEvent event = new DocumentUpdatedAsyncEvent(); + event.setUserId(principal.getId()); + event.setDocumentId(documentId); + ThreadLocalContext.get().addAsyncEvent(event); // Always return OK JsonObjectBuilder response = Json.createObjectBuilder() diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js index 7305ed3d..e3365405 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js @@ -12,6 +12,7 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim $scope.offset = 0; $scope.currentPage = 1; $scope.limit = _.isUndefined(localStorage.documentsPageSize) ? '10' : localStorage.documentsPageSize; + $scope.displayMode = _.isUndefined(localStorage.displayMode) ? 'list' : localStorage.displayMode; $scope.search = $state.params.search ? $state.params.search : ''; $scope.setSearch = function (search) { $scope.search = search }; $scope.searchOpened = false; @@ -113,7 +114,14 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim } $scope.loadDocuments(); }); - + + /** + * Watch for display mode change. + */ + $scope.$watch('displayMode', function (next) { + localStorage.displayMode = next; + }); + /** * Display a document. */ diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index f8b9c473..869760ac 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -41,6 +41,8 @@ "document": { "navigation_up": "Go up one level", "toggle_navigation": "Toggle folder navigation", + "display_mode_list": "Display documents in list", + "display_mode_grid": "Display documents in grid", "search_simple": "Simple search", "search_fulltext": "Fulltext search", "search_creator": "Creator", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 00f64cc0..8fa5ad05 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -169,6 +169,22 @@ + +
+ + + + + + +
+
+
@@ -253,6 +269,39 @@
{{ 'document.title' | translate }}
+ +
+
+ +
+ +
+ {{ 'document.no_documents' | translate }} + +
+ +
+
+
+ + + + +
+
+
+ {{ document.title }} ({{ document.file_count }}) + + +
+
+
+
+
+
+
+
+
    Date: Thu, 1 Nov 2018 16:29:22 +0100 Subject: [PATCH 007/129] #254: no 404 if no main file for a document --- docs-web/src/main/webapp/src/partial/docs/document.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 8fa5ad05..9b75e6a0 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -284,7 +284,7 @@
    - +
    From a75b40bbfb9dcdcc6f2a9f929301c5594899f251 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 1 Nov 2018 16:40:55 +0100 Subject: [PATCH 008/129] fix tests --- .../java/com/sismics/docs/rest/TestAuditLogResource.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java index 54e2073e..d8efd589 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java @@ -56,7 +56,7 @@ public class TestAuditLogResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, auditlog1Token) .get(JsonObject.class); JsonArray logs = json.getJsonArray("logs"); - Assert.assertTrue(logs.size() == 3); + Assert.assertEquals(3, logs.size()); Assert.assertEquals(countByClass(logs, "Document"), 1); Assert.assertEquals(countByClass(logs, "Acl"), 2); Assert.assertEquals("auditlog1", logs.getJsonObject(0).getString("username")); @@ -73,7 +73,7 @@ public class TestAuditLogResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, auditlog1Token) .get(JsonObject.class); logs = json.getJsonArray("logs"); - Assert.assertTrue(logs.size() == 2); + Assert.assertEquals(2, logs.size()); Assert.assertEquals(countByClass(logs, "Document"), 1); Assert.assertEquals(countByClass(logs, "Tag"), 1); @@ -88,7 +88,7 @@ public class TestAuditLogResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, auditlog1Token) .get(JsonObject.class); logs = json.getJsonArray("logs"); - Assert.assertTrue(logs.size() == 3); + Assert.assertEquals(3, logs.size()); Assert.assertEquals(countByClass(logs, "Document"), 1); Assert.assertEquals(countByClass(logs, "Tag"), 2); @@ -105,7 +105,7 @@ public class TestAuditLogResource extends BaseJerseyTest { json = target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, auditlog1Token) .get(JsonObject.class); - Assert.assertEquals(update1Date, json.getJsonNumber("update_date").longValue()); + Assert.assertTrue(json.getJsonNumber("update_date").longValue() > update1Date); // Adding a file to a document updates it // Get all logs for the document json = target().path("/auditlog") From 42828efa19ecedc9efe82c28ac5eff5a75c5b733 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 7 Nov 2018 13:42:43 +0100 Subject: [PATCH 009/129] #168: disable TOTP as admin for a specific user --- .../docs/rest/resource/UserResource.java | 48 +++++++++++++++++-- .../sismics/docs/rest/TestUserResource.java | 15 +++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index ed0df960..1b01b094 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -489,6 +489,7 @@ public class UserResource extends BaseResource { * @apiDescription All associated entities will be deleted as well. * @apiName DeleteUserUsername * @apiGroup User + * @apiParam {String} username Username * @apiSuccess {String} status Status OK * @apiError (client) ForbiddenError Access denied or the user cannot be deleted * @apiError (client) UserNotFound The user does not exist @@ -555,6 +556,47 @@ public class UserResource extends BaseResource { .add("status", "ok"); return Response.ok().entity(response.build()).build(); } + + /** + * Disable time-based one-time password for a specific user. + * + * @api {post} /user/:username/disable_totp Disable TOTP authentication for a specific user + * @apiName PostUserUsernameDisableTotp + * @apiGroup User + * @apiParam {String} username Username + * @apiSuccess {String} status Status OK + * @apiError (client) ForbiddenError Access denied or connected as guest + * @apiError (client) ValidationError Validation error + * @apiPermission user + * @apiVersion 1.5.0 + * + * @param username Username + * @return Response + */ + @POST + @Path("{username: [a-zA-Z0-9_]+}/disable_totp") + public Response disableTotpUsername(@PathParam("username") String username) { + if (!authenticate() || principal.isGuest()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + // Get the user + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(username); + if (user == null) { + throw new ForbiddenClientException(); + } + + // Remove the TOTP key + user.setTotpKey(null); + userDao.update(user, principal.getId()); + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } /** * Returns the information about the connected user. @@ -683,7 +725,7 @@ public class UserResource extends BaseResource { .add("disabled", user.getDisableDate() != null); return Response.ok().entity(response.build()).build(); } - + /** * Returns all active users. * @@ -876,9 +918,9 @@ public class UserResource extends BaseResource { } /** - * Disable time-based one-time password. + * Disable time-based one-time password for the current user. * - * @api {post} /user/disable_totp Disable TOTP authentication + * @api {post} /user/disable_totp Disable TOTP authentication for the current user * @apiName PostUserDisableTotp * @apiGroup User * @apiParam {String{1..100}} password Password diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index 7013ac61..74ee47dd 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -323,6 +323,9 @@ public class TestUserResource extends BaseJerseyTest { @Test public void testTotp() { + // Login admin + String adminToken = clientUtil.login("admin", "admin", false); + // Create totp1 user clientUtil.createUser("totp1"); String totp1Token = clientUtil.login("totp1"); @@ -373,7 +376,17 @@ public class TestUserResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) .post(Entity.form(new Form() .param("password", "12345678")), JsonObject.class); - + + // Enable TOTP for totp1 + target().path("/user/enable_totp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) + .post(Entity.form(new Form()), JsonObject.class); + + // Disable TOTP for totp1 with admin + target().path("/user/totp1/disable_totp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form()), JsonObject.class); + // Login with totp1 without a validation code target().path("/user/login").request() .post(Entity.form(new Form() From d8d5249a2328767e7b60b165d6699786d6ccdc8c Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 9 Nov 2018 14:49:34 +0100 Subject: [PATCH 010/129] Closes #257: admin users can see all logs --- .../sismics/docs/core/dao/AuditLogDao.java | 19 +++++++++++-------- .../core/dao/criteria/AuditLogCriteria.java | 14 ++++++++++++++ .../docs/rest/resource/AuditLogResource.java | 2 ++ .../docs/rest/resource/UserResource.java | 5 ++++- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/AuditLogDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/AuditLogDao.java index b501fa81..037ef676 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/AuditLogDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/AuditLogDao.java @@ -27,7 +27,6 @@ public class AuditLogDao { * * @param auditLog Audit log * @return New ID - * @throws Exception */ public String create(AuditLog auditLog) { // Create the UUID @@ -47,10 +46,9 @@ public class AuditLogDao { * @param paginatedList List of audit logs (updated by side effects) * @param criteria Search criteria * @param sortCriteria Sort criteria - * @return List of audit logs */ public void findByCriteria(PaginatedList paginatedList, AuditLogCriteria criteria, SortCriteria sortCriteria) { - Map parameterMap = new HashMap(); + Map parameterMap = new HashMap<>(); StringBuilder baseQuery = new StringBuilder("select l.LOG_ID_C c0, l.LOG_CREATEDATE_D c1, u.USE_USERNAME_C c2, l.LOG_IDENTITY_C c3, l.LOG_CLASSENTITY_C c4, l.LOG_TYPE_C c5, l.LOG_MESSAGE_C c6 from T_AUDIT_LOG l "); baseQuery.append(" join T_USER u on l.LOG_IDUSER_C = u.USE_ID_C "); @@ -67,10 +65,15 @@ public class AuditLogDao { } if (criteria.getUserId() != null) { - // Get all logs originating from the user, not necessarly on owned items - // Filter out ACL logs - queries.add(baseQuery + " where l.LOG_IDUSER_C = :userId and l.LOG_CLASSENTITY_C != 'Acl' "); - parameterMap.put("userId", criteria.getUserId()); + if (criteria.isAdmin()) { + // For admin users, display all logs except ACL logs + queries.add(baseQuery + " where l.LOG_CLASSENTITY_C != 'Acl' "); + } else { + // Get all logs originating from the user, not necessarly on owned items + // Filter out ACL logs + queries.add(baseQuery + " where l.LOG_IDUSER_C = :userId and l.LOG_CLASSENTITY_C != 'Acl' "); + parameterMap.put("userId", criteria.getUserId()); + } } // Perform the search @@ -78,7 +81,7 @@ public class AuditLogDao { List l = PaginatedLists.executePaginatedQuery(paginatedList, queryParam, sortCriteria); // Assemble results - List auditLogDtoList = new ArrayList(); + List auditLogDtoList = new ArrayList<>(); for (Object[] o : l) { int i = 0; AuditLogDto auditLogDto = new AuditLogDto(); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/AuditLogCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/AuditLogCriteria.java index eca636f6..890adae2 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/AuditLogCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/AuditLogCriteria.java @@ -16,6 +16,11 @@ public class AuditLogCriteria { * User ID. */ private String userId; + + /** + * The search is done for an admin user. + */ + private boolean isAdmin = false; public String getDocumentId() { return documentId; @@ -32,4 +37,13 @@ public class AuditLogCriteria { public void setUserId(String userId) { this.userId = userId; } + + public boolean isAdmin() { + return isAdmin; + } + + public AuditLogCriteria setAdmin(boolean admin) { + isAdmin = admin; + return this; + } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java index 51bcd0cb..1c12c314 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java @@ -6,6 +6,7 @@ import com.sismics.docs.core.dao.AclDao; import com.sismics.docs.core.dao.AuditLogDao; import com.sismics.docs.core.dao.criteria.AuditLogCriteria; import com.sismics.docs.core.dao.dto.AuditLogDto; +import com.sismics.docs.core.util.SecurityUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.SortCriteria; @@ -65,6 +66,7 @@ public class AuditLogResource extends BaseResource { if (Strings.isNullOrEmpty(documentId)) { // Search logs for a user criteria.setUserId(principal.getId()); + criteria.setAdmin(SecurityUtil.skipAclCheck(getTargetIdList(null))); } else { // Check ACL on the document AclDao aclDao = new AclDao(); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 1b01b094..94097186 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -680,6 +680,7 @@ public class UserResource extends BaseResource { * @apiParam {String} username Username * @apiSuccess {String} username Username * @apiSuccess {String} email E-mail + * @apiSuccess {Boolean} totp_enabled True if TOTP authentication is enabled * @apiSuccess {Number} storage_quota Storage quota (in bytes) * @apiSuccess {Number} storage_current Quota used (in bytes) * @apiSuccess {String[]} groups Groups @@ -720,6 +721,7 @@ public class UserResource extends BaseResource { .add("username", user.getUsername()) .add("groups", groups) .add("email", user.getEmail()) + .add("totp_enabled", user.getTotpKey() != null) .add("storage_quota", user.getStorageQuota()) .add("storage_current", user.getStorageCurrent()) .add("disabled", user.getDisableDate() != null); @@ -739,6 +741,7 @@ public class UserResource extends BaseResource { * @apiSuccess {String} users.id ID * @apiSuccess {String} users.username Username * @apiSuccess {String} users.email E-mail + * @apiSuccess {Boolean} users.totp_enabled True if TOTP authentication is enabled * @apiSuccess {Number} users.storage_quota Storage quota (in bytes) * @apiSuccess {Number} users.storage_current Quota used (in bytes) * @apiSuccess {Number} users.create_date Create date (timestamp) @@ -781,8 +784,8 @@ public class UserResource extends BaseResource { users.add(Json.createObjectBuilder() .add("id", userDto.getId()) .add("username", userDto.getUsername()) - .add("totp_enabled", userDto.getTotpKey() != null) .add("email", userDto.getEmail()) + .add("totp_enabled", userDto.getTotpKey() != null) .add("storage_quota", userDto.getStorageQuota()) .add("storage_current", userDto.getStorageCurrent()) .add("create_date", userDto.getCreateTimestamp()) From 3902d6361ea0ef02d56cfff4069a3ae19a72b36e Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 23 Nov 2018 14:54:11 +0100 Subject: [PATCH 011/129] #256: versioning API --- .../com/sismics/docs/core/dao/FileDao.java | 8 ++-- .../com/sismics/docs/core/model/jpa/File.java | 45 +++++++++++++++++++ .../docs/core/service/InboxService.java | 4 +- .../com/sismics/docs/core/util/FileUtil.java | 45 ++++++++++++++----- .../src/main/resources/config.properties | 2 +- .../resources/db/update/dbupdate-022-0.sql | 5 +++ docs-web/src/dev/resources/config.properties | 2 +- .../docs/rest/resource/DocumentResource.java | 4 +- .../docs/rest/resource/FileResource.java | 6 ++- docs-web/src/prod/resources/config.properties | 2 +- .../src/stress/resources/config.properties | 2 +- .../sismics/docs/rest/TestFileResource.java | 34 ++++++++++++++ 12 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 docs-core/src/main/resources/db/update/dbupdate-022-0.sql diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/FileDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/FileDao.java index 4610621e..a35a03e4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/FileDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/FileDao.java @@ -148,7 +148,9 @@ public class FileDao { fileDb.setContent(file.getContent()); fileDb.setOrder(file.getOrder()); fileDb.setMimeType(file.getMimeType()); - + fileDb.setVersionId(file.getVersionId()); + fileDb.setLatestVersion(file.isLatestVersion()); + return file; } @@ -180,11 +182,11 @@ public class FileDao { public List getByDocumentId(String userId, String documentId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); if (documentId == null) { - Query q = em.createQuery("select f from File f where f.documentId is null and f.deleteDate is null and f.userId = :userId order by f.createDate asc"); + Query q = em.createQuery("select f from File f where f.documentId is null and f.deleteDate is null and f.latestVersion = true and f.userId = :userId order by f.createDate asc"); q.setParameter("userId", userId); return q.getResultList(); } - Query q = em.createQuery("select f from File f where f.documentId = :documentId and f.deleteDate is null order by f.order asc"); + Query q = em.createQuery("select f from File f where f.documentId = :documentId and f.latestVersion = true and f.deleteDate is null order by f.order asc"); q.setParameter("documentId", documentId); return q.getResultList(); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java index 3db2ab7b..c3e7064a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java @@ -71,6 +71,24 @@ public class File implements Loggable { @Column(name = "FIL_ORDER_N") private Integer order; + /** + * Version ID. + */ + @Column(name = "FIL_IDVERSION_C") + private String versionId; + + /** + * Version number (starting at 0). + */ + @Column(name = "FIL_VERSION_N", nullable = false) + private Integer version; + + /** + * True if it's the latest version of the file. + */ + @Column(name = "FIL_LATESTVERSION_B", nullable = false) + private boolean latestVersion; + /** * Private key to decrypt the file. * Not saved to database, of course. @@ -160,6 +178,33 @@ public class File implements Loggable { this.privateKey = privateKey; } + public String getVersionId() { + return versionId; + } + + public File setVersionId(String versionId) { + this.versionId = versionId; + return this; + } + + public Integer getVersion() { + return version; + } + + public File setVersion(Integer version) { + this.version = version; + return this; + } + + public boolean isLatestVersion() { + return latestVersion; + } + + public File setLatestVersion(boolean latestVersion) { + this.latestVersion = latestVersion; + return this; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java b/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java index a349d329..ae0b7500 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java +++ b/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java @@ -212,7 +212,7 @@ public class InboxService extends AbstractScheduledService { } // Save the document, create the base ACLs - document = DocumentUtil.createDocument(document, "admin"); + DocumentUtil.createDocument(document, "admin"); // Add the tag String tagId = ConfigUtil.getConfigStringValue(ConfigType.INBOX_TAG); @@ -232,7 +232,7 @@ public class InboxService extends AbstractScheduledService { // Add files to the document for (EmailUtil.FileContent fileContent : mailContent.getFileContentList()) { - FileUtil.createFile(fileContent.getName(), fileContent.getFile(), fileContent.getSize(), + FileUtil.createFile(fileContent.getName(), null, fileContent.getFile(), fileContent.getSize(), document.getLanguage(), "admin", document.getId()); } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java index a4604038..98dbeca0 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java @@ -98,6 +98,7 @@ public class FileUtil { * Create a new file. * * @param name File name, can be null + * @param previousFileId ID of the previous version of the file, if the new file is a new version * @param unencryptedFile Path to the unencrypted file * @param fileSize File size * @param language File language, can be null if associated to no document @@ -106,7 +107,7 @@ public class FileUtil { * @return File ID * @throws Exception e */ - public static String createFile(String name, Path unencryptedFile, long fileSize, String language, String userId, String documentId) throws Exception { + public static String createFile(String name, String previousFileId, Path unencryptedFile, long fileSize, String language, String userId, String documentId) throws Exception { // Validate mime type String mimeType; try { @@ -132,22 +133,42 @@ public class FileUtil { } } - // Get files of this document - FileDao fileDao = new FileDao(); - int order = 0; - if (documentId != null) { - for (File file : fileDao.getByDocumentId(userId, documentId)) { - file.setOrder(order++); - } - } - - // Create the file + // Prepare the file File file = new File(); - file.setOrder(order); + file.setOrder(0); + file.setVersion(0); + file.setLatestVersion(true); file.setDocumentId(documentId); file.setName(StringUtils.abbreviate(name, 200)); file.setMimeType(mimeType); file.setUserId(userId); + + // Get files of this document + FileDao fileDao = new FileDao(); + if (documentId != null) { + if (previousFileId == null) { + // It's not a new version, so put it in last order + file.setOrder(fileDao.getByDocumentId(userId, documentId).size()); + } else { + // It's a new version, update the previous version + File previousFile = fileDao.getActiveById(previousFileId); + if (previousFile == null || !previousFile.getDocumentId().equals(documentId)) { + throw new IOException("Previous version mismatch"); + } + + if (previousFile.getVersionId() == null) { + previousFile.setVersionId(UUID.randomUUID().toString()); + } + + previousFile.setLatestVersion(false); + file.setVersionId(previousFile.getVersionId()); + file.setVersion(previousFile.getVersion() + 1); + + fileDao.update(previousFile); + } + } + + // Create the file String fileId = fileDao.create(file, userId); // Save the file diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index eb3e256a..5d8d3532 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=21 \ No newline at end of file +db.version=22 \ No newline at end of file diff --git a/docs-core/src/main/resources/db/update/dbupdate-022-0.sql b/docs-core/src/main/resources/db/update/dbupdate-022-0.sql new file mode 100644 index 00000000..c8333f93 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-022-0.sql @@ -0,0 +1,5 @@ +alter table T_FILE add column FIL_VERSION_N int not null default 0; +alter table T_FILE add column FIL_LATESTVERSION_B bit not null default 1; +alter table T_FILE add column FIL_IDVERSION_C varchar(36); + +update T_CONFIG set CFG_VALUE_C = '22' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index e55e3dbc..7ab7347c 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=21 \ No newline at end of file +db.version=22 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index 2c7380b7..7de9134f 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -900,7 +900,7 @@ public class DocumentResource extends BaseResource { } // Save the document, create the base ACLs - document = DocumentUtil.createDocument(document, principal.getId()); + DocumentUtil.createDocument(document, principal.getId()); // Raise a document created event DocumentCreatedAsyncEvent documentCreatedAsyncEvent = new DocumentCreatedAsyncEvent(); @@ -911,7 +911,7 @@ public class DocumentResource extends BaseResource { // Add files to the document try { for (EmailUtil.FileContent fileContent : mailContent.getFileContentList()) { - FileUtil.createFile(fileContent.getName(), fileContent.getFile(), fileContent.getSize(), + FileUtil.createFile(fileContent.getName(), null, fileContent.getFile(), fileContent.getSize(), document.getLanguage(), principal.getId(), document.getId()); } } catch (IOException e) { diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 4f03f156..24fd97bb 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -66,6 +66,7 @@ public class FileResource extends BaseResource { * @apiName PutFile * @apiGroup File * @apiParam {String} id Document ID + * @apiParam {String} previousFileId ID of the file to replace by this new version * @apiParam {String} file File data * @apiSuccess {String} status Status OK * @apiSuccess {String} id File ID @@ -88,6 +89,7 @@ public class FileResource extends BaseResource { @Consumes("multipart/form-data") public Response add( @FormDataParam("id") String documentId, + @FormDataParam("previousFileId") String previousFileId, @FormDataParam("file") FormDataBodyPart fileBodyPart) { if (!authenticate()) { throw new ForbiddenClientException(); @@ -122,7 +124,7 @@ public class FileResource extends BaseResource { try { String name = fileBodyPart.getContentDisposition() != null ? URLDecoder.decode(fileBodyPart.getContentDisposition().getFileName(), "UTF-8") : null; - String fileId = FileUtil.createFile(name, unencryptedFile, fileSize, documentDto == null ? + String fileId = FileUtil.createFile(name, previousFileId, unencryptedFile, fileSize, documentDto == null ? null : documentDto.getLanguage(), principal.getId(), documentId); // Always return OK @@ -392,6 +394,7 @@ public class FileResource extends BaseResource { * @apiSuccess {String} files.id ID * @apiSuccess {String} files.mimetype MIME type * @apiSuccess {String} files.name File name + * @apiSuccess {String} files.version Zero-based version number * @apiSuccess {String} files.processing True if the file is currently processing * @apiSuccess {String} files.document_id Document ID * @apiSuccess {String} files.create_date Create date (timestamp) @@ -433,6 +436,7 @@ public class FileResource extends BaseResource { .add("id", fileDb.getId()) .add("processing", FileUtil.isProcessingFile(fileDb.getId())) .add("name", JsonUtil.nullable(fileDb.getName())) + .add("version", fileDb.getVersion()) .add("mimetype", fileDb.getMimeType()) .add("document_id", JsonUtil.nullable(fileDb.getDocumentId())) .add("create_date", fileDb.getCreateDate().getTime()) diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index e55e3dbc..7ab7347c 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=21 \ No newline at end of file +db.version=22 \ No newline at end of file diff --git a/docs-web/src/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties index e55e3dbc..7ab7347c 100644 --- a/docs-web/src/stress/resources/config.properties +++ b/docs-web/src/stress/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=21 \ No newline at end of file +db.version=22 \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index 053b72e6..278f1bb6 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -140,9 +140,11 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals(2, files.size()); Assert.assertEquals(file1Id, files.getJsonObject(0).getString("id")); Assert.assertEquals("PIA00452.jpg", files.getJsonObject(0).getString("name")); + Assert.assertEquals(0, files.getJsonObject(0).getInt("version")); Assert.assertEquals(163510L, files.getJsonObject(0).getJsonNumber("size").longValue()); Assert.assertEquals(file2Id, files.getJsonObject(1).getString("id")); Assert.assertEquals("PIA00452.jpg", files.getJsonObject(1).getString("name")); + Assert.assertEquals(0, files.getJsonObject(1).getInt("version")); // Rename a file target().path("file/" + file1Id) @@ -225,6 +227,38 @@ public class TestFileResource extends BaseJerseyTest { target().path("/file/" + file2Id + "/process").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) .post(Entity.form(new Form()), JsonObject.class); + + // Add a new version to a file + String file3Id; + try (InputStream is0 = Resources.getResource("file/document.txt").openStream()) { + StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is0, "document.txt"); + try (FormDataMultiPart multiPart = new FormDataMultiPart()) { + json = target() + .register(MultiPartFeature.class) + .path("/file").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) + .put(Entity.entity( + multiPart + .field("id", document1Id) + .field("previousFileId", file2Id) + .bodyPart(streamDataBodyPart), + MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); + file3Id = json.getString("id"); + Assert.assertNotNull(file2Id); + } + } + + // Check the newly created version + json = target().path("/file/list") + .queryParam("id", document1Id) + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) + .get(JsonObject.class); + files = json.getJsonArray("files"); + Assert.assertEquals(1, files.size()); + Assert.assertEquals(file3Id, files.getJsonObject(0).getString("id")); + Assert.assertEquals("document.txt", files.getJsonObject(0).getString("name")); + Assert.assertEquals(1, files.getJsonObject(0).getInt("version")); } /** From 33aaf8afd28c73ca4f72dd57da301ac296d3e3f1 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 28 Nov 2018 13:09:20 +0100 Subject: [PATCH 012/129] css fix --- .../java/com/sismics/docs/rest/resource/FileResource.java | 2 +- docs-web/src/main/webapp/src/style/main.less | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 24fd97bb..02195329 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -290,7 +290,7 @@ public class FileResource extends BaseResource { DocumentDao documentDao = new DocumentDao(); FileDao fileDao = new FileDao(); File file = fileDao.getFile(id); - if (file == null) { + if (file == null || file.getDocumentId() == null) { throw new NotFoundException(); } DocumentDto documentDto = documentDao.getDocument(file.getDocumentId(), PermType.WRITE, getTargetIdList(null)); diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 4998f80a..a9c13ce5 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -653,6 +653,10 @@ input[readonly].share-link { } } +.navbar-brand { + font-weight: 500; +} + .nav > li { font-weight: 500; } From db4f5f9011c5222f665b4caecccb99474f2a5d58 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 5 Dec 2018 17:23:19 +0100 Subject: [PATCH 013/129] fix workflow form --- .../settings/SettingsWorkflowEdit.js | 1 + .../partial/docs/settings.workflow.edit.html | 22 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js index a4a4dfa1..444b1e12 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js @@ -1,3 +1,4 @@ + 'use strict'; /** diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html index 1cc03eca..e3efd853 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html @@ -136,20 +136,10 @@
    -
    -
    -

    - -
    -
    -
    + +
    +
    +

    + +
    +
    \ No newline at end of file From 98fa89bd809044eb13da85a198fe9d62041cef5c Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 28 Nov 2018 13:09:20 +0100 Subject: [PATCH 014/129] css fix (cherry picked from commit 33aaf8afd28c73ca4f72dd57da301ac296d3e3f1) --- .../java/com/sismics/docs/rest/resource/FileResource.java | 2 +- docs-web/src/main/webapp/src/style/main.less | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 4f03f156..ea6dbfcb 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -288,7 +288,7 @@ public class FileResource extends BaseResource { DocumentDao documentDao = new DocumentDao(); FileDao fileDao = new FileDao(); File file = fileDao.getFile(id); - if (file == null) { + if (file == null || file.getDocumentId() == null) { throw new NotFoundException(); } DocumentDto documentDto = documentDao.getDocument(file.getDocumentId(), PermType.WRITE, getTargetIdList(null)); diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 4998f80a..a9c13ce5 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -653,6 +653,10 @@ input[readonly].share-link { } } +.navbar-brand { + font-weight: 500; +} + .nav > li { font-weight: 500; } From 5169deb005b1324af9e44d729a920d37bf993ef6 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 5 Dec 2018 17:23:19 +0100 Subject: [PATCH 015/129] fix workflow form (cherry picked from commit db4f5f9011c5222f665b4caecccb99474f2a5d58) --- .../settings/SettingsWorkflowEdit.js | 1 + .../partial/docs/settings.workflow.edit.html | 22 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js index a4a4dfa1..444b1e12 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsWorkflowEdit.js @@ -1,3 +1,4 @@ + 'use strict'; /** diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html index 1cc03eca..e3efd853 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.workflow.edit.html @@ -136,20 +136,10 @@
-
-
-

- -
-
-
+ +
+
+

+ +
+
\ No newline at end of file From 5fbbcfc888418aba6d83c06832fd085b6cfc8ce3 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Thu, 24 Jan 2019 11:38:17 +0100 Subject: [PATCH 016/129] Open jdk11 (#287) Java 11 compat --- docs-web/pom.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs-web/pom.xml b/docs-web/pom.xml index b5eb7f28..32aa4aa7 100644 --- a/docs-web/pom.xml +++ b/docs-web/pom.xml @@ -26,6 +26,25 @@ docs-web-common + + + javax.xml.bind + jaxb-api + 2.3.0 + + + + com.sun.xml.bind + jaxb-core + 2.3.0 + + + + com.sun.xml.bind + jaxb-impl + 2.3.0 + + org.glassfish.jersey.containers From 183e86aad6602e2e85acae429b6f2d4b11562d22 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 24 Jan 2019 11:48:42 +0100 Subject: [PATCH 017/129] fix travis build for PR --- .travis.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 30fe4095..b94a31a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,14 +7,17 @@ before_install: - sudo apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra - sudo apt-get -y -q install haveged && sudo service haveged start after_success: - - mvn -Pprod -DskipTests clean install - - docker login -u $DOCKER_USER -p $DOCKER_PASS - - export REPO=sismics/docs - - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` - - docker build -f Dockerfile -t $REPO:$COMMIT . - - docker tag $REPO:$COMMIT $REPO:$TAG - - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER - - docker push $REPO + - | + if [ "$TRAVIS_PULL_REQUEST" = "false" ]; + mvn -Pprod -DskipTests clean install + docker login -u $DOCKER_USER -p $DOCKER_PASS + export REPO=sismics/docs + export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` + docker build -f Dockerfile -t $REPO:$COMMIT . + docker tag $REPO:$COMMIT $REPO:$TAG + docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER + docker push $REPO + fi env: global: - secure: LRGpjWORb0qy6VuypZjTAfA8uRHlFUMTwb77cenS9PPRBxuSnctC531asS9Xg3DqC5nsRxBBprgfCKotn5S8nBSD1ceHh84NASyzLSBft3xSMbg7f/2i7MQ+pGVwLncusBU6E/drnMFwZBleo+9M8Tf96axY5zuUp90MUTpSgt0= From c7c7badaf04fcf8663c38997657b43bacd4b88e5 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 24 Jan 2019 11:49:45 +0100 Subject: [PATCH 018/129] fix travis build for PR --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b94a31a1..683ab10a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ before_install: - sudo apt-get -y -q install haveged && sudo service haveged start after_success: - | - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; + if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then mvn -Pprod -DskipTests clean install docker login -u $DOCKER_USER -p $DOCKER_PASS export REPO=sismics/docs From 8a5e90e56237c4a0e059aadd211ba339b3b60e10 Mon Sep 17 00:00:00 2001 From: Freekers <1370857+Freekers@users.noreply.github.com> Date: Thu, 24 Jan 2019 11:52:44 +0100 Subject: [PATCH 019/129] Added OCR support for Dutch (Nederlands) (#286) Dutch support for OCR --- .travis.yml | 2 +- Dockerfile | 2 +- .../main/java/com/sismics/docs/core/constant/Constants.java | 2 +- docs-web/src/main/webapp/src/app/docs/app.js | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 683ab10a..6a1c0e61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ language: java before_install: - sudo add-apt-repository -y ppa:mc3man/trusty-media - sudo apt-get -qq update - - sudo apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra + - sudo apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra tesseract-ocr-nld - sudo apt-get -y -q install haveged && sudo service haveged start after_success: - | diff --git a/Dockerfile b/Dockerfile index 74764c72..ddb5eb09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM sismics/ubuntu-jetty:9.4.12 MAINTAINER b.gamard@sismics.com -RUN apt-get update && apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra && \ +RUN apt-get update && apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra tesseract-ocr-nld && \ apt-get clean && rm -rf /var/lib/apt/lists/* # Remove the embedded javax.mail jar from Jetty diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java index 78aebf50..70037313 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java @@ -38,7 +38,7 @@ public class Constants { /** * Supported document languages. */ - public static final List SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor"); + public static final List SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor", "nld"); /** * Base URL environment variable. diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index 7b9e26da..c6e1c90a 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -507,7 +507,8 @@ angular.module('docs', { key: 'chi_tra', label: '繁体中文' }, { key: 'jpn', label: '日本語' }, { key: 'tha', label: 'ภาษาไทย' }, - { key: 'kor', label: '한국어' } + { key: 'kor', label: '한국어' }, + { key: 'nld', label: 'Nederlands' } ]; }) /** @@ -530,4 +531,4 @@ angular.module('docs', if (location.search.indexOf("protractor") > -1) { window.name = 'NG_DEFER_BOOTSTRAP!'; -} \ No newline at end of file +} From 10d5c4334b059362c4bf24b271a912632dd92b41 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Thu, 24 Jan 2019 11:53:45 +0100 Subject: [PATCH 020/129] =?UTF-8?q?Fixed=20some=20spelling=20mistakes=20+?= =?UTF-8?q?=20added=20translated=20properties=20for=20german=E2=80=A6=20(#?= =?UTF-8?q?266)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit german fixes --- .../sismics/docs/adapter/LanguageAdapter.java | 1 + .../app/src/main/res/drawable-xhdpi/deu.png | Bin 0 -> 9365 bytes .../app/src/main/res/drawable-xxhdpi/deu.png | Bin 0 -> 20921 bytes .../app/src/main/res/values-de/strings.xml | 148 ++++++++++++++++++ .../app/src/main/res/values/strings.xml | 1 + .../src/main/resources/messages.properties.de | 10 ++ .../src/main/resources/messages_de.properties | 10 ++ docs-importer/SismicsDocs.ico | Bin 0 -> 85182 bytes docs-web/src/main/webapp/src/locale/de.json | 135 +++++++++------- 9 files changed, 246 insertions(+), 59 deletions(-) create mode 100644 docs-android/app/src/main/res/drawable-xhdpi/deu.png create mode 100644 docs-android/app/src/main/res/drawable-xxhdpi/deu.png create mode 100644 docs-android/app/src/main/res/values-de/strings.xml create mode 100644 docs-core/src/main/resources/messages.properties.de create mode 100644 docs-core/src/main/resources/messages_de.properties create mode 100644 docs-importer/SismicsDocs.ico diff --git a/docs-android/app/src/main/java/com/sismics/docs/adapter/LanguageAdapter.java b/docs-android/app/src/main/java/com/sismics/docs/adapter/LanguageAdapter.java index a5f24ce3..80797651 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/adapter/LanguageAdapter.java +++ b/docs-android/app/src/main/java/com/sismics/docs/adapter/LanguageAdapter.java @@ -33,6 +33,7 @@ public class LanguageAdapter extends BaseAdapter { } languageList.add(new Language("fra", R.string.language_french, R.drawable.fra)); languageList.add(new Language("eng", R.string.language_english, R.drawable.eng)); + languageList.add(new Language("deu", R.string.language_german, R.drawable.deu)); } @Override diff --git a/docs-android/app/src/main/res/drawable-xhdpi/deu.png b/docs-android/app/src/main/res/drawable-xhdpi/deu.png new file mode 100644 index 0000000000000000000000000000000000000000..f459ec25868faa327285ae7e319c063c647653de GIT binary patch literal 9365 zcmeHN&ubGw6#iy5Nllw75vout^dPh#NChc^AShLvLu(PidMFBB1P=;cyy`_f3SRUd z5DyAo`~!+!M5tIQh#-hHU>n5xqivGiS>M|<)7dp`ETP$5*nzh*vu|eS``&xAvop!P ziSaYtoxPm^x(lNt=O`ay*^!~^1zEmJS@y>0#haA5ETZGh@zX#*3L_`ZU;QM84$OQM zu1mTYCmCbx9`uAj*d9zgka(a)3&cKzQjH8C#O(}v!0Soll!odn&iNLQ`JRK8*I0JB z`1V@$1$xed?(DTBR89m`&8v&WvgBLr?ORBJFz0vwxLiO;Zz z`f!>A!UELNxN#)li;_4J@OexxWc~E^q3LduG+Z!zy+W!GHs?!VY)#++P@z>~k@U-f zHj$}PxRk~MXf%NV@&Hw;6kwG=o5)njz|~!ZX30RL3Gn*NDaw=LpRbyT=I>z zH^c7f69WHbrfqB#D-ZBzX)x)xQ2rWLtTH|e<$Z4l+eM2$-?rD3gWq^U!x{^LCXgZ6mIWn)d&RcH+;~7&)pn+4i&)J86L`HGI|4#@Ti!bU z^)CnB+tb}f6J0lsVqx6_6#ulL^iDsb2-L%*P@y@>7`34_f#L#6w(T9ML>B+jGT!lv z#sp{{Y^CmBNAJ`G#4`^NRZQ;z$+g)}gT&3uw%)y_9ADErGh3S{GSfOz% zbgbeLF$wT-MaS?UvX>^1@7ax<<3QLU5=aq&szP<{1Ae}m!7l<|CXBpew26QhMkWEa zG?U=4;&NT67;0jm^|<%)y7d{ACJ-vb76#XoxP@ng+BG2%s`+W~!~=d0OuzVg7DyfA v{{r|m?WiT*Xor?_?eB{~5%Xj_)rD-;L literal 0 HcmV?d00001 diff --git a/docs-android/app/src/main/res/drawable-xxhdpi/deu.png b/docs-android/app/src/main/res/drawable-xxhdpi/deu.png new file mode 100644 index 0000000000000000000000000000000000000000..9468588da504e3f970179e8898d2428dd4b91733 GIT binary patch literal 20921 zcmeI4J7^S96o&uFCYfw7D?SP_0Uww&iH#sAR*^_Hg~4Z$E@`w$A&6inh#(4rf?}mM zA_@{PBBG_y%1SVTvO>fMig}4dlYM&5y1TQRJLB1f1T!<|!pYpZ=gi}N^Us~V4|3r^ ze{W0k#%2I5ees?{w2iYhHPYXsQxAJ-6Fm_hI0?j_vsu?VW39ku^!0QfIsMLxAACGw z<#IWS)8s{<&dZ_Ye+eicK?w>-0SN>Iw<<^h2?PYUDo6nd1O&G#NC6211h*I zw<<^gR4@2pt-IKUW-BVi<^AbQ3du~Sb$TM_c0i)7!zmVG`Fx4!S{jguS%^ez*b%F$ zd#E<()2|P?+?k{lZVY!=mc;`o434dfY$w?!GQL5-D=tfQ;50$@lI%5Ex?maFW@-2* zMO!EFcu={Kr-iJYY@Cb-i-K$qsD2$}TxS+WV>t`XSHQ^^0J|vg4Ox)EEUw#bs_!Z? zyBa`YP|jjNtfUxObTJ^eHf3oWL%OOD5;rN#!l=6&ARp9c$#1a;rz${UhE^3IBWTH= zmyb>uX0=rHm=utMiQgRK?NLAuAlIA%a>e2l8lBZ0K$1Em3rQjH;QWkYCZOU#Ww*z20bor!0HC(ZD7tLK9I#E4$rs805ngnI*kt) zEqfrz6e>l{i)Vec!4{sp0#ZN%0l}>bQa}O$!L15XKmq~5tqQ6Y5Kk0^E%9i+S2BJ{ zuilTT`gu6Bp&K&fPmDlv4#&;4pU#KvrFT35*#T(h>tu z9j&bj8;e4nx!%RqXe9&&poKL3stj35WieTRV~E25^lBVWLdFL-@8ezpkgT9R$MF8? z7_KMNmOzxK%+4NFX4%RY3|!ARxF^K?+D9Ah=aQ3P>OzxK%+4NFX4% bRY3xv4MTT_UVPj>#$kQ?`g=z94xar3=sm?0 literal 0 HcmV?d00001 diff --git a/docs-android/app/src/main/res/values-de/strings.xml b/docs-android/app/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..08250eaf --- /dev/null +++ b/docs-android/app/src/main/res/values-de/strings.xml @@ -0,0 +1,148 @@ + + + + + Ungültige E-Mail + Zu kurz (min. %d) + Zu lang (max. %d) + Erforderlich + Nur Buchstaben und Zahlen + + + Sismics Docs + Navigationsleiste öffnen + Navigationsleiste schließen + github.com/sismics/docs, sowie die Login-Daten unten eingeben]]> + Server + Username + Password + Login + OK + Abbrechen + Login gescheitert + Benutzername oder Passwort falsch + Netzwerkfehler + Netzwerkfehler, überprüfen Sie die Internetverbindung und die Server-URL + Ungültige URL + Bitte überprüfen Sie die Server-URL und versuchen Sie es erneut + Ein Absturz ist aufgetreten, ein Bericht wurde gesendet, um dieses Problem zu beheben + Erstellungsdatum + Aktuelle Datei herunterladen + Herunterladen + Dokumente durchsuchen + Alle Dokumente + Geteilte Dokumente + Alle Tags + Keine Tags + Fehler beim Laden von Tags + Keine Dokumente + Fehler beim Laden von Dokumenten + Keine Dateien + Fehler beim Laden von Dateien + Neues Dokument + Teilen + Schließen + Hinzufügen + Freigabename (optional) + Dieses Dokument wird derzeit nicht freigegeben + Diese Freigabe löschen + Send this share link + Fehler beim Laden von Freigaben + Fehler beim Hinzufügen der Freigabe + Freigabe Link + Fehler beim Löschen der Freigabe + Freigabe senden an + Datei hinzufügen + Datei hochladen von + Einstellungen + Ausloggen + Version + Build + Erweiterte Einstellungen + Über + GitHub + Fehler berichten + Cache leeren + Zwischengespeicherte Dateien löschen + Cache wurde geleert + Suchhistorie löschen + Leert die aktuellen Suchvorschläge + Suchvorschläge wurden gelöscht + Cache Größe + Français + English + Deutsch + Speichern + Bearbeiten + Netzwerkfehler, bitte versuchen Sie es erneut + Bitte warten + Daten werden gesendet + Löschen + Dokument löschen + Dieses Dokument und alle zugehörigen Dateien wirklich löschen? + Netzwerkfehler beim Löschen des Dokuments + Lösche Dokument + Datei löschen + Die aktuelle Datei wirklich löschen? + Netzwerkfehler beim Löschen der Datei + Lösche Datei + Fehler beim Lesen der Datei + Sismics Docs + Neue Datei in das Dokument hochladen + Fehler beim Hochladen der neuen Datei + Aktuelle Datei löschen + Erweiterte Suche + Suche + Tags hinzufügen + Erstellungsdatum + Beschreibung + Titel + Einfache Suche + Volltextsuche + Ersteller + Nach Datum + Vor Datum + Tags durchsuchen + Alle Sprachen + Informationen anzeigen + Wer kann darauf zugreifen? + Kommentare + Keine Kommentare + Fehler beim Laden von Kommentaren + Senden + Kommentar hinzufügen + Fehler beim Hinzufügen des Kommentars + Füge Kommentar hinzu + Kommentar löschen + Lösche Kommentar + Fehler beim Löschen des Kommentars + PDF + Download + Rand + Bild an Seite anpassen + Kommentare exportieren + Metadaten exportieren + mm + Sismics Docs Datei Export + Sismics Docs Dokumentenexport + Sismics Docs PDF Export + Letzte Aktivität + Aktivitäten + E-Mail + Speicherbegrenzung + %1$d/%2$d MB + Validierungscode + Geteilt + Sprache + Geltungsbereich + Typ + Quelle + Format + Verleger + Identifikator + Thema + Rechte + Mitwirkende + Beziehungen + + diff --git a/docs-android/app/src/main/res/values/strings.xml b/docs-android/app/src/main/res/values/strings.xml index 5d453293..e5ca3d48 100644 --- a/docs-android/app/src/main/res/values/strings.xml +++ b/docs-android/app/src/main/res/values/strings.xml @@ -71,6 +71,7 @@ Cache size Français English + Deutsch Save Edit Network error, please try again diff --git a/docs-core/src/main/resources/messages.properties.de b/docs-core/src/main/resources/messages.properties.de new file mode 100644 index 00000000..bfdd17d9 --- /dev/null +++ b/docs-core/src/main/resources/messages.properties.de @@ -0,0 +1,10 @@ +email.template.password_recovery.subject=Bitte setzen Sie ihr Passwort zur\u00FCck +email.template.password_recovery.hello=Hallo {0}. +email.template.password_recovery.instruction1=Wir haben eine Anfrage zum Zur\u00FCcksetzen Ihres Passworts erhalten.
Wenn Sie keine Hilfe angefordert haben, können Sie diese E-Mail einfach ignorieren. +email.template.password_recovery.instruction2=Um Ihr Passwort zur\u00FCckzusetzen, besuchen Sie bitte den folgenden Link: +email.template.password_recovery.click_here=Klicken Sie hier, um Ihr Passwort zur\u00FCckzusetzen +email.template.route_step_validate.subject=Ein Dokument braucht Ihre Aufmerksamkeit +email.template.route_step_validate.hello=Hallo {0}. +email.template.route_step_validate.instruction1=Ihnen wurde ein Workflow-Schritt zugewiesen, der Ihre Aufmerksamkeit erfordert. +email.template.route_step_validate.instruction2=Um das Dokument anzuzeigen und den Workflow zu \u00FCberpr\u00FCfen, besuchen Sie bitte den folgenden Link: +email.no_html.error=Ihr E-Mail-Client unterst\u00FCtzt keine HTML-Nachrichten \ No newline at end of file diff --git a/docs-core/src/main/resources/messages_de.properties b/docs-core/src/main/resources/messages_de.properties new file mode 100644 index 00000000..bfdd17d9 --- /dev/null +++ b/docs-core/src/main/resources/messages_de.properties @@ -0,0 +1,10 @@ +email.template.password_recovery.subject=Bitte setzen Sie ihr Passwort zur\u00FCck +email.template.password_recovery.hello=Hallo {0}. +email.template.password_recovery.instruction1=Wir haben eine Anfrage zum Zur\u00FCcksetzen Ihres Passworts erhalten.
Wenn Sie keine Hilfe angefordert haben, können Sie diese E-Mail einfach ignorieren. +email.template.password_recovery.instruction2=Um Ihr Passwort zur\u00FCckzusetzen, besuchen Sie bitte den folgenden Link: +email.template.password_recovery.click_here=Klicken Sie hier, um Ihr Passwort zur\u00FCckzusetzen +email.template.route_step_validate.subject=Ein Dokument braucht Ihre Aufmerksamkeit +email.template.route_step_validate.hello=Hallo {0}. +email.template.route_step_validate.instruction1=Ihnen wurde ein Workflow-Schritt zugewiesen, der Ihre Aufmerksamkeit erfordert. +email.template.route_step_validate.instruction2=Um das Dokument anzuzeigen und den Workflow zu \u00FCberpr\u00FCfen, besuchen Sie bitte den folgenden Link: +email.no_html.error=Ihr E-Mail-Client unterst\u00FCtzt keine HTML-Nachrichten \ No newline at end of file diff --git a/docs-importer/SismicsDocs.ico b/docs-importer/SismicsDocs.ico new file mode 100644 index 0000000000000000000000000000000000000000..4042f802faf9950fa2c5a9563ce316c9129071ba GIT binary patch literal 85182 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

%RP5(l!xzICVih9DKmyXVsPF$;}*pi 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 literal 0 HcmV?d00001 diff --git a/docs-web/src/main/webapp/src/locale/de.json b/docs-web/src/main/webapp/src/locale/de.json index 476d0dcc..7fdf845f 100644 --- a/docs-web/src/main/webapp/src/locale/de.json +++ b/docs-web/src/main/webapp/src/locale/de.json @@ -24,8 +24,8 @@ "passwordreset": { "message": "Bitte geben Sie ein neues Passwort ein.", "submit": "Mein Passwort ändern", - "error_title": "Fehler beim ändern des Passworts.", - "error_message": "Ihre Anfrage zur Passwort-Wiederherstellung ist abgelaufen, bitte wiederholen Sie die die Anfrage." + "error_title": "Fehler beim Ändern des Passworts.", + "error_message": "Ihre Anfrage zur Passwort-Wiederherstellung ist abgelaufen, bitte wiederholen Sie die Anfrage." }, "index": { "toggle_navigation": "Navigation ein-/ausblenden", @@ -36,11 +36,13 @@ "logged_as": "Eingeloggt als {{ username }}", "nav_settings": "Einstellungen", "logout": "Logout", - "global_quota_warning": "Warnung! Der frei zur Verfügung stehende maximale Speicherplatz ist fast erreicht bei {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) verwendet {{ total | number: 0 }}MB" + "global_quota_warning": "Warnung! Der frei zur Verfügung stehende, maximale Speicherplatz ist fast erreicht bei {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) verwendet {{ total | number: 0 }}MB" }, "document": { "navigation_up": "Eine Stufe höher", "toggle_navigation": "Navigation ein-/ausblenden", + "display_mode_list": "Dokumente in Liste anzeigen", + "display_mode_grid": "Dokumente im Raster anzeigen", "search_simple": "Einfache Suche", "search_fulltext": "Volltext Suche", "search_creator": "Urheber", @@ -76,8 +78,8 @@ "type": "Typ", "coverage": "Geltungsbereich", "rights": "Rechte", - "relations": "Beziehung", - "page_size": "Seiten Größe", + "relations": "Beziehungen", + "page_size": "Seitengröße", "page_size_10": "10 pro Seite", "page_size_20": "20 pro Seite", "page_size_30": "30 pro Seite", @@ -94,12 +96,12 @@ "shared_document_message": "Sie können dieses Dokument mit diesem Link freigeben. Beachten Sie, dass jeder, der diesen Link hat, das Dokument sehen kann.
", "not_found": "Dokument nicht gefunden", "forbidden": "Zugriff verweigert", - "download_files": "Download Datei", + "download_files": "Datei herunterladen", "export_pdf": "in PDF exportieren", "by_creator": "von", "comments": "Kommentar", "no_comments": "Noch keine Kommentare zu diesem Dokument vorhanden", - "add_comment": "Fügen sie einen Komentar hinzu", + "add_comment": "Fügen sie einen Kommentar hinzu", "error_loading_comments": "Fehler beim Laden eines Kommentars", "workflow_current": "Aktueller Workflow-Status", "workflow_comment": "Fügen Sie einen Workflow Kommentar hinzu", @@ -111,27 +113,28 @@ "delete_file_message": "Wollen Sie diese Datei wirklich löschen?", "upload_pending": "Ausstehend...", "upload_progress": "Hochladen...", - "upload_error": "Fehler beim hochladen", + "upload_error": "Fehler beim Hochladen", "upload_error_quota": "Maximaler Speicherplatz erreicht", - "drop_zone": "Drag & Drop Dateien hier her ziehen, um diese hochzuladen", + "drop_zone": "Drag & Drop Dateien hierherziehen, um diese hochzuladen", "add_files": "Dateien hinzufügen", - "file_processing_indicator": "Diese Datei wird gerade bearbeitet. Die Suche wird nicht verfügbar sein, bevor sie abgeschlossen ist.", - "reprocess_file": "Diese Datei erneut bearbeiten" + "file_processing_indicator": "Diese Datei wird gerade bearbeitet. Die Suche wird nicht verfügbar sein, bevor der Vorgang abgeschlossen ist.", + "reprocess_file": "Diese Datei erneut verarbeiten" }, "workflow": { "workflow": "Workflow", - "message": "Verifizieren oder validieren Sie Ihre Dokumente mit Mitarbeitern Ihres Unternehmens mithilfe von Workflows.", + "message": "Verifizieren oder validieren Sie Ihre Dokumente mit Mitarbeitern Ihres Unternehmens mit Hilfe von Workflows.", "workflow_start_label": "Welcher Workflow soll gestartet werden?", "add_more_workflow": "Fügen Sie weitere Workflows hinzu", "start_workflow_submit": "Starten Sie den Workflow", "full_name": "{{ name }} gestartet am {{ create_date | date }}", "cancel_workflow": "Abbrechen des aktuellen Workflows", "cancel_workflow_title": "Abbrechen des Workflows", - "cancel_workflow_message": "Wollen Sie den laufenden Workflow wirklich abbrechen?" + "cancel_workflow_message": "Wollen Sie den laufenden Workflow wirklich abbrechen?", + "no_workflow": "Sie können keinen Workflow für dieses Dokument starten." }, "permissions": { "permissions": "Berechtigungen", - "message": "Die Berechtigungen können direkt auf dieses Dokument angewendet werden, oder sind erhältlich bei tags.", + "message": "Die Berechtigungen können direkt auf dieses Dokument angewendet werden, oder können von tags vorgegeben werden.", "title": "Berechtigungen auf diesem Dokument", "inherited_tags": "Von Tags geerbte Berechtigungen", "acl_source": "Von", @@ -150,7 +153,7 @@ "primary_metadata": "Primäre Metadaten", "title_placeholder": "Titel des Dokuments", "description_placeholder": "Zusammenfassung, Inhaltsverzeichnis oder Freitext", - "new_files": "New files", + "new_files": "neue Dateien", "orphan_files": "+ {{ count }} Datei{{ count > 1 ? 's' : '' }}", "additional_metadata": "Weitere Metadaten", "subject_placeholder": "Schlüsselwörter, abstrakte Sätze oder Klassifizierungscodes", @@ -163,16 +166,16 @@ "default": { "upload_pending": "Ausstehend...", "upload_progress": "Lädt hoch...", - "upload_error": "Fehler beim hochladen", + "upload_error": "Fehler beim Hochladen", "upload_error_quota": "Maximaler Speicherplatz erreicht", - "quick_upload": "Schnelles hochladen", - "drop_zone": "Drag & Drop Dateien hier her ziehen, um diese hochzuladen", + "quick_upload": "Schnelles Hochladen", + "drop_zone": "Drag & Drop Dateien hierherziehen, um diese hochzuladen", "add_files": "Dateien hinzufügen", "add_new_document": "Neues Dokument hinzufügen", "latest_activity": "Letzte Aktivitäten", - "footer_sismics": "Programmiert mit by Sismics", + "footer_sismics": "Programmiert mit von Sismics", "api_documentation": "API Dokumentation", - "feedback": "Geben Sie uns Feedback", + "feedback": "Geben Sie uns Ihr Feedback", "workflow_document_list": "Mir zugeordnete Dokumente", "select_all": "Alle auswählen", "select_none": "Nichts auswählen" @@ -209,7 +212,7 @@ "title": "Tags", "message_1": "Tags sind Kategorien, die den Dokumenten zugeordnet sind.", "message_2": "Ein Dokument kann mit mehreren Tags versehen werden und ein Tag kann auf mehrere Dokumente angewendet werden.", - "message_3": "Verwendung der Schaltfläche können Sie die Berechtigungen für ein Tag bearbeiten.", + "message_3": "Unter Verwendung der Schaltfläche können Sie die Berechtigungen für ein Tag bearbeiten.", "message_4": "Wenn ein Tag von einem anderen Benutzer oder einer anderen Gruppe gelesen werden kann, können die zugehörigen Dokumente auch von diesen Personen gelesen werden.", "message_5": "Kennzeichnen Sie z.B. Ihre Firmendokumente mit einem Tag MyCompany und fügen Sie die Berechtigung Can read zu einer Gruppe hinzu employees" }, @@ -219,7 +222,9 @@ "name": "Name", "color": "Farbe", "parent": "Übergeordnet", - "info": "Berechtigungen für dieses Tag werden auch auf Dokumente angewendet, die mit einem Tag versehen sind {{ name }}" + "info": "Berechtigungen für dieses Tag werden auch auf Dokumente angewendet, die mit einem Tag versehen sind {{ name }}", + "circular_reference_title": "Zirkuläre Referenz", + "circular_reference_message": "Die Hierarchie der übergeordneten Tags bildet eine Schleife. Bitte wählen Sie ein anderes übergeordnetes Tag." } }, "group": { @@ -234,7 +239,7 @@ "profile": { "groups": "Gruppen", "quota_used": "Benutzter Speicherplatz", - "percent_used": "{{ percent | number: 0 }}% Benutzt", + "percent_used": "{{ percent | number: 0 }}% genutzt", "related_links": "Weiterführende Links", "document_created": "Dokumente erstellt von {{ username }}", "edit_user": "Benutzer {{ username }} bearbeiten" @@ -250,18 +255,18 @@ } }, "settings": { - "menu_personal_settings": "Peröhnliche Einstellungen", + "menu_personal_settings": "Persönliche Einstellungen", "menu_user_account": "Benutzerkonto", "menu_two_factor_auth": "Zwei-Faktor-Authentifizierung", - "menu_opened_sessions": "Geöffnete Sitzung", + "menu_opened_sessions": "Geöffnete Sitzungen", "menu_file_importer": "Massen Datei Importer", "menu_general_settings": "Generelle Einstellungen", - "menu_workflow": "Workflow", - "menu_users": "Benutzer", - "menu_groups": "Gruppen", - "menu_vocabularies": "Vokabulare", + "menu_workflow": "Workflows", + "menu_users": "Benutzerverwaltung", + "menu_groups": "Gruppenverwaltung", + "menu_vocabularies": "Vokabulareinträge", "menu_configuration": "Einstellungen", - "menu_inbox": "Inbox scannen", + "menu_inbox": "Posteingang durchsuchen", "menu_monitoring": "Überwachung", "user": { "title": "Benutzerverwaltung", @@ -290,7 +295,7 @@ } }, "workflow": { - "title": "Workflow Konfigurator", + "title": "Workflows", "add_workflow": "Workflow hinzufügen", "name": "Name", "create_date": "Erstellungsdatum", @@ -306,22 +311,23 @@ "type_approve": "Genehmigen", "type_validate": "Bestätigen", "target": "Zugewiesen an", - "target_help": "Zulassen: Überprüfen und fortsetzen des Workflows
Genemigen: Übernehmen oder lehnen Sie die Überprüfung ab", + "target_help": "Zulassen: Überprüfen und fortsetzen des Workflows
Genehmigen: Übernehmen oder lehnen Sie die Überprüfung ab", "add_step": "Workflow Schritt hinzufügen", "actions": "Was passiert danach?", - "remove_action": "Aktion entfernen" + "remove_action": "Aktion entfernen", + "acl_info": "Nur hier definierte Benutzer und Gruppen können diesen Workflow für ein Dokument starten" } }, "security": { "enable_totp": "Zwei-Faktor-Authentifizierung aktivieren", - "enable_totp_message": "Stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Handy haben, die bereit ist, ein neues Konto hinzuzufügen.", + "enable_totp_message": "Stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Telefon haben, die bereit ist, ein neues Konto hinzuzufügen.", "title": "Zwei-Faktor-Authentifizierung", - "message_1": "Die Zwei-Faktor-Authentifizierung ermöglicht Ihnen eine weitere Abischerung Ihres {{ appName }} Benutzerkontos. Bevor Sie diese Funktion aktivieren, stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Telefon haben:", + "message_1": "Die Zwei-Faktor-Authentifizierung ermöglicht Ihnen eine weitere Absicherung Ihres {{ appName }} Benutzerkontos. Bevor Sie diese Funktion aktivieren, stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Telefon haben:", "message_google_authenticator": "Für Android, iOS, und Blackberry: Google Authenticator", "message_duo_mobile": "Für Android und iOS: Duo Mobile", "message_authenticator": "Für Windows Phone: Authenticator", - "message_2": "Diese Anwendungen generieren automatisch einen Validierungscod der sich nach einer gewissen Zeitspanne ändert. Sie müssen diesen Validierungscode jedes Mal eingeben, wenn Sie sich bei {{ appName }} anmelden. .", - "secret_key": "Ihr geheimer Schlüssel ist: {{ secret }}", + "message_2": "Diese Anwendungen generieren automatisch einen Validierungscode, der sich nach einer gewissen Zeitspanne ändert. Sie müssen diesen Validierungscode jedes Mal eingeben, wenn Sie sich bei {{ appName }} anmelden. .", + "secret_key": "Ihr geheimer Schlüssel lautet: {{ secret }}", "secret_key_warning": "Konfigurieren Sie Ihre TOTP-App jetzt mit diesem geheimen Schlüssel auf Ihrem Telefon. Sie können später nicht mehr darauf zugreifen.", "totp_enabled_message": "Die Zwei-Faktor-Authentifizierung ist in Ihrem Konto aktiviert.
Bei jeder Anmeldung auf {{ appName }}, werden Sie in Ihrer konfigurierten Telefon-App nach einem Bestätigungscode gefragt.
Wenn Sie Ihr Telefon verlieren, können Sie sich nicht in Ihrem Konto anmelden, aber aktive Sitzungen ermöglichen es Ihnen, einen geheimen Schlüssel neu zu generieren.", "disable_totp": { @@ -358,7 +364,7 @@ }, "config": { "title_guest_access": "Gastzugang", - "message_guest_access": "Der Gastzugang ist ein Modus in dem jeder Zugriff hat und {{ appName }} ohne Passwort nutzen kann.
Wie ein normaler Benutzer kann der Gastbenutzer nur auf seine Dokumente zugreifen und Berechtigungen zugreifen.
", + "message_guest_access": "Der Gastzugang ist ein Modus, in dem jeder auf {{appName}} ohne Kennwort zugreifen kann.
Wie ein normaler Benutzer kann der Gastbenutzer nur auf seine Dokumente und diejenigen zugreifen, auf die er über Berechtigungen zugreifen kann.
", "enable_guest_access": "Gastzugang aktivieren", "disable_guest_access": "Gastzugang deaktivieren", "title_theme": "Aussehen anpassen", @@ -371,18 +377,24 @@ "logo": "Logo (quadratische Größe)", "background_image": "Hintergrundbild", "uploading_image": "Bild hochladen...", - "title_smtp": "SMTP Email Einstellungen für Passwort wiederherstellungfür das Zürucksetzen des Passworts", "smtp_hostname": "SMTP Server", "smtp_port": "SMTP Port", "smtp_from": "Absender E-Mail", "smtp_username": "SMTP Benutzername", "smtp_password": "SMTP Passwort", - "smtp_updated": "SMTP Konfiguration erfolgreich aktualisiert" + "smtp_updated": "SMTP Konfiguration erfolgreich aktualisiert", + "webhooks": "Webhooks", + "webhooks_explain": "Webhooks werden aufgerufen, wenn das angegebene Ereignis eintritt. Die angegebene URL wird mit einer JSON-Payload gepostet, die den Ereignisnamen und die ID der betreffenden Ressource enthält.", + "webhook_event": "Ereignisse", + "webhook_url": "URL", + "webhook_create_date": "Erstelldatum", + "webhook_add": "Webhook hinzufügen" }, "inbox": { "title": "Posteingang durchsuchen", - "message": "Wenn Sie diese Funktion aktivieren, durchsucht das System den angegebenen Posteingang jede Minute nach ungelesenen E-Mails und importieren diese automatisch.
Nach dem Import einer E-Mail wird diese als gelesen markiert.
Folgen Sie den Links zu Konfigurationseinstellungen für Gmail, Outlook.com, Yahoo.", - "enabled": "Posteingang duchrsuchen aktivieren", + "message": "Wenn Sie diese Funktion aktivieren, durchsucht das System den angegebenen Posteingang jede Minute nach ungelesenen E-Mails und importiert diese automatisch.
Nach dem Import einer E-Mail wird diese als gelesen markiert.
Folgen Sie den Links zu Konfigurationseinstellungen für Gmail, Outlook.com, Yahoo.", + "enabled": "Durchsuchen des Posteingangs aktivieren", "hostname": "IMAP Server", "port": "IMAP Port (143 oder 993)", "username": "IMAP Benutzername", @@ -391,16 +403,21 @@ "test": "Konfiguration testen", "last_sync": "Letzte Synchronisation: {{ data.date | date: 'medium' }}, {{ data.count }} E-Mail(s){{ data.count > 1 ? 's' : '' }} importiert", "test_success": "Die Verbindung zum Posteingang war erfolgreich ({{ count }} unread message{{ count > 1 ? 's' : '' }})", - "test_fail": "Beim Verbinden mit dem Posteingang ist ein Fehler aufgetreten, bitte überprüfen Sie die Einstellungen" + "test_fail": "Beim Verbinden mit dem Posteingang ist ein Fehler aufgetreten, bitte überprüfen Sie die Einstellungen", + "saved": "IMAP Konfiguration erfolgreich gespeichert" }, "monitoring": { "background_tasks": "Hintergrundaufgaben", - "queued_tasks": "Es gibt derzeit {{ count }}} anstehende Tasks.", + "queued_tasks": "Es gibt derzeit {{ count }} anstehende Tasks.", "queued_tasks_explain": "Dateiverarbeitung, Thumbnail-Erstellung, Index-Update, optische Zeichenerkennung sind Hintergrundaufgaben. Eine große Anzahl unbearbeiteter Aufgaben führt zu unvollständigen Suchergebnissen.", - "server_logs": "Server logs", + "server_logs": "Server Logs", "log_date": "Datum", "log_tag": "Tag", - "log_message": "Nachricht" + "log_message": "Nachricht", + "indexing": "Indexierung", + "indexing_info": "Wenn Sie Unstimmigkeiten in den Suchergebnissen feststellen, können Sie versuchen, eine vollständige Neuindizierung durchzuführen. Die Suchergebnisse sind bis zum Abschluss dieser Operation unvollständig.", + "start_reindexing": "Vollständige Neuindizierung starten", + "reindexing_started": "Neuindizierung wurde gestartet, bitte warten Sie, bis es keine Hintergrundaufgaben mehr gibt." }, "session": { "title": "Geöffnete Sitzungen", @@ -413,7 +430,7 @@ "clear": "Alle anderen Sitzungen löschen" }, "vocabulary": { - "title": "Vokabulareinträge", + "title": "Vokabular", "choose_vocabulary": "Wählen Sie ein Vokabular aus, das Sie bearbeiten möchten.", "type": "Typ", "coverage": "Abdeckung", @@ -423,31 +440,31 @@ "new_entry": "Neuer Eintrag" }, "fileimporter": { - "title": "Massen Datei Importeur", + "title": "Massen Datei Importer", "advanced_users": "Für fortgeschrittene Benutzer!", "need_intro": "Wenn Sie:", "need_1": "Ganze Verzeichnisse von Dateien auf einmal importieren möchten", "need_2": "Ein Verzeichnis nach neuen Dateien durchsuchen lassen und gefunden Dateien importieren lassen möchten", "line_1": "Gehen Sie zu sismics/docs/releases und laden Sie das Datei-Importer-Tool für Ihr System herunter.", - "line_2": "Folgen Sie dem Link instructions here um das Import-Toll zu nutzen.", - "line_3": "Ihre Dateien werden in Quick upload importiert, danach können Sie die Dateien weiterbearbeiten und Dokumenten zuordnen oder Dokumente erstellen.", + "line_2": "Folgen Sie den Anweisungen, um das Import-Tool zu nutzen.", + "line_3": "Ihre Dateien werden in Modus 'Schnelles Hochladen' importiert. Danach können Sie die Dateien weiterbearbeiten und Dokumenten zuordnen oder Dokumente erstellen.", "download": "Herunterladen", "instructions": "Anweisungen" } }, "feedback": { "title": "Geben Sie uns Feedback", - "message": "Irgendwelche Vorschläge oder Fragen zu Sismics Docs? Wir hören Ihnen genre zu!", + "message": "Irgendwelche Vorschläge oder Fragen zu Sismics Docs? Wir hören Ihnen gerne zu!", "sent_title": "Feedback gesendet", "sent_message": "Vielen Dank für Ihr Feedback! Es wird uns helfen, Sismics Docs noch besser zu machen." }, "import": { "title": "Wird importiert", - "error_quota": "Speicher Limit erreicht, kontaktieren Sie Ihren Administrator, um den Ihnen zur verfügung gestellten Speicherplatz zu erhöhen.", + "error_quota": "Speicherlimit erreicht. Kontaktieren Sie Ihren Administrator, um den Ihnen zur Verfügung gestellten Speicherplatz zu erhöhen.", "error_general": "Beim Versuch, Ihre Datei zu importieren, ist ein Fehler aufgetreten. Bitte stellen Sie sicher, dass es sich um eine gültige EML-Datei handelt." }, "app_share": { - "main": "Fragen Sie nach einen Link zu einem gemeinsam genutzten Dokument, um darauf zuzugreifen.", + "main": "Fragen Sie nach einem Link zu einem gemeinsam genutzten Dokument, um darauf zuzugreifen.", "403": { "title": "Nicht erlaubt", "message": "Das Dokument, das Sie anzeigen möchten, ist nicht mehr freigegeben." @@ -458,7 +475,7 @@ "acl_target": "Für", "acl_permission": "Zugriffsberechtigung", "add_permission": "Zugriffsberechtigung hinzufügen", - "search_user_group": "Suchen einen Benutzer oder eine Gruppe" + "search_user_group": "Suchen nach einem Benutzer oder einer Gruppe" }, "auditlog": { "log_created": "erstellt", @@ -468,7 +485,7 @@ "Comment": "Kommentar", "Document": "Dokument", "File": "Datei", - "Group": "Guppe", + "Group": "Gruppe", "Tag": "Tag", "User": "Benutzer", "RouteModel": "Workflow-Muster", @@ -515,13 +532,13 @@ "too_long": "Zu lang", "email": "Muss eine gültige E-Mailadresse sein", "password_confirm": "Passwort und Passwortbestätigung müssen übereinstimmen", - "number": "Nummer erfoderlich", + "number": "Nummer erforderlich", "no_space": "Leerzeichen sind nicht erlaubt" }, "action_type": { "ADD_TAG": "Tag hinzufügen", "REMOVE_TAG": "Tag entfernen", - "PROCESS_FILES": "Dateien bearbeiten" + "PROCESS_FILES": "Dateien verarbeiten" }, "pagination": { "previous": "Vorherige", @@ -532,7 +549,7 @@ "ok": "OK", "cancel": "Abbrechen", "share": "Teilen", - "unshare": "Nicht mehr Teilen", + "unshare": "Nicht mehr teilen", "close": "Schliessen", "add": "Hinzufügen", "open": "Öffnen", @@ -543,7 +560,7 @@ "delete": "Löschen", "rename": "Umbenennen", "loading": "Lädt...", - "send": "Gesendet", + "send": "Absenden", "enabled": "Aktiviert", "disabled": "Deaktiviert" } \ No newline at end of file From 6e56a0f5689feddd02058c23ce53b83bf5589713 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 24 Jan 2019 17:26:46 +0100 Subject: [PATCH 021/129] #289: better search parsing (including wildcard and fuzzy) --- .../util/indexing/LuceneIndexingHandler.java | 47 +++++++++++-------- .../docs/rest/TestDocumentResource.java | 1 + 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java index 40f17127..948e18fb 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java @@ -25,8 +25,7 @@ import org.apache.lucene.document.Field; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.*; -import org.apache.lucene.queryparser.flexible.standard.QueryParserUtil; -import org.apache.lucene.queryparser.flexible.standard.StandardQueryParser; +import org.apache.lucene.queryparser.simple.SimpleQueryParser; import org.apache.lucene.search.*; import org.apache.lucene.search.highlight.Highlighter; import org.apache.lucene.search.highlight.QueryScorer; @@ -371,29 +370,26 @@ public class LuceneIndexingHandler implements IndexingHandler { * @throws Exception e */ private Map search(String searchQuery, String fullSearchQuery) throws Exception { - // Escape query and add quotes so QueryParser generate a PhraseQuery - String escapedSearchQuery = "\"" + QueryParserUtil.escape(searchQuery + " " + fullSearchQuery) + "\""; - String escapedFullSearchQuery = "\"" + QueryParserUtil.escape(fullSearchQuery) + "\""; + // The fulltext query searches in all fields + searchQuery = searchQuery + " " + fullSearchQuery; // Build search query Analyzer analyzer = new StandardAnalyzer(); - StandardQueryParser qpHelper = new StandardQueryParser(analyzer); - qpHelper.setPhraseSlop(100); // PhraseQuery add terms // Search on documents and files BooleanQuery query = new BooleanQuery.Builder() - .add(qpHelper.parse(escapedSearchQuery, "title"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "description"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "subject"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "identifier"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "publisher"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "format"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "source"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "type"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "coverage"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "rights"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedSearchQuery, "filename"), BooleanClause.Occur.SHOULD) - .add(qpHelper.parse(escapedFullSearchQuery, "content"), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "title").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "description").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "subject").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "identifier").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "publisher").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "format").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "source").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "type").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "coverage").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "rights").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "filename").parse(searchQuery), BooleanClause.Occur.SHOULD) + .add(buildQueryParser(analyzer, "content").parse(fullSearchQuery), BooleanClause.Occur.SHOULD) .build(); // Search @@ -435,6 +431,19 @@ public class LuceneIndexingHandler implements IndexingHandler { return documentMap; } + /** + * Build a query parser for searching. + * + * @param analyzer Analyzer + * @param field Field + * @return Query parser + */ + private SimpleQueryParser buildQueryParser(Analyzer analyzer, String field) { + SimpleQueryParser simpleQueryParser = new SimpleQueryParser(analyzer, field); + simpleQueryParser.setDefaultOperator(BooleanClause.Occur.MUST); // AND all the terms + return simpleQueryParser; + } + /** * Build Lucene document from database document. * diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index 964e7e6d..9f0c78d0 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -184,6 +184,7 @@ public class TestDocumentResource extends BaseJerseyTest { // Search documents Assert.assertEquals(1, searchDocuments("full:uranium full:einstein", document1Token)); + Assert.assertEquals(2, searchDocuments("tit*", document1Token)); Assert.assertEquals(2, searchDocuments("full:title", document1Token)); Assert.assertEquals(2, searchDocuments("title", document1Token)); Assert.assertEquals(1, searchDocuments("super description", document1Token)); From 7a285d11a530ea52831d585323cd1769d5c48451 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 24 Jan 2019 17:34:44 +0100 Subject: [PATCH 022/129] Closes #270: missing permission for android 28 --- docs-android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs-android/app/src/main/AndroidManifest.xml b/docs-android/app/src/main/AndroidManifest.xml index 66708a1d..7cb9edaf 100644 --- a/docs-android/app/src/main/AndroidManifest.xml +++ b/docs-android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + Date: Thu, 24 Jan 2019 20:03:10 +0100 Subject: [PATCH 023/129] Closes #267: spaces not allowed in group names --- .../src/main/webapp/src/partial/docs/settings.group.edit.html | 2 ++ .../test/java/com/sismics/docs/rest/TestDocumentResource.java | 1 + 2 files changed, 3 insertions(+) diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html index f93accd0..6e0938cc 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.group.edit.html @@ -9,6 +9,7 @@

@@ -16,6 +17,7 @@ {{ 'validation.required' | translate }} {{ 'validation.too_short' | translate }} {{ 'validation.too_long' | translate }} + {{ 'validation.no_space' | translate }} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index 9f0c78d0..4b426beb 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -185,6 +185,7 @@ public class TestDocumentResource extends BaseJerseyTest { // Search documents Assert.assertEquals(1, searchDocuments("full:uranium full:einstein", document1Token)); Assert.assertEquals(2, searchDocuments("tit*", document1Token)); + Assert.assertEquals(2, searchDocuments("docu*", document1Token)); Assert.assertEquals(2, searchDocuments("full:title", document1Token)); Assert.assertEquals(2, searchDocuments("title", document1Token)); Assert.assertEquals(1, searchDocuments("super description", document1Token)); From fe40a0a677d7d902cc8ac08b7ad0fe01eccc708a Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Thu, 24 Jan 2019 20:20:03 +0100 Subject: [PATCH 024/129] Closes #168: UI for disabling TOTP as admin --- .../docs/rest/resource/UserResource.java | 2 +- .../controller/settings/SettingsUserEdit.js | 20 +++++++++++++++++++ docs-web/src/main/webapp/src/locale/en.json | 5 ++++- .../src/partial/docs/settings.user.edit.html | 10 +++++++++- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 94097186..6a2ed09a 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -576,7 +576,7 @@ public class UserResource extends BaseResource { @POST @Path("{username: [a-zA-Z0-9_]+}/disable_totp") public Response disableTotpUsername(@PathParam("username") String username) { - if (!authenticate() || principal.isGuest()) { + if (!authenticate()) { throw new ForbiddenClientException(); } checkBaseFunction(BaseFunction.ADMIN); diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js index 1b7b146b..87cf9ca3 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js @@ -77,6 +77,9 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog, }); }; + /** + * Send a password reset email. + */ $scope.passwordReset = function () { Restangular.one('user').post('password_lost', { username: $stateParams.username @@ -87,4 +90,21 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog, $dialog.messageBox(title, msg, btns); }); }; + + $scope.disableTotp = function () { + var title = $translate.instant('settings.user.edit.disable_totp_title'); + var msg = $translate.instant('settings.user.edit.disable_totp_message'); + var btns = [ + { result:'cancel', label: $translate.instant('cancel') }, + { result:'ok', label: $translate.instant('ok'), cssClass: 'btn-primary' } + ]; + + $dialog.messageBox(title, msg, btns, function (result) { + if (result === 'ok') { + Restangular.one('user/' + $stateParams.username + '/disable_totp').post('').then(function() { + $scope.user.totp_enabled = false; + }); + } + }); + }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 869760ac..135e4587 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -291,7 +291,10 @@ "disabled": "Disabled user", "password_reset_btn": "Send a password reset email to this user", "password_lost_sent_title": "Password reset email sent", - "password_lost_sent_message": "A password reset email has been sent to {{ username }}" + "password_lost_sent_message": "A password reset email has been sent to {{ username }}", + "disable_totp_btn": "Disable two-factor authentification for this user", + "disable_totp_title": "Disable two-factor authentication", + "disable_totp_message": "Are you sure you want to disable two-factor authentication for this user?" } }, "workflow": { diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html index ab40ef13..879f7887 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html @@ -113,10 +113,18 @@
-
+ +
+
+ +
+
\ No newline at end of file From 3b281a8c3f6fb90ae2089b1c152e9ab9f3c8fad2 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 30 Jan 2019 16:53:07 +0100 Subject: [PATCH 025/129] Closes #270: add notification channel for android > 26 --- docs-android/app/build.gradle | 2 +- .../docs/service/FileUploadService.java | 23 +++++++++++++++---- .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs-android/app/build.gradle b/docs-android/app/build.gradle index 25cfc4f4..a6238e78 100644 --- a/docs-android/app/build.gradle +++ b/docs-android/app/build.gradle @@ -4,7 +4,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:3.3.0' } } apply plugin: 'com.android.application' diff --git a/docs-android/app/src/main/java/com/sismics/docs/service/FileUploadService.java b/docs-android/app/src/main/java/com/sismics/docs/service/FileUploadService.java index 07fcb6cd..4a17325a 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/service/FileUploadService.java +++ b/docs-android/app/src/main/java/com/sismics/docs/service/FileUploadService.java @@ -1,10 +1,12 @@ package com.sismics.docs.service; import android.app.IntentService; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.PowerManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; @@ -29,7 +31,8 @@ import okhttp3.internal.Util; * @author bgamard */ public class FileUploadService extends IntentService { - private static final String TAG = "FileUploadService"; + private static final String TAG = "sismicsdocs:fileupload"; + private static final String CHANNEL_ID = "FileUploadService"; private static final int UPLOAD_NOTIFICATION_ID = 1; private static final int UPLOAD_NOTIFICATION_ID_DONE = 2; @@ -49,18 +52,30 @@ public class FileUploadService extends IntentService { super.onCreate(); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - notification = new NotificationCompat.Builder(this); + initChannels(); + notification = new NotificationCompat.Builder(this, CHANNEL_ID); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); } + private void initChannels() { + if (Build.VERSION.SDK_INT < 26) { + return; + } + + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, + "File Upload", NotificationManager.IMPORTANCE_HIGH); + channel.setDescription("Used to show file upload progress"); + notificationManager.createNotificationChannel(channel); + } + @Override protected void onHandleIntent(Intent intent) { if (intent == null) { return; } - wakeLock.acquire(); + wakeLock.acquire(60_000 * 30); // 30 minutes upload time maximum try { onStart(); handleFileUpload(intent.getStringExtra(PARAM_DOCUMENT_ID), (Uri) intent.getParcelableExtra(PARAM_URI)); @@ -77,7 +92,7 @@ public class FileUploadService extends IntentService { * * @param documentId Document ID * @param uri Data URI - * @throws IOException + * @throws IOException e */ private void handleFileUpload(final String documentId, final Uri uri) throws Exception { final InputStream is = getContentResolver().openInputStream(uri); diff --git a/docs-android/gradle/wrapper/gradle-wrapper.properties b/docs-android/gradle/wrapper/gradle-wrapper.properties index 84239c64..b0546596 100644 --- a/docs-android/gradle/wrapper/gradle-wrapper.properties +++ b/docs-android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 18 22:37:49 CEST 2018 +#Wed Jan 30 16:31:31 CET 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip From b2dc460b4be93a5de77f2cb18a07fd64f44c8a14 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 30 Jan 2019 17:38:30 +0100 Subject: [PATCH 026/129] Closes #269: translate audit log --- .../docs/activity/AuditLogActivity.java | 12 +++---- .../docs/adapter/AuditLogListAdapter.java | 35 +++++++++++++------ .../app/src/main/res/values-de/strings.xml | 19 +++++++--- .../app/src/main/res/values-fr/strings.xml | 15 ++++++++ .../app/src/main/res/values/strings.xml | 15 ++++++++ docs-web/src/main/webapp/src/locale/en.json | 5 +-- 6 files changed, 79 insertions(+), 22 deletions(-) diff --git a/docs-android/app/src/main/java/com/sismics/docs/activity/AuditLogActivity.java b/docs-android/app/src/main/java/com/sismics/docs/activity/AuditLogActivity.java index 391e9c81..2131eab8 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/activity/AuditLogActivity.java +++ b/docs-android/app/src/main/java/com/sismics/docs/activity/AuditLogActivity.java @@ -52,7 +52,7 @@ public class AuditLogActivity extends AppCompatActivity { } // Configure the swipe refresh layout - SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout); + SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout); swipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_bright, android.R.color.holo_green_light, android.R.color.holo_orange_light, @@ -65,7 +65,7 @@ public class AuditLogActivity extends AppCompatActivity { }); // Navigate to user profile on click - final ListView auditLogListView = (ListView) findViewById(R.id.auditLogListView); + final ListView auditLogListView = findViewById(R.id.auditLogListView); auditLogListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { @@ -88,15 +88,15 @@ public class AuditLogActivity extends AppCompatActivity { * Refresh the view. */ private void refreshView(String documentId) { - final SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout); - final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar); - final ListView auditLogListView = (ListView) findViewById(R.id.auditLogListView); + final SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout); + final ProgressBar progressBar = findViewById(R.id.progressBar); + final ListView auditLogListView = findViewById(R.id.auditLogListView); progressBar.setVisibility(View.VISIBLE); auditLogListView.setVisibility(View.GONE); AuditLogResource.list(this, documentId, new HttpCallback() { @Override public void onSuccess(JSONObject response) { - auditLogListView.setAdapter(new AuditLogListAdapter(response.optJSONArray("logs"))); + auditLogListView.setAdapter(new AuditLogListAdapter(AuditLogActivity.this, response.optJSONArray("logs"))); } @Override diff --git a/docs-android/app/src/main/java/com/sismics/docs/adapter/AuditLogListAdapter.java b/docs-android/app/src/main/java/com/sismics/docs/adapter/AuditLogListAdapter.java index 83c03f35..912f9d81 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/adapter/AuditLogListAdapter.java +++ b/docs-android/app/src/main/java/com/sismics/docs/adapter/AuditLogListAdapter.java @@ -1,8 +1,6 @@ package com.sismics.docs.adapter; import android.content.Context; -import android.content.Intent; -import android.text.TextUtils; import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; @@ -30,12 +28,19 @@ public class AuditLogListAdapter extends BaseAdapter { */ private List logList; + /** + * Context. + */ + private Context context; + /** * Audit log list adapter. * + * @param context Context * @param logs Logs */ - public AuditLogListAdapter(JSONArray logs) { + public AuditLogListAdapter(Context context, JSONArray logs) { + this.context = context; this.logList = new ArrayList<>(); for (int i = 0; i < logs.length(); i++) { @@ -67,11 +72,21 @@ public class AuditLogListAdapter extends BaseAdapter { // Build message final JSONObject log = getItem(position); - StringBuilder message = new StringBuilder(log.optString("class")); + StringBuilder message = new StringBuilder(); + + // Translate entity name + int stringId = context.getResources().getIdentifier("auditlog_" + log.optString("class"), "string", context.getPackageName()); + if (stringId == 0) { + message.append(log.optString("class")); + } else { + message.append(context.getResources().getString(stringId)); + } + message.append(" "); + switch (log.optString("type")) { - case "CREATE": message.append(" created"); break; - case "UPDATE": message.append(" updated"); break; - case "DELETE": message.append(" deleted"); break; + case "CREATE": message.append(context.getResources().getString(R.string.auditlog_created)); break; + case "UPDATE": message.append(context.getResources().getString(R.string.auditlog_updated)); break; + case "DELETE": message.append(context.getResources().getString(R.string.auditlog_deleted)); break; } switch (log.optString("class")) { case "Document": @@ -85,9 +100,9 @@ public class AuditLogListAdapter extends BaseAdapter { } // Fill the view - TextView usernameTextView = (TextView) view.findViewById(R.id.usernameTextView); - TextView messageTextView = (TextView) view.findViewById(R.id.messageTextView); - TextView dateTextView = (TextView) view.findViewById(R.id.dateTextView); + TextView usernameTextView = view.findViewById(R.id.usernameTextView); + TextView messageTextView = view.findViewById(R.id.messageTextView); + TextView dateTextView = view.findViewById(R.id.dateTextView); usernameTextView.setText(log.optString("username")); messageTextView.setText(message); String date = DateFormat.getDateFormat(parent.getContext()).format(new Date(log.optLong("create_date"))); diff --git a/docs-android/app/src/main/res/values-de/strings.xml b/docs-android/app/src/main/res/values-de/strings.xml index 08250eaf..e4828671 100644 --- a/docs-android/app/src/main/res/values-de/strings.xml +++ b/docs-android/app/src/main/res/values-de/strings.xml @@ -9,7 +9,6 @@ Nur Buchstaben und Zahlen - Sismics Docs Navigationsleiste öffnen Navigationsleiste schließen github.com/sismics/docs, sowie die Login-Daten unten eingeben]]> @@ -69,9 +68,6 @@ Leert die aktuellen Suchvorschläge Suchvorschläge wurden gelöscht Cache Größe - Français - English - Deutsch Speichern Bearbeiten Netzwerkfehler, bitte versuchen Sie es erneut @@ -145,4 +141,19 @@ Mitwirkende Beziehungen + + ACL + Kommentar + Dokument + Datei + Gruppe + Workflow + Workflow-Muster + Tag + Benutzer + Webhook + erstellt + aktualisiert + gelöscht + diff --git a/docs-android/app/src/main/res/values-fr/strings.xml b/docs-android/app/src/main/res/values-fr/strings.xml index 8933a715..cc589ba4 100644 --- a/docs-android/app/src/main/res/values-fr/strings.xml +++ b/docs-android/app/src/main/res/values-fr/strings.xml @@ -141,4 +141,19 @@ Contributeurs Relations + + ACL + Commentaire + Document + Fichier + Groupe + Workflow + Modèle de workflow + Tag + Utilisateur + Webhook + créé + mis à jour + supprimé + diff --git a/docs-android/app/src/main/res/values/strings.xml b/docs-android/app/src/main/res/values/strings.xml index e5ca3d48..92ad3257 100644 --- a/docs-android/app/src/main/res/values/strings.xml +++ b/docs-android/app/src/main/res/values/strings.xml @@ -145,4 +145,19 @@ Contributors Relations + + ACL + Comment + Document + File + Group + Workflow + Workflow model + Tag + User + Webhook + created + updated + deleted + diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 135e4587..73d300c8 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -489,10 +489,11 @@ "Document": "Document", "File": "File", "Group": "Group", + "Route": "Workflow", + "RouteModel": "Workflow model", "Tag": "Tag", "User": "User", - "RouteModel": "Workflow model", - "Route": "Workflow" + "Webhook": "Webhook" }, "selectrelation": { "typeahead": "Type a document title" From 4469bb7beef47065951d45a114a00d1e1946a51d Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Wed, 30 Jan 2019 18:15:00 +0100 Subject: [PATCH 027/129] #256: upload a new version UI --- .../document/DocumentViewContent.js | 25 ++++++++++++++++--- docs-web/src/main/webapp/src/locale/en.json | 3 ++- .../partial/docs/document.view.content.html | 8 +++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js index b90d1fc6..fa7061cf 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js @@ -64,10 +64,26 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root }); }; + $scope.uploadNewVersion = function (files, file) { + if (!$scope.document.writable || !files || files.length === 0) { + return; + } + + var uploadedfile = files[0]; + var previousFileId = file.id; + file.id = undefined; + file.progress = 0; + file.name = uploadedfile.name; + file.create_date = new Date().getTime(); + file.mimetype = uploadedfile.type; + file.version++; + $scope.uploadFile(uploadedfile, file, previousFileId); + }; + /** * File has been drag & dropped. */ - $scope.fileDropped = function(files) { + $scope.fileDropped = function (files) { if (!$scope.document.writable) { return; } @@ -75,7 +91,7 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root if (files && files.length) { // Adding files to the UI var newfiles = []; - _.each(files, function(file) { + _.each(files, function (file) { var newfile = { progress: 0, name: file.name, @@ -101,7 +117,7 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root /** * Upload a file. */ - $scope.uploadFile = function(file, newfile) { + $scope.uploadFile = function(file, newfile, previousFileId) { // Upload the file newfile.status = $translate.instant('document.view.content.upload_progress'); return Upload.upload({ @@ -109,7 +125,8 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root url: '../api/file', file: file, fields: { - id: $stateParams.id + id: $stateParams.id, + previousFileId: previousFileId } }) .progress(function(e) { diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 73d300c8..4a979096 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -118,7 +118,8 @@ "drop_zone": "Drag & drop files here to upload", "add_files": "Add files", "file_processing_indicator": "This file is being processed. Searching will not be available before it is complete.", - "reprocess_file": "Reprocess this file" + "reprocess_file": "Reprocess this file", + "upload_new_version": "Upload a new version" }, "workflow": { "workflow": "Workflow", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html index 69b6b474..e1d9a553 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html @@ -56,7 +56,7 @@
-
{{ file.name }}
+
{{ file.name }}
+
\ No newline at end of file From 0d50676586653739c1a964c43ad155f3aaa039ec Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Fri, 1 Feb 2019 11:44:05 +0100 Subject: [PATCH 030/129] Added translations strings for german lang, docs-android and docs-web (#294) --- docs-web/src/main/webapp/src/locale/de.json | 23 ++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs-web/src/main/webapp/src/locale/de.json b/docs-web/src/main/webapp/src/locale/de.json index 7fdf845f..2a4665ed 100644 --- a/docs-web/src/main/webapp/src/locale/de.json +++ b/docs-web/src/main/webapp/src/locale/de.json @@ -118,7 +118,9 @@ "drop_zone": "Drag & Drop Dateien hierherziehen, um diese hochzuladen", "add_files": "Dateien hinzufügen", "file_processing_indicator": "Diese Datei wird gerade bearbeitet. Die Suche wird nicht verfügbar sein, bevor der Vorgang abgeschlossen ist.", - "reprocess_file": "Diese Datei erneut verarbeiten" + "reprocess_file": "Diese Datei erneut verarbeiten", + "upload_new_version": "Neue Version hochladen", + "open_versions": "Versionshistorie anzeigen" }, "workflow": { "workflow": "Workflow", @@ -203,6 +205,13 @@ "edit": { "title": "Datei bearbeiten", "name": "Dateinamen" + }, + "versions": { + "title": "Versionshistorie", + "filename": "Datiename", + "mimetype": "Typ", + "create_date": "Erstellungsdatum", + "version": "Version" } }, "tag": { @@ -291,7 +300,10 @@ "disabled": "Deaktivierter Benutzer", "password_reset_btn": "Senden Sie eine E-Mail zum Zurücksetzen des Kennworts an diesen Benutzer", "password_lost_sent_title": "Passwort zurücksetzen Email gesendet", - "password_lost_sent_message": "Passwort zurücksetzen Email an {{ username }} gesendet." + "password_lost_sent_message": "Passwort zurücksetzen Email an {{ username }} gesendet.", + "disable_totp_btn": "Zwei-Faktor-Authentifizierung für diesen Benutzer deaktivieren", + "disable_totp_title": "Zwei-Faktor-Authentifizierung deaktivieren", + "disable_totp_message": "Sind Sie sicher, dass sie die Zwei-Faktor-Authentifizierung für den Benutzer deaktivieren möchten?" } }, "workflow": { @@ -486,10 +498,11 @@ "Document": "Dokument", "File": "Datei", "Group": "Gruppe", + "RouteModel": "Workflow-Muster", + "Route": "Workflow", "Tag": "Tag", "User": "Benutzer", - "RouteModel": "Workflow-Muster", - "Route": "Workflow" + "Webhook": "Webhook" }, "selectrelation": { "typeahead": "Tippen Sie einen Dokumentnamen ein" @@ -563,4 +576,4 @@ "send": "Absenden", "enabled": "Aktiviert", "disabled": "Deaktiviert" -} \ No newline at end of file +} From c6eb1c813cb0b1af1e18085d8e69be0023a49797 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Fri, 1 Feb 2019 14:47:18 +0100 Subject: [PATCH 031/129] #268: endpoint to test TOTP code --- .../docs/rest/resource/UserResource.java | 41 +++++++++++++++++++ .../sismics/docs/rest/TestUserResource.java | 11 ++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index 6a2ed09a..a851da2c 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -919,6 +919,47 @@ public class UserResource extends BaseResource { .add("secret", key.getKey()); return Response.ok().entity(response.build()).build(); } + + /** + * Test time-based one-time password. + * + * @api {post} /user/test_totp Test TOTP authentication + * @apiDescription Test a TOTP validation code. + * @apiName PostUserTestTotp + * @apiParam {String} code TOTP validation code + * @apiGroup User + * @apiSuccess {String} status Status OK + * @apiError (client) ForbiddenError The validation code is not valid or access denied + * @apiPermission user + * @apiVersion 1.6.0 + * + * @return Response + */ + @POST + @Path("test_totp") + public Response testTotp(@FormParam("code") String validationCodeStr) { + if (!authenticate() || principal.isGuest()) { + throw new ForbiddenClientException(); + } + + // Get the user + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(principal.getName()); + + // Test the validation code + if (user.getTotpKey() != null) { + int validationCode = ValidationUtil.validateInteger(validationCodeStr, "code"); + GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator(); + if (!googleAuthenticator.authorize(user.getTotpKey(), validationCode)) { + throw new ForbiddenClientException(); + } + } + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } /** * Disable time-based one-time password for the current user. diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index 74ee47dd..a727ff91 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -370,7 +370,16 @@ public class TestUserResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) .get(JsonObject.class); Assert.assertTrue(json.getBoolean("totp_enabled")); - + + // Generate a OTP + validationCode = googleAuthenticator.calculateCode(secret, new Date().getTime() / 30000); + + // Test a validation code + target().path("/user/test_totp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) + .post(Entity.form(new Form() + .param("code", Integer.toString(validationCode))), JsonObject.class); + // Disable TOTP for totp1 target().path("/user/disable_totp").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) From 822a4ae776552aa4bd4237cfebef410a4b17b041 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 4 Feb 2019 16:47:43 +0100 Subject: [PATCH 032/129] Closes #268: test TOTP after activation --- .../controller/settings/SettingsSecurity.js | 18 +++++++++++-- docs-web/src/main/webapp/src/locale/en.json | 5 +++- .../src/partial/docs/settings.security.html | 26 ++++++++++++++----- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js index c1e2a0d7..357d558e 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js @@ -11,7 +11,7 @@ angular.module('docs').controller('SettingsSecurity', function($scope, User, $di /** * Enable TOTP. */ - $scope.enableTotp = function() { + $scope.enableTotp = function () { var title = $translate.instant('settings.security.enable_totp'); var msg = $translate.instant('settings.security.enable_totp_message'); var btns = [ @@ -34,7 +34,7 @@ angular.module('docs').controller('SettingsSecurity', function($scope, User, $di /** * Disable TOTP. */ - $scope.disableTotp = function() { + $scope.disableTotp = function () { $uibModal.open({ templateUrl: 'partial/docs/settings.security.disabletotp.html', controller: 'SettingsSecurityModalDisableTotp' @@ -53,4 +53,18 @@ angular.module('docs').controller('SettingsSecurity', function($scope, User, $di }); }); }; + + /** + * Test TOTP. + */ + $scope.testValidationCodeSuccess = null; + $scope.testTotp = function (code) { + Restangular.one('user/test_totp').post('', { + code: code + }).then(function() { + $scope.testValidationCodeSuccess = true; + }, function () { + $scope.testValidationCodeSuccess = false; + }); + }; }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index 7556e882..e9df206a 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -347,7 +347,10 @@ "message": "Your account will not be protected by the two-factor authentication anymore.", "confirm_password": "Confirm your password", "submit": "Disable two-factor authentication" - } + }, + "test_totp": "Please enter the validation code displayed on your phone :", + "test_code_success": "Validation code OK", + "test_code_fail": "This code is not valid, please double check that your phone is properly configured or disable Two-factor authentication" }, "group": { "title": "Groups management", diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.security.html b/docs-web/src/main/webapp/src/partial/docs/settings.security.html index 94ab86e3..84276fea 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.security.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.security.html @@ -19,12 +19,26 @@
-
-

- -

- {{ 'settings.security.secret_key_warning' | translate }} -

+
+
+

+ +

+ {{ 'settings.security.secret_key_warning' | translate }} +

+
+
+

{{ 'settings.security.test_totp' | translate }}

+
+ + +
+

+ {{ 'settings.security.test_code_success' | translate }} + {{ 'settings.security.test_code_fail' | translate }} +

+

From aa91a7fe248930b494d753055d3410ca9b55d84e Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 4 Feb 2019 16:51:29 +0100 Subject: [PATCH 033/129] Closes #291: feedback modal only for admin users --- docs-web/src/main/webapp/src/partial/docs/document.default.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index d9ad9487..a2244902 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -114,6 +114,6 @@

- \ No newline at end of file From dedfae7b33d5770a410f90c01c28c69e17165988 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Mon, 4 Feb 2019 17:46:21 +0100 Subject: [PATCH 034/129] update french translation --- .../src/main/resources/messages.properties.de | 4 +- docs-web/src/main/webapp/src/locale/de.json | 4 +- docs-web/src/main/webapp/src/locale/fr.json | 50 +++++++++++++++---- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/docs-core/src/main/resources/messages.properties.de b/docs-core/src/main/resources/messages.properties.de index bfdd17d9..7c45de50 100644 --- a/docs-core/src/main/resources/messages.properties.de +++ b/docs-core/src/main/resources/messages.properties.de @@ -1,10 +1,10 @@ email.template.password_recovery.subject=Bitte setzen Sie ihr Passwort zur\u00FCck email.template.password_recovery.hello=Hallo {0}. -email.template.password_recovery.instruction1=Wir haben eine Anfrage zum Zur\u00FCcksetzen Ihres Passworts erhalten.
Wenn Sie keine Hilfe angefordert haben, können Sie diese E-Mail einfach ignorieren. +email.template.password_recovery.instruction1=Wir haben eine Anfrage zum Zur\u00FCcksetzen Ihres Passworts erhalten.
Wenn Sie keine Hilfe angefordert haben, k\u00F6nnen Sie diese E-Mail einfach ignorieren. email.template.password_recovery.instruction2=Um Ihr Passwort zur\u00FCckzusetzen, besuchen Sie bitte den folgenden Link: email.template.password_recovery.click_here=Klicken Sie hier, um Ihr Passwort zur\u00FCckzusetzen email.template.route_step_validate.subject=Ein Dokument braucht Ihre Aufmerksamkeit email.template.route_step_validate.hello=Hallo {0}. email.template.route_step_validate.instruction1=Ihnen wurde ein Workflow-Schritt zugewiesen, der Ihre Aufmerksamkeit erfordert. email.template.route_step_validate.instruction2=Um das Dokument anzuzeigen und den Workflow zu \u00FCberpr\u00FCfen, besuchen Sie bitte den folgenden Link: -email.no_html.error=Ihr E-Mail-Client unterst\u00FCtzt keine HTML-Nachrichten \ No newline at end of file +email.no_html.error=Ihr E-Mail-Client unterst\u00FCtzt keine HTML-Nachrichten diff --git a/docs-web/src/main/webapp/src/locale/de.json b/docs-web/src/main/webapp/src/locale/de.json index 2a4665ed..da4f5476 100644 --- a/docs-web/src/main/webapp/src/locale/de.json +++ b/docs-web/src/main/webapp/src/locale/de.json @@ -498,8 +498,8 @@ "Document": "Dokument", "File": "Datei", "Group": "Gruppe", - "RouteModel": "Workflow-Muster", "Route": "Workflow", + "RouteModel": "Workflow-Muster", "Tag": "Tag", "User": "Benutzer", "Webhook": "Webhook" @@ -576,4 +576,4 @@ "send": "Absenden", "enabled": "Aktiviert", "disabled": "Deaktiviert" -} +} \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/fr.json b/docs-web/src/main/webapp/src/locale/fr.json index 7f0f3bc5..358d1372 100644 --- a/docs-web/src/main/webapp/src/locale/fr.json +++ b/docs-web/src/main/webapp/src/locale/fr.json @@ -41,6 +41,8 @@ "document": { "navigation_up": "Monter d'un niveau", "toggle_navigation": "Activer la navigation par dossier", + "display_mode_list": "Afficher les documents en liste", + "display_mode_grid": "Afficher les documents en grille", "search_simple": "Recherche simple", "search_fulltext": "Recherche texte intégral", "search_creator": "Créateur", @@ -116,7 +118,9 @@ "drop_zone": "Glisser & déposer des fichiers ici pour les envoyer", "add_files": "Ajouter des fichiers", "file_processing_indicator": "Ce fichier est en cours de traitement. La recherche ne sera pas disponible avant la fin du traitement.", - "reprocess_file": "Retraiter ce fichier" + "reprocess_file": "Retraiter ce fichier", + "upload_new_version": "Envoyer une nouvelle version", + "open_versions": "Afficher l'historique de versions" }, "workflow": { "workflow": "Workflow", @@ -127,7 +131,8 @@ "full_name": "{{ name }} a démarré le {{ create_date | date }}", "cancel_workflow": "Annuler le workflow en cours", "cancel_workflow_title": "Annuler le workflow", - "cancel_workflow_message": "Voulez-vous vraiment annuler le workflow en cours ?" + "cancel_workflow_message": "Voulez-vous vraiment annuler le workflow en cours ?", + "no_workflow": "Vous ne pouvez pas démarrer de workflow sur ce document" }, "permissions": { "permissions": "Permissions", @@ -200,6 +205,13 @@ "edit": { "title": "Modifier le fichier", "name": "Nom de fichier" + }, + "versions": { + "title": "Historique des versions", + "filename": "Nom de fichier", + "mimetype": "Type", + "create_date": "Date de création", + "version": "Version" } }, "tag": { @@ -219,7 +231,9 @@ "name": "Nom", "color": "Couleur", "parent": "Parent", - "info": "Les permissions sur ce tag seront également appliquées aux documents taggés avec {{ name }}" + "info": "Les permissions sur ce tag seront également appliquées aux documents taggés avec {{ name }}", + "circular_reference_title": "Référence circulaire", + "circular_reference_message": "La hiérarchie des tags parents forment une boucle, veuillez choisir un autre parent." } }, "group": { @@ -286,7 +300,10 @@ "disabled": "Utilisateur désactivé", "password_reset_btn": "Envoyer un email de réinitialisation de mot de passe à cet utilisateur", "password_lost_sent_title": "Email de réinitialisation de mot de passe envoyé", - "password_lost_sent_message": "Un email de réinitalisation de mot de passe a été envoyé à {{ username }}" + "password_lost_sent_message": "Un email de réinitalisation de mot de passe a été envoyé à {{ username }}", + "disable_totp_btn": "Désactiver l'authentification en deux étapes pour cet utilisateur", + "disable_totp_title": "Désactiver l'authentification en deux étapes", + "disable_totp_message": "Etes-vous sûr de vouloir désactiver l'authentification en deux étapes pour cet utilisateur ?" } }, "workflow": { @@ -309,7 +326,8 @@ "target_help": "Approbation : Accepter ou rejeter l'étape de workflow
Validation : Examiner et poursuivre le workflow", "add_step": "Ajouter une étape de workflow", "actions": "Qu'est-ce qui se passe après?", - "remove_action": "Supprimer l'action" + "remove_action": "Supprimer l'action", + "acl_info": "Seulement les utilisateurs et les groupes définis ici pourront démarrer ce workflow sur un document" } }, "security": { @@ -377,7 +395,13 @@ "smtp_from": "Email d'envoi", "smtp_username": "Nom d'utilisateur SMTP", "smtp_password": "Mot de passe SMTP", - "smtp_updated": "Configuration SMTP mise à jour avec succès" + "smtp_updated": "Configuration SMTP mise à jour avec succès", + "webhooks": "Webhooks", + "webhooks_explain": "Les webhooks seront appelés lorsque l'évènement spécifié surgira. L'URL donné sera POST-ée avec un JSON contenant le nom de l'évènement et l'identifiant de la ressource concernée.", + "webhook_event": "Evènement", + "webhook_url": "URL", + "webhook_create_date": "Date de création", + "webhook_add": "Ajouter un webhook" }, "inbox": { "title": "Scanning de boîte de réception", @@ -391,7 +415,8 @@ "test": "Tester les paramètres", "last_sync": "Dernière synchronisation : {{ data.date | date: 'medium' }}, {{ data.count }} message{{ data.count> 1 ? 's' : '' }} importé{{ data.count> 1 ? 's' : '' }}", "test_success": "La connexion à la boîte de réception est réussie ({{ count }} message{{ count> 1 ? 's' : '' }}) non lus", - "test_fail": "Une erreur est survenue lors de la connexion à la boîte de réception, veuillez vérifier les paramètres" + "test_fail": "Une erreur est survenue lors de la connexion à la boîte de réception, veuillez vérifier les paramètres", + "saved": "Configuration IMAP sauvegardée avec succès" }, "monitoring": { "background_tasks": "Tâches d'arrière-plan", @@ -400,7 +425,11 @@ "server_logs": "Logs serveur", "log_date": "Date", "log_tag": "Tag", - "log_message": "Message" + "log_message": "Message", + "indexing": "Indexation", + "indexing_info": "Si nous notez des incohérences dans les résultats de recherche, vous pouvez essayer une réindexation complète. Les résultats de recherche seront incomplets pendant l'opération.", + "start_reindexing": "Démarrer une réindexation complète", + "reindexing_started": "Réindexation démarrée, veuillez patienter jusqu'à ce qu'il ne reste aucune tâche en arrière plan." }, "session": { "title": "Sessions ouvertes", @@ -469,10 +498,11 @@ "Document": "Document", "File": "Fichier", "Group": "Groupe", + "Route": "Workflow", + "RouteModel": "Modèle de workflow", "Tag": "Tag", "User": "Utilisateur", - "RouteModel": "Modèle de workflow", - "Route": "Workflow" + "Webhook": "Webhook" }, "selectrelation": { "typeahead": "Entrez un titre de document" From 5b818c825838e589575056c6b22e7291fc7d4df2 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 5 Feb 2019 14:14:06 +0100 Subject: [PATCH 035/129] Closes #277: display files in list --- .../document/DocumentViewContent.js | 9 ++ docs-web/src/main/webapp/src/locale/en.json | 4 +- .../partial/docs/document.view.content.html | 97 ++++++++++++++++++- docs-web/src/main/webapp/src/style/main.less | 24 +++++ 4 files changed, 132 insertions(+), 2 deletions(-) diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js index 0caa29fa..dd8efe7c 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js @@ -4,6 +4,15 @@ * Document view content controller. */ angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, Upload, $translate, $uibModal) { + $scope.displayMode = _.isUndefined(localStorage.fileDisplayMode) ? 'grid' : localStorage.fileDisplayMode; + + /** + * Watch for display mode change. + */ + $scope.$watch('displayMode', function (next) { + localStorage.fileDisplayMode = next; + }); + /** * Configuration for file sorting. */ diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index e9df206a..05be0817 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -120,7 +120,9 @@ "file_processing_indicator": "This file is being processed. Searching will not be available before it is complete.", "reprocess_file": "Reprocess this file", "upload_new_version": "Upload a new version", - "open_versions": "Show version history" + "open_versions": "Show version history", + "display_mode_list": "Display files in list", + "display_mode_grid": "Display files in grid" }, "workflow": { "workflow": "Workflow", diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html index b136dc5c..d91e5795 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html @@ -37,12 +37,32 @@ + +
+ + + + + + +
+ +
+ +
-
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
FilenameTypeSizeVersion
+
+ +
+
+ {{ file.name }} + + {{ file.mimetype }}{{ file.size | filesize }}v{{ file.version + 1 }}.0 + +
+

{{ 'document.view.content.drop_zone' | translate }} diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index a9c13ce5..1949e685 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -239,6 +239,30 @@ ul.tag-tree { } } +// File list +.table-files { + td { + vertical-align: middle !important; + } + + .thumbnail-list { + display: inline-block; + position: relative; + width: 32px; + height: 32px; + overflow: hidden; + + img { + position: absolute; + left: 50%; + top: 50%; + height: 100%; + width: auto; + transform: translate(-50%,-50%); + } + } +} + // Permissions table .table-permissions { .label-acl { From d4d1c3526449ed7bd5594f363222f02ad7dde68a Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 5 Feb 2019 15:37:46 +0100 Subject: [PATCH 036/129] Closes #260: Re-index all if the Lucene directory is unusable --- .../docs/core/model/context/AppContext.java | 4 +--- .../util/indexing/LuceneIndexingHandler.java | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java index 9cc433a3..0b191b82 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java @@ -89,9 +89,7 @@ public class AppContext { } indexingHandler.startUp(); } catch (Exception e) { - log.error("Error starting the indexing handler, rebuilding the index: " + e.getMessage()); - RebuildIndexAsyncEvent rebuildIndexAsyncEvent = new RebuildIndexAsyncEvent(); - asyncEventBus.post(rebuildIndexAsyncEvent); + log.error("Error starting the indexing handler", e); } // Start file service diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java index 948e18fb..178a9f96 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java @@ -9,6 +9,8 @@ import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.dao.ConfigDao; import com.sismics.docs.core.dao.criteria.DocumentCriteria; import com.sismics.docs.core.dao.dto.DocumentDto; +import com.sismics.docs.core.event.RebuildIndexAsyncEvent; +import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.jpa.Config; import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.File; @@ -42,7 +44,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.sql.Timestamp; import java.util.*; @@ -83,6 +87,25 @@ public class LuceneIndexingHandler implements IndexingHandler { @Override public void startUp() throws Exception { + try { + initLucene(); + } catch (Exception e) { + // An error occurred initializing Lucene, the index is out of date or broken, delete everything + log.info("Unable to initialize Lucene, cleaning up the index: " + e.getMessage()); + Path luceneDirectory = DirectoryUtil.getLuceneDirectory(); + Files.walk(luceneDirectory) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(java.io.File::delete); + + // Re-initialize and schedule a full reindex + initLucene(); + RebuildIndexAsyncEvent rebuildIndexAsyncEvent = new RebuildIndexAsyncEvent(); + AppContext.getInstance().getAsyncEventBus().post(rebuildIndexAsyncEvent); + } + } + + private void initLucene() throws Exception { ConfigDao configDao = new ConfigDao(); Config luceneStorageConfig = configDao.getById(ConfigType.LUCENE_DIRECTORY_STORAGE); String luceneStorage = luceneStorageConfig == null ? null : luceneStorageConfig.getValue(); From 940b365447ed637f7da8caf74facb2ed76cf0a65 Mon Sep 17 00:00:00 2001 From: Benjamin Gamard Date: Tue, 5 Feb 2019 17:32:47 +0100 Subject: [PATCH 037/129] Closes #236: onboarding wizard --- docs-web/src/main/webapp/src/app/docs/app.js | 10 +- .../controller/document/DocumentDefault.js | 43 ++++ docs-web/src/main/webapp/src/index.html | 6 +- .../webapp/src/lib/angular.ng-onboarding.js | 185 +++++++++++++++++ docs-web/src/main/webapp/src/locale/en.json | 22 ++ .../src/partial/docs/document.default.html | 2 +- .../webapp/src/partial/docs/document.html | 4 +- .../main/webapp/src/style/ng-onboarding.css | 196 ++++++++++++++++++ 8 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 docs-web/src/main/webapp/src/lib/angular.ng-onboarding.js create mode 100644 docs-web/src/main/webapp/src/style/ng-onboarding.css diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index c6e1c90a..f1e2544a 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -7,7 +7,7 @@ angular.module('docs', // Dependencies ['ui.router', 'ui.bootstrap', 'dialog', 'ngProgress', 'monospaced.qrcode', 'yaru22.angular-timeago', 'ui.validate', 'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'ngFileUpload', 'pascalprecht.translate', - 'tmh.dynamicLocale'] + 'tmh.dynamicLocale', 'ngOnboarding'] ) /** @@ -514,7 +514,7 @@ angular.module('docs', /** * Initialize ngProgress. */ -.run(function($rootScope, ngProgressFactory, $http) { +.run (function ($rootScope, ngProgressFactory, $http) { $rootScope.ngProgress = ngProgressFactory.createInstance(); // Watch for the number of XHR running @@ -527,6 +527,12 @@ angular.module('docs', $rootScope.ngProgress.start(); } }); +}) +/** + * Initialize ngOnboarding. + */ +.run (function ($rootScope) { + $rootScope.onboardingEnabled = false; }); if (location.search.indexOf("protractor") > -1) { diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js index f6fbf074..5e211736 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js @@ -142,4 +142,47 @@ angular.module('docs').controller('DocumentDefault', function ($scope, $rootScop }).then(function (data) { $scope.documentsWorkflow = data.documents; }); + + // Onboarding + $translate('onboarding.step1.title').then(function () { + if (localStorage.onboardingDisplayed || $(window).width() < 1000) { + return; + } + localStorage.onboardingDisplayed = true; + + $rootScope.onboardingEnabled = true; + + $rootScope.onboardingSteps = [ + { + title: $translate.instant('onboarding.step1.title'), + description: $translate.instant('onboarding.step1.description'), + position: "centered", + width: 300 + }, + { + title: $translate.instant('onboarding.step2.title'), + description: $translate.instant('onboarding.step2.description'), + attachTo: "#document-add-btn", + position: "right" + }, + { + title: $translate.instant('onboarding.step3.title'), + description: $translate.instant('onboarding.step3.description'), + attachTo: "#quick-upload-zone", + position: "left" + }, + { + title: $translate.instant('onboarding.step4.title'), + description: $translate.instant('onboarding.step4.description'), + attachTo: "#search-box", + position: "right" + }, + { + title: $translate.instant('onboarding.step5.title'), + description: $translate.instant('onboarding.step5.description'), + attachTo: "#navigation-tag", + position: "right" + } + ]; + }); }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index c17f8f0d..47b3cd19 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -13,6 +13,7 @@ + @@ -49,6 +50,7 @@ + @@ -106,6 +108,8 @@ + +