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