Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.67% |
175 / 193 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
MediaWikiRevisionEntitySchemaUpdater | |
90.67% |
175 / 193 |
|
50.00% |
4 / 8 |
27.59 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
newFromContext | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
truncateSchemaTextForCommentData | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
overwriteWholeSchema | |
89.66% |
26 / 29 |
|
0.00% |
0 / 1 |
5.03 | |||
updateSchemaNameBadge | |
94.44% |
51 / 54 |
|
0.00% |
0 / 1 |
6.01 | |||
getUpdateNameBadgeAutocomment | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
5 | |||
updateSchemaText | |
94.12% |
48 / 51 |
|
0.00% |
0 / 1 |
6.01 | |||
saveRevision | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace EntitySchema\DataAccess; |
6 | |
7 | use Diff\Patcher\PatcherException; |
8 | use EntitySchema\Domain\Model\EntitySchemaId; |
9 | use EntitySchema\MediaWiki\Content\EntitySchemaContent; |
10 | use EntitySchema\MediaWiki\EntitySchemaServices; |
11 | use EntitySchema\MediaWiki\HookRunner; |
12 | use EntitySchema\Services\Converter\EntitySchemaConverter; |
13 | use EntitySchema\Services\Converter\FullArrayEntitySchemaData; |
14 | use MediaWiki\CommentStore\CommentStoreComment; |
15 | use MediaWiki\Context\IContextSource; |
16 | use MediaWiki\Languages\LanguageFactory; |
17 | use MediaWiki\MediaWikiServices; |
18 | use MediaWiki\Revision\RevisionLookup; |
19 | use MediaWiki\Revision\RevisionRecord; |
20 | use MediaWiki\Revision\SlotRecord; |
21 | use MediaWiki\Storage\PageUpdater; |
22 | |
23 | /** |
24 | * @license GPL-2.0-or-later |
25 | */ |
26 | class MediaWikiRevisionEntitySchemaUpdater implements EntitySchemaUpdater { |
27 | |
28 | public const AUTOCOMMENT_UPDATED_SCHEMATEXT = 'entityschema-summary-update-schema-text'; |
29 | public const AUTOCOMMENT_UPDATED_NAMEBADGE = 'entityschema-summary-update-schema-namebadge'; |
30 | public const AUTOCOMMENT_UPDATED_LABEL = 'entityschema-summary-update-schema-label'; |
31 | public const AUTOCOMMENT_UPDATED_DESCRIPTION = 'entityschema-summary-update-schema-description'; |
32 | public const AUTOCOMMENT_UPDATED_ALIASES = 'entityschema-summary-update-schema-aliases'; |
33 | public const AUTOCOMMENT_RESTORE = 'entityschema-summary-restore'; |
34 | public const AUTOCOMMENT_UNDO = 'entityschema-summary-undo'; |
35 | |
36 | private MediaWikiPageUpdaterFactory $pageUpdaterFactory; |
37 | private WatchlistUpdater $watchListUpdater; |
38 | private IContextSource $context; |
39 | private RevisionLookup $revisionLookup; |
40 | private LanguageFactory $languageFactory; |
41 | private HookRunner $hookRunner; |
42 | |
43 | public function __construct( |
44 | MediaWikiPageUpdaterFactory $pageUpdaterFactory, |
45 | WatchlistUpdater $watchListUpdater, |
46 | IContextSource $context, |
47 | RevisionLookup $revisionLookup, |
48 | LanguageFactory $languageFactory, |
49 | HookRunner $hookRunner |
50 | ) { |
51 | $this->pageUpdaterFactory = $pageUpdaterFactory; |
52 | $this->watchListUpdater = $watchListUpdater; |
53 | $this->context = $context; |
54 | $this->revisionLookup = $revisionLookup; |
55 | $this->languageFactory = $languageFactory; |
56 | $this->hookRunner = $hookRunner; |
57 | } |
58 | |
59 | // TODO this should probably be a service in the service container |
60 | public static function newFromContext( IContextSource $context ): self { |
61 | $services = MediaWikiServices::getInstance(); |
62 | return new self( |
63 | EntitySchemaServices::getMediaWikiPageUpdaterFactory( $services ), |
64 | EntitySchemaServices::getWatchlistUpdater( $services ), |
65 | $context, |
66 | $services->getRevisionLookup(), |
67 | $services->getLanguageFactory(), |
68 | EntitySchemaServices::getHookRunner( $services ) |
69 | ); |
70 | } |
71 | |
72 | private function truncateSchemaTextForCommentData( string $schemaText ): string { |
73 | $language = $this->languageFactory->getLanguage( 'en' ); |
74 | return $language->truncateForVisual( $schemaText, 5000 ); |
75 | } |
76 | |
77 | /** |
78 | * Update a Schema with new content. This will remove existing schema content. |
79 | * |
80 | * @param EntitySchemaId $id |
81 | * @param string[] $labels |
82 | * @param string[] $descriptions |
83 | * @param string[][] $aliasGroups |
84 | * @param string $schemaText |
85 | * @param int $baseRevId |
86 | * @param CommentStoreComment $summary |
87 | */ |
88 | public function overwriteWholeSchema( |
89 | EntitySchemaId $id, |
90 | array $labels, |
91 | array $descriptions, |
92 | array $aliasGroups, |
93 | string $schemaText, |
94 | int $baseRevId, |
95 | CommentStoreComment $summary |
96 | ): EntitySchemaStatus { |
97 | $updaterStatus = $this->pageUpdaterFactory->getPageUpdater( $id->getId(), $this->context ); |
98 | if ( !$updaterStatus->isOK() ) { |
99 | return EntitySchemaStatus::wrap( $updaterStatus ); |
100 | } |
101 | $status = EntitySchemaStatus::newEdit( |
102 | $id, |
103 | $updaterStatus->getSavedTempUser(), |
104 | $updaterStatus->getContext() |
105 | ); |
106 | $updater = $updaterStatus->getPageUpdater(); |
107 | if ( $updater->grabParentRevision() === null ) { |
108 | $status->fatal( 'entityschema-error-schemaupdate-failed' ); |
109 | return $status; |
110 | } |
111 | if ( $updater->hasEditConflict( $baseRevId ) ) { |
112 | $status->fatal( 'edit-conflict' ); |
113 | return $status; |
114 | } |
115 | |
116 | $content = new EntitySchemaContent( |
117 | EntitySchemaEncoder::getPersistentRepresentation( |
118 | $id, |
119 | $labels, |
120 | $descriptions, |
121 | $aliasGroups, |
122 | $schemaText |
123 | ) |
124 | ); |
125 | $this->saveRevision( $status, $updater, $content, $summary ); |
126 | if ( !$status->isOK() ) { |
127 | return $status; |
128 | } |
129 | |
130 | $this->watchListUpdater->optionallyWatchEditedSchema( $this->context->getUser(), $id ); |
131 | |
132 | return $status; |
133 | } |
134 | |
135 | public function updateSchemaNameBadge( |
136 | EntitySchemaId $id, |
137 | string $langCode, |
138 | string $label, |
139 | string $description, |
140 | array $aliases, |
141 | int $baseRevId |
142 | ): EntitySchemaStatus { |
143 | $updaterStatus = $this->pageUpdaterFactory->getPageUpdater( $id->getId(), $this->context ); |
144 | if ( !$updaterStatus->isOK() ) { |
145 | return EntitySchemaStatus::wrap( $updaterStatus ); |
146 | } |
147 | $status = EntitySchemaStatus::newEdit( |
148 | $id, |
149 | $updaterStatus->getSavedTempUser(), |
150 | $updaterStatus->getContext() |
151 | ); |
152 | $updater = $updaterStatus->getPageUpdater(); |
153 | $parentRevision = $updater->grabParentRevision(); |
154 | if ( $parentRevision === null ) { |
155 | $status->fatal( 'entityschema-error-schemaupdate-failed' ); |
156 | return $status; |
157 | } |
158 | |
159 | $baseRevision = $this->revisionLookup->getRevisionById( $baseRevId ); |
160 | |
161 | $updateGuard = new EntitySchemaUpdateGuard(); |
162 | try { |
163 | $schemaData = $updateGuard->guardSchemaUpdate( |
164 | $baseRevision, |
165 | $parentRevision, |
166 | static function ( FullArrayEntitySchemaData $schemaData ) use ( |
167 | $langCode, |
168 | $label, |
169 | $description, |
170 | $aliases |
171 | ) { |
172 | $schemaData->data['labels'][$langCode] = $label; |
173 | $schemaData->data['descriptions'][$langCode] = $description; |
174 | $schemaData->data['aliases'][$langCode] = $aliases; |
175 | } |
176 | ); |
177 | } catch ( PatcherException $e ) { |
178 | return EntitySchemaStatus::newFatal( 'edit-conflict' ); |
179 | } |
180 | |
181 | if ( $schemaData === null ) { |
182 | return $status; |
183 | } |
184 | |
185 | $autoComment = $this->getUpdateNameBadgeAutocomment( |
186 | $baseRevision, |
187 | $langCode, |
188 | $label, |
189 | $description, |
190 | $aliases |
191 | ); |
192 | |
193 | $content = new EntitySchemaContent( |
194 | EntitySchemaEncoder::getPersistentRepresentation( |
195 | $id, |
196 | $schemaData->labels, |
197 | $schemaData->descriptions, |
198 | $schemaData->aliases, |
199 | $schemaData->schemaText |
200 | ) |
201 | ); |
202 | $this->saveRevision( $status, $updater, $content, $autoComment ); |
203 | if ( !$status->isOK() ) { |
204 | return $status; |
205 | } |
206 | |
207 | $this->watchListUpdater->optionallyWatchEditedSchema( $this->context->getUser(), $id ); |
208 | |
209 | return $status; |
210 | } |
211 | |
212 | private function getUpdateNameBadgeAutocomment( |
213 | RevisionRecord $baseRevision, |
214 | string $langCode, |
215 | string $label, |
216 | string $description, |
217 | array $aliases |
218 | ): CommentStoreComment { |
219 | |
220 | $schemaConverter = new EntitySchemaConverter(); |
221 | $schemaData = $schemaConverter->getPersistenceSchemaData( |
222 | // @phan-suppress-next-line PhanUndeclaredMethod |
223 | $baseRevision->getContent( SlotRecord::MAIN )->getText() |
224 | ); |
225 | |
226 | $label = EntitySchemaCleaner::trimWhitespaceAndControlChars( $label ); |
227 | $description = EntitySchemaCleaner::trimWhitespaceAndControlChars( $description ); |
228 | $aliases = EntitySchemaCleaner::cleanupArrayOfStrings( $aliases ); |
229 | $language = $this->languageFactory->getLanguage( $langCode ); |
230 | |
231 | $typeOfChange = []; |
232 | if ( ( $schemaData->labels[$langCode] ?? '' ) !== $label ) { |
233 | $typeOfChange[self::AUTOCOMMENT_UPDATED_LABEL] = $label; |
234 | } |
235 | if ( ( $schemaData->descriptions[$langCode] ?? '' ) !== $description ) { |
236 | $typeOfChange[self::AUTOCOMMENT_UPDATED_DESCRIPTION] = $description; |
237 | } |
238 | if ( ( $schemaData->aliases[$langCode] ?? [] ) !== $aliases ) { |
239 | $typeOfChange[self::AUTOCOMMENT_UPDATED_ALIASES] = $language->commaList( $aliases ); |
240 | } |
241 | |
242 | if ( count( $typeOfChange ) === 1 ) { // TODO what if it’s 0? |
243 | $autocommentKey = key( $typeOfChange ); |
244 | $autosummary = $typeOfChange[$autocommentKey]; |
245 | } else { |
246 | $autocommentKey = self::AUTOCOMMENT_UPDATED_NAMEBADGE; |
247 | $autosummary = ''; |
248 | } |
249 | |
250 | $autocomment = $autocommentKey . ':' . $langCode; |
251 | |
252 | return CommentStoreComment::newUnsavedComment( |
253 | '/* ' . $autocomment . ' */' . $autosummary, |
254 | [ |
255 | 'key' => $autocommentKey, |
256 | 'language' => $langCode, |
257 | 'label' => $label, |
258 | 'description' => $description, |
259 | 'aliases' => $aliases, |
260 | ] |
261 | ); |
262 | } |
263 | |
264 | /** |
265 | * @param EntitySchemaId $id |
266 | * @param string $schemaText |
267 | * @param int $baseRevId |
268 | * @param string|null $userSummary |
269 | */ |
270 | public function updateSchemaText( |
271 | EntitySchemaId $id, |
272 | string $schemaText, |
273 | int $baseRevId, |
274 | ?string $userSummary = null |
275 | ): EntitySchemaStatus { |
276 | $updaterStatus = $this->pageUpdaterFactory->getPageUpdater( $id->getId(), $this->context ); |
277 | if ( !$updaterStatus->isOK() ) { |
278 | return EntitySchemaStatus::wrap( $updaterStatus ); |
279 | } |
280 | $status = EntitySchemaStatus::newEdit( |
281 | $id, |
282 | $updaterStatus->getSavedTempUser(), |
283 | $updaterStatus->getContext() |
284 | ); |
285 | $updater = $updaterStatus->getPageUpdater(); |
286 | $parentRevision = $updater->grabParentRevision(); |
287 | if ( $parentRevision === null ) { |
288 | $status->fatal( 'entityschema-error-schemaupdate-failed' ); |
289 | return $status; |
290 | } |
291 | |
292 | $baseRevision = $this->revisionLookup->getRevisionById( $baseRevId ); |
293 | |
294 | $updateGuard = new EntitySchemaUpdateGuard(); |
295 | try { |
296 | $schemaData = $updateGuard->guardSchemaUpdate( |
297 | $baseRevision, |
298 | $parentRevision, |
299 | static function ( FullArrayEntitySchemaData $schemaData ) use ( $schemaText ) { |
300 | $schemaData->data['schemaText'] = $schemaText; |
301 | } |
302 | ); |
303 | } catch ( PatcherException $e ) { |
304 | $status->fatal( 'edit-conflict' ); |
305 | return $status; |
306 | } |
307 | |
308 | if ( $schemaData === null ) { |
309 | return $status; |
310 | } |
311 | |
312 | $commentText = '/* ' . self::AUTOCOMMENT_UPDATED_SCHEMATEXT . ' */' . $userSummary; |
313 | $summary = CommentStoreComment::newUnsavedComment( |
314 | $commentText, |
315 | [ |
316 | 'key' => self::AUTOCOMMENT_UPDATED_SCHEMATEXT, |
317 | 'userSummary' => $userSummary, |
318 | 'schemaText_truncated' => $this->truncateSchemaTextForCommentData( |
319 | // TODO use unpatched $schemaText or patched $schemaData->schemaText here? |
320 | $schemaData->schemaText |
321 | ), |
322 | ] |
323 | ); |
324 | |
325 | $persistentRepresentation = EntitySchemaEncoder::getPersistentRepresentation( |
326 | $id, |
327 | $schemaData->labels, |
328 | $schemaData->descriptions, |
329 | $schemaData->aliases, |
330 | $schemaData->schemaText |
331 | ); |
332 | |
333 | $content = new EntitySchemaContent( $persistentRepresentation ); |
334 | $this->saveRevision( $status, $updater, $content, $summary ); |
335 | if ( !$status->isOK() ) { |
336 | return $status; |
337 | } |
338 | |
339 | $this->watchListUpdater->optionallyWatchEditedSchema( $this->context->getUser(), $id ); |
340 | |
341 | return $status; |
342 | } |
343 | |
344 | private function saveRevision( |
345 | EntitySchemaStatus $status, |
346 | PageUpdater $updater, |
347 | EntitySchemaContent $content, |
348 | CommentStoreComment $summary |
349 | ): void { |
350 | $context = $status->getContext(); |
351 | if ( !$this->hookRunner->onEditFilterMergedContent( |
352 | $context, $content, $status, $summary->text, $context->getUser(), false |
353 | ) ) { |
354 | return; |
355 | } |
356 | |
357 | $updater->setContent( SlotRecord::MAIN, $content ); |
358 | $updater->saveRevision( |
359 | $summary, |
360 | EDIT_UPDATE | EDIT_INTERNAL |
361 | ); |
362 | $status->merge( $updater->getStatus() ); |
363 | } |
364 | |
365 | } |