Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.85% |
86 / 87 |
|
88.89% |
8 / 9 |
CRAP | |
0.00% |
0 / 1 |
MergeLexemesInteractor | |
98.85% |
86 / 87 |
|
88.89% |
8 / 9 |
16 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
mergeLexemes | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
checkCanMerge | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getLexeme | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
validateEntities | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getSummary | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
attemptSaveMerge | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
1 | |||
saveLexeme | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
4.00 | |||
updateWatchlistEntries | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace Wikibase\Lexeme\Interactors\MergeLexemes; |
6 | |
7 | use IContextSource; |
8 | use MediaWiki\Permissions\PermissionManager; |
9 | use WatchedItemStoreInterface; |
10 | use Wikibase\DataModel\Entity\EntityDocument; |
11 | use Wikibase\Lexeme\DataAccess\Store\MediaWikiLexemeRedirector; |
12 | use Wikibase\Lexeme\Domain\Merge\Exceptions\LexemeLoadingException; |
13 | use Wikibase\Lexeme\Domain\Merge\Exceptions\LexemeNotFoundException; |
14 | use Wikibase\Lexeme\Domain\Merge\Exceptions\LexemeSaveFailedException; |
15 | use Wikibase\Lexeme\Domain\Merge\Exceptions\MergingException; |
16 | use Wikibase\Lexeme\Domain\Merge\Exceptions\PermissionDeniedException; |
17 | use Wikibase\Lexeme\Domain\Merge\Exceptions\ReferenceSameLexemeException; |
18 | use Wikibase\Lexeme\Domain\Merge\LexemeMerger; |
19 | use Wikibase\Lexeme\Domain\Model\Lexeme; |
20 | use Wikibase\Lexeme\Domain\Model\LexemeId; |
21 | use Wikibase\Lib\FormatableSummary; |
22 | use Wikibase\Lib\Store\EntityRevisionLookup; |
23 | use Wikibase\Lib\Store\LookupConstants; |
24 | use Wikibase\Lib\Store\RevisionedUnresolvedRedirectException; |
25 | use Wikibase\Lib\Store\StorageException; |
26 | use Wikibase\Lib\Summary; |
27 | use Wikibase\Repo\Content\EntityContent; |
28 | use Wikibase\Repo\EditEntity\EditEntityStatus; |
29 | use Wikibase\Repo\EditEntity\MediaWikiEditEntityFactory; |
30 | use Wikibase\Repo\Store\EntityPermissionChecker; |
31 | use Wikibase\Repo\Store\EntityTitleStoreLookup; |
32 | use Wikibase\Repo\SummaryFormatter; |
33 | |
34 | /** |
35 | * @license GPL-2.0-or-later |
36 | */ |
37 | class MergeLexemesInteractor { |
38 | |
39 | private SummaryFormatter $summaryFormatter; |
40 | private EntityRevisionLookup $entityRevisionLookup; |
41 | private MediaWikiLexemeRedirector $lexemeRedirector; |
42 | private EntityPermissionChecker $permissionChecker; |
43 | private PermissionManager $permissionManager; |
44 | private EntityTitleStoreLookup $entityTitleLookup; |
45 | private LexemeMerger $lexemeMerger; |
46 | private WatchedItemStoreInterface $watchedItemStore; |
47 | private MediaWikiEditEntityFactory $editEntityFactory; |
48 | |
49 | public function __construct( |
50 | LexemeMerger $lexemeMerger, |
51 | SummaryFormatter $summaryFormatter, |
52 | MediaWikiLexemeRedirector $lexemeRedirector, |
53 | EntityPermissionChecker $permissionChecker, |
54 | PermissionManager $permissionManager, |
55 | EntityTitleStoreLookup $entityTitleLookup, |
56 | WatchedItemStoreInterface $watchedItemStore, |
57 | EntityRevisionLookup $entityRevisionLookup, |
58 | MediaWikiEditEntityFactory $editEntityFactory |
59 | ) { |
60 | $this->lexemeMerger = $lexemeMerger; |
61 | $this->summaryFormatter = $summaryFormatter; |
62 | $this->lexemeRedirector = $lexemeRedirector; |
63 | $this->permissionChecker = $permissionChecker; |
64 | $this->permissionManager = $permissionManager; |
65 | $this->entityTitleLookup = $entityTitleLookup; |
66 | $this->watchedItemStore = $watchedItemStore; |
67 | $this->entityRevisionLookup = $entityRevisionLookup; |
68 | $this->editEntityFactory = $editEntityFactory; |
69 | } |
70 | |
71 | /** |
72 | * @param LexemeId $sourceId |
73 | * @param LexemeId $targetId |
74 | * @param string|null $summary - only relevant when called through the API |
75 | * @param string[] $tags |
76 | * |
77 | * @return MergeLexemesStatus Note that the status is only returned |
78 | * to wrap the context and saved temp user in a strongly typed container. |
79 | * Errors are (currently) reported as exceptions, not as a failed status. |
80 | * (It would be nice to fix this at some point and use status consistently.) |
81 | * |
82 | * @throws MergingException |
83 | */ |
84 | public function mergeLexemes( |
85 | LexemeId $sourceId, |
86 | LexemeId $targetId, |
87 | IContextSource $context, |
88 | ?string $summary = null, |
89 | bool $botEditRequested = false, |
90 | array $tags = [] |
91 | ): MergeLexemesStatus { |
92 | $this->checkCanMerge( $sourceId, $context ); |
93 | $this->checkCanMerge( $targetId, $context ); |
94 | |
95 | $source = $this->getLexeme( $sourceId ); |
96 | $target = $this->getLexeme( $targetId ); |
97 | |
98 | $this->validateEntities( $source, $target ); |
99 | |
100 | $this->lexemeMerger->merge( $source, $target ); |
101 | |
102 | $mergeStatus = $this->attemptSaveMerge( $source, $target, $context, $summary, $botEditRequested, $tags ); |
103 | $context = $mergeStatus->getContext(); |
104 | $this->updateWatchlistEntries( $sourceId, $targetId ); |
105 | |
106 | $redirectStatus = $this->lexemeRedirector |
107 | ->createRedirect( $sourceId, $targetId, $botEditRequested, $tags, $context ); |
108 | $context = $redirectStatus->getContext(); |
109 | |
110 | return MergeLexemesStatus::newMerge( |
111 | $mergeStatus->getSavedTempUser() ?? $redirectStatus->getSavedTempUser(), |
112 | $context |
113 | ); |
114 | } |
115 | |
116 | private function checkCanMerge( LexemeId $lexemeId, IContextSource $context ): void { |
117 | $status = $this->permissionChecker->getPermissionStatusForEntityId( |
118 | $context->getUser(), |
119 | EntityPermissionChecker::ACTION_MERGE, |
120 | $lexemeId |
121 | ); |
122 | |
123 | if ( !$status->isOK() ) { |
124 | // would be nice to propagate the errors from $status... |
125 | throw new PermissionDeniedException(); |
126 | } |
127 | } |
128 | |
129 | /** |
130 | * @throws MergingException |
131 | */ |
132 | private function getLexeme( LexemeId $lexemeId ): Lexeme { |
133 | try { |
134 | $revision = $this->entityRevisionLookup->getEntityRevision( |
135 | $lexemeId, |
136 | 0, |
137 | LookupConstants::LATEST_FROM_MASTER |
138 | ); |
139 | |
140 | if ( $revision ) { |
141 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
142 | return $revision->getEntity(); |
143 | } else { |
144 | throw new LexemeNotFoundException( $lexemeId ); |
145 | } |
146 | } catch ( StorageException | RevisionedUnresolvedRedirectException $ex ) { |
147 | throw new LexemeLoadingException(); |
148 | } |
149 | } |
150 | |
151 | /** |
152 | * @throws ReferenceSameLexemeException |
153 | */ |
154 | private function validateEntities( EntityDocument $fromEntity, EntityDocument $toEntity ): void { |
155 | if ( $toEntity->getId()->equals( $fromEntity->getId() ) ) { |
156 | throw new ReferenceSameLexemeException(); |
157 | } |
158 | } |
159 | |
160 | /** |
161 | * @param string $direction either 'from' or 'to' |
162 | * @param LexemeId $id |
163 | * @param string|null $customSummary |
164 | * |
165 | * @return Summary |
166 | */ |
167 | private function getSummary( |
168 | string $direction, |
169 | LexemeId $id, |
170 | ?string $customSummary = null |
171 | ): Summary { |
172 | $summary = new Summary( 'wblmergelexemes', $direction, null, [ $id->getSerialization() ] ); |
173 | $summary->setUserSummary( $customSummary ); |
174 | |
175 | return $summary; |
176 | } |
177 | |
178 | private function attemptSaveMerge( |
179 | Lexeme $source, |
180 | Lexeme $target, |
181 | IContextSource $context, |
182 | ?string $summary, |
183 | bool $botEditRequested, |
184 | array $tags |
185 | ): MergeLexemesStatus { |
186 | $toResult = $this->saveLexeme( |
187 | $source, |
188 | $context, |
189 | $this->getSummary( 'to', $target->getId(), $summary ), |
190 | $botEditRequested, |
191 | $tags |
192 | ); |
193 | $context = $toResult->getContext(); |
194 | |
195 | $fromResult = $this->saveLexeme( |
196 | $target, |
197 | $context, |
198 | $this->getSummary( 'from', $source->getId(), $summary ), |
199 | $botEditRequested, |
200 | $tags |
201 | ); |
202 | $context = $fromResult->getContext(); |
203 | |
204 | return MergeLexemesStatus::newMerge( |
205 | $fromResult->getSavedTempUser() ?? $toResult->getSavedTempUser(), |
206 | $context |
207 | ); |
208 | } |
209 | |
210 | private function saveLexeme( |
211 | Lexeme $lexeme, |
212 | IContextSource $context, |
213 | FormatableSummary $summary, |
214 | bool $botEditRequested, |
215 | array $tags |
216 | ): EditEntityStatus { |
217 | // TODO: the EntityContent::EDIT_IGNORE_CONSTRAINTS flag does not seem to be used by Lexeme |
218 | // (LexemeHandler has no onSaveValidators) |
219 | $flags = EDIT_UPDATE | EntityContent::EDIT_IGNORE_CONSTRAINTS; |
220 | if ( $botEditRequested && $this->permissionManager->userHasRight( $context->getUser(), 'bot' ) ) { |
221 | $flags |= EDIT_FORCE_BOT; |
222 | } |
223 | |
224 | $formattedSummary = $this->summaryFormatter->formatSummary( $summary ); |
225 | |
226 | $editEntity = $this->editEntityFactory->newEditEntity( $context, $lexeme->getId() ); |
227 | $status = $editEntity->attemptSave( |
228 | $lexeme, |
229 | $formattedSummary, |
230 | $flags, |
231 | false, |
232 | null, |
233 | $tags |
234 | ); |
235 | if ( !$status->isOK() ) { |
236 | throw new LexemeSaveFailedException( $status->getWikiText() ); |
237 | } |
238 | |
239 | return $status; |
240 | } |
241 | |
242 | private function updateWatchlistEntries( LexemeId $fromId, LexemeId $toId ): void { |
243 | $this->watchedItemStore->duplicateAllAssociatedEntries( |
244 | $this->entityTitleLookup->getTitleForId( $fromId ), |
245 | $this->entityTitleLookup->getTitleForId( $toId ) |
246 | ); |
247 | } |
248 | |
249 | } |