Line data Source code
1 : // Copyright (C) 2018 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.restapi.account; 16 : 17 : import static com.google.common.collect.ImmutableList.toImmutableList; 18 : 19 : import com.google.common.base.CharMatcher; 20 : import com.google.common.base.Strings; 21 : import com.google.common.collect.ImmutableList; 22 : import com.google.gerrit.common.Nullable; 23 : import com.google.gerrit.entities.Account; 24 : import com.google.gerrit.entities.HumanComment; 25 : import com.google.gerrit.entities.PatchSet; 26 : import com.google.gerrit.entities.Project; 27 : import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput; 28 : import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo; 29 : import com.google.gerrit.extensions.common.CommentInfo; 30 : import com.google.gerrit.extensions.restapi.AuthException; 31 : import com.google.gerrit.extensions.restapi.BadRequestException; 32 : import com.google.gerrit.extensions.restapi.Response; 33 : import com.google.gerrit.extensions.restapi.RestApiException; 34 : import com.google.gerrit.extensions.restapi.RestModifyView; 35 : import com.google.gerrit.index.query.Predicate; 36 : import com.google.gerrit.index.query.QueryParseException; 37 : import com.google.gerrit.server.CommentsUtil; 38 : import com.google.gerrit.server.CurrentUser; 39 : import com.google.gerrit.server.PatchSetUtil; 40 : import com.google.gerrit.server.account.AccountResource; 41 : import com.google.gerrit.server.change.ChangeJson; 42 : import com.google.gerrit.server.permissions.PermissionBackendException; 43 : import com.google.gerrit.server.query.change.ChangeData; 44 : import com.google.gerrit.server.query.change.ChangePredicates; 45 : import com.google.gerrit.server.query.change.ChangeQueryBuilder; 46 : import com.google.gerrit.server.query.change.InternalChangeQuery; 47 : import com.google.gerrit.server.restapi.change.CommentJson; 48 : import com.google.gerrit.server.restapi.change.CommentJson.HumanCommentFormatter; 49 : import com.google.gerrit.server.update.BatchUpdate; 50 : import com.google.gerrit.server.update.BatchUpdateOp; 51 : import com.google.gerrit.server.update.ChangeContext; 52 : import com.google.gerrit.server.update.UpdateException; 53 : import com.google.gerrit.server.util.time.TimeUtil; 54 : import com.google.inject.Inject; 55 : import com.google.inject.Provider; 56 : import com.google.inject.Singleton; 57 : import java.time.Instant; 58 : import java.util.ArrayList; 59 : import java.util.Collections; 60 : import java.util.LinkedHashMap; 61 : import java.util.List; 62 : import java.util.Map; 63 : import java.util.Objects; 64 : 65 : @Singleton 66 : public class DeleteDraftComments 67 : implements RestModifyView<AccountResource, DeleteDraftCommentsInput> { 68 : 69 : private final Provider<CurrentUser> userProvider; 70 : private final BatchUpdate.Factory batchUpdateFactory; 71 : private final ChangeQueryBuilder queryBuilder; 72 : private final Provider<InternalChangeQuery> queryProvider; 73 : private final ChangeData.Factory changeDataFactory; 74 : private final ChangeJson.Factory changeJsonFactory; 75 : private final Provider<CommentJson> commentJsonProvider; 76 : private final CommentsUtil commentsUtil; 77 : private final PatchSetUtil psUtil; 78 : 79 : @Inject 80 : DeleteDraftComments( 81 : Provider<CurrentUser> userProvider, 82 : BatchUpdate.Factory batchUpdateFactory, 83 : ChangeQueryBuilder queryBuilder, 84 : Provider<InternalChangeQuery> queryProvider, 85 : ChangeData.Factory changeDataFactory, 86 : ChangeJson.Factory changeJsonFactory, 87 : Provider<CommentJson> commentJsonProvider, 88 : CommentsUtil commentsUtil, 89 148 : PatchSetUtil psUtil) { 90 148 : this.userProvider = userProvider; 91 148 : this.batchUpdateFactory = batchUpdateFactory; 92 148 : this.queryBuilder = queryBuilder; 93 148 : this.queryProvider = queryProvider; 94 148 : this.changeDataFactory = changeDataFactory; 95 148 : this.changeJsonFactory = changeJsonFactory; 96 148 : this.commentJsonProvider = commentJsonProvider; 97 148 : this.commentsUtil = commentsUtil; 98 148 : this.psUtil = psUtil; 99 148 : } 100 : 101 : @Override 102 : public Response<ImmutableList<DeletedDraftCommentInfo>> apply( 103 : AccountResource rsrc, DeleteDraftCommentsInput input) 104 : throws RestApiException, UpdateException { 105 2 : CurrentUser user = userProvider.get(); 106 2 : if (!user.isIdentifiedUser()) { 107 0 : throw new AuthException("Authentication required"); 108 : } 109 2 : if (!rsrc.getUser().hasSameAccountId(user)) { 110 : // Disallow even for admins or users with Modify Account. Drafts are not like preferences or 111 : // other account info; there is no way even for admins to read or delete another user's drafts 112 : // using the normal draft endpoints under the change resource, so disallow it here as well. 113 : // (Admins may still call this endpoint with impersonation, but in that case it would pass the 114 : // hasSameAccountId check.) 115 1 : throw new AuthException("Cannot delete drafts of other user"); 116 : } 117 : 118 2 : HumanCommentFormatter humanCommentFormatter = 119 2 : commentJsonProvider.get().newHumanCommentFormatter(); 120 2 : Account.Id accountId = rsrc.getUser().getAccountId(); 121 2 : Instant now = TimeUtil.now(); 122 2 : Map<Project.NameKey, BatchUpdate> updates = new LinkedHashMap<>(); 123 2 : List<Op> ops = new ArrayList<>(); 124 : for (ChangeData cd : 125 : queryProvider 126 2 : .get() 127 : // Don't attempt to mutate any changes the user can't currently see. 128 2 : .enforceVisibility(true) 129 2 : .query(predicate(accountId, input))) { 130 2 : BatchUpdate update = 131 2 : updates.computeIfAbsent( 132 2 : cd.project(), p -> batchUpdateFactory.create(p, rsrc.getUser(), now)); 133 2 : Op op = new Op(humanCommentFormatter, accountId); 134 2 : update.addOp(cd.getId(), op); 135 2 : ops.add(op); 136 2 : } 137 : 138 : // Currently there's no way to let some updates succeed even if others fail. Even if there were, 139 : // all updates from this operation only happen in All-Users and thus are fully atomic, so 140 : // allowing partial failure would have little value. 141 2 : BatchUpdate.execute(updates.values(), ImmutableList.of(), false); 142 : 143 2 : return Response.ok( 144 2 : ops.stream().map(Op::getResult).filter(Objects::nonNull).collect(toImmutableList())); 145 : } 146 : 147 : private Predicate<ChangeData> predicate(Account.Id accountId, DeleteDraftCommentsInput input) 148 : throws BadRequestException { 149 2 : Predicate<ChangeData> hasDraft = ChangePredicates.draftBy(commentsUtil, accountId); 150 2 : if (CharMatcher.whitespace().trimFrom(Strings.nullToEmpty(input.query)).isEmpty()) { 151 2 : return hasDraft; 152 : } 153 : try { 154 1 : return Predicate.and(hasDraft, queryBuilder.parse(input.query)); 155 0 : } catch (QueryParseException e) { 156 0 : throw new BadRequestException("Invalid query: " + e.getMessage(), e); 157 : } 158 : } 159 : 160 : private class Op implements BatchUpdateOp { 161 : private final HumanCommentFormatter humanCommentFormatter; 162 : private final Account.Id accountId; 163 : private DeletedDraftCommentInfo result; 164 : 165 2 : Op(HumanCommentFormatter humanCommentFormatter, Account.Id accountId) { 166 2 : this.humanCommentFormatter = humanCommentFormatter; 167 2 : this.accountId = accountId; 168 2 : } 169 : 170 : @Override 171 : public boolean updateChange(ChangeContext ctx) throws PermissionBackendException { 172 2 : ImmutableList.Builder<CommentInfo> comments = ImmutableList.builder(); 173 2 : boolean dirty = false; 174 2 : for (HumanComment c : commentsUtil.draftByChangeAuthor(ctx.getNotes(), accountId)) { 175 2 : dirty = true; 176 2 : PatchSet.Id psId = PatchSet.id(ctx.getChange().getId(), c.key.patchSetId); 177 2 : commentsUtil.setCommentCommitId(c, ctx.getChange(), psUtil.get(ctx.getNotes(), psId)); 178 2 : commentsUtil.deleteHumanComments(ctx.getUpdate(psId), Collections.singleton(c)); 179 2 : comments.add(humanCommentFormatter.format(c)); 180 2 : } 181 2 : if (dirty) { 182 2 : result = new DeletedDraftCommentInfo(); 183 2 : result.change = 184 2 : changeJsonFactory.noOptions().format(changeDataFactory.create(ctx.getNotes())); 185 2 : result.deleted = comments.build(); 186 : } 187 2 : return dirty; 188 : } 189 : 190 : @Nullable 191 : DeletedDraftCommentInfo getResult() { 192 2 : return result; 193 : } 194 : } 195 : }