Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.01% |
190 / 322 |
|
53.85% |
7 / 13 |
CRAP | |
0.00% |
0 / 1 |
ApiVisualEditor | |
59.01% |
190 / 322 |
|
53.85% |
7 / 13 |
396.98 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
getParsoidClient | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getUserForPermissions | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
2.50 | |||
getUserForPreview | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
pstWikitext | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
51.74% |
119 / 230 |
|
0.00% |
0 / 1 |
368.75 | |||
isAllowedNamespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAvailableNamespaceIds | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
isAllowedContentType | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedParams | |
100.00% |
35 / 35 |
|
100.00% |
1 / 1 |
1 | |||
needsToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isWriteMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Parsoid/RESTBase+MediaWiki API wrapper. |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @copyright 2011-2021 VisualEditor Team and others; see AUTHORS.txt |
8 | * @license MIT |
9 | */ |
10 | |
11 | namespace MediaWiki\Extension\VisualEditor; |
12 | |
13 | use Article; |
14 | use MediaWiki\Api\ApiBase; |
15 | use MediaWiki\Api\ApiBlockInfoTrait; |
16 | use MediaWiki\Api\ApiMain; |
17 | use MediaWiki\Api\ApiResult; |
18 | use MediaWiki\Config\Config; |
19 | use MediaWiki\Content\ContentHandler; |
20 | use MediaWiki\Content\Transform\ContentTransformer; |
21 | use MediaWiki\Content\WikitextContent; |
22 | use MediaWiki\Context\DerivativeContext; |
23 | use MediaWiki\Context\RequestContext; |
24 | use MediaWiki\EditPage\EditPage; |
25 | use MediaWiki\EditPage\IntroMessageBuilder; |
26 | use MediaWiki\EditPage\PreloadedContentBuilder; |
27 | use MediaWiki\EditPage\TextboxBuilder; |
28 | use MediaWiki\Language\RawMessage; |
29 | use MediaWiki\Logger\LoggerFactory; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Page\PageReference; |
32 | use MediaWiki\Page\WikiPageFactory; |
33 | use MediaWiki\Permissions\PermissionManager; |
34 | use MediaWiki\Registration\ExtensionRegistry; |
35 | use MediaWiki\Request\DerivativeRequest; |
36 | use MediaWiki\Revision\RevisionLookup; |
37 | use MediaWiki\SpecialPage\SpecialPageFactory; |
38 | use MediaWiki\Title\Title; |
39 | use MediaWiki\User\Options\UserOptionsLookup; |
40 | use MediaWiki\User\TempUser\TempUserCreator; |
41 | use MediaWiki\User\User; |
42 | use MediaWiki\User\UserFactory; |
43 | use MediaWiki\User\UserIdentity; |
44 | use MediaWiki\Watchlist\WatchlistManager; |
45 | use MessageLocalizer; |
46 | use Wikimedia\Assert\Assert; |
47 | use Wikimedia\ParamValidator\ParamValidator; |
48 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
49 | |
50 | class ApiVisualEditor extends ApiBase { |
51 | use ApiBlockInfoTrait; |
52 | use ApiParsoidTrait; |
53 | |
54 | private RevisionLookup $revisionLookup; |
55 | private TempUserCreator $tempUserCreator; |
56 | private UserFactory $userFactory; |
57 | private UserOptionsLookup $userOptionsLookup; |
58 | private WatchlistManager $watchlistManager; |
59 | private ContentTransformer $contentTransformer; |
60 | private WikiPageFactory $wikiPageFactory; |
61 | private IntroMessageBuilder $introMessageBuilder; |
62 | private PreloadedContentBuilder $preloadedContentBuilder; |
63 | private SpecialPageFactory $specialPageFactory; |
64 | private VisualEditorParsoidClientFactory $parsoidClientFactory; |
65 | |
66 | public function __construct( |
67 | ApiMain $main, |
68 | string $name, |
69 | RevisionLookup $revisionLookup, |
70 | TempUserCreator $tempUserCreator, |
71 | UserFactory $userFactory, |
72 | UserOptionsLookup $userOptionsLookup, |
73 | WatchlistManager $watchlistManager, |
74 | ContentTransformer $contentTransformer, |
75 | IBufferingStatsdDataFactory $statsdDataFactory, |
76 | WikiPageFactory $wikiPageFactory, |
77 | IntroMessageBuilder $introMessageBuilder, |
78 | PreloadedContentBuilder $preloadedContentBuilder, |
79 | SpecialPageFactory $specialPageFactory, |
80 | VisualEditorParsoidClientFactory $parsoidClientFactory |
81 | ) { |
82 | parent::__construct( $main, $name ); |
83 | $this->setLogger( LoggerFactory::getInstance( 'VisualEditor' ) ); |
84 | $this->setStats( $statsdDataFactory ); |
85 | $this->revisionLookup = $revisionLookup; |
86 | $this->tempUserCreator = $tempUserCreator; |
87 | $this->userFactory = $userFactory; |
88 | $this->userOptionsLookup = $userOptionsLookup; |
89 | $this->watchlistManager = $watchlistManager; |
90 | $this->contentTransformer = $contentTransformer; |
91 | $this->wikiPageFactory = $wikiPageFactory; |
92 | $this->introMessageBuilder = $introMessageBuilder; |
93 | $this->preloadedContentBuilder = $preloadedContentBuilder; |
94 | $this->specialPageFactory = $specialPageFactory; |
95 | $this->parsoidClientFactory = $parsoidClientFactory; |
96 | } |
97 | |
98 | /** |
99 | * @inheritDoc |
100 | */ |
101 | protected function getParsoidClient(): ParsoidClient { |
102 | return $this->parsoidClientFactory->createParsoidClient( |
103 | $this->getRequest()->getHeader( 'Cookie' ) |
104 | ); |
105 | } |
106 | |
107 | /** |
108 | * @see EditPage::getUserForPermissions |
109 | */ |
110 | private function getUserForPermissions(): User { |
111 | $user = $this->getUser(); |
112 | if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) { |
113 | return $this->userFactory->newUnsavedTempUser( |
114 | $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() ) |
115 | ); |
116 | } |
117 | return $user; |
118 | } |
119 | |
120 | /** |
121 | * @see ApiParse::getUserForPreview |
122 | */ |
123 | private function getUserForPreview(): UserIdentity { |
124 | $user = $this->getUser(); |
125 | if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) { |
126 | return $this->userFactory->newUnsavedTempUser( |
127 | $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() ) |
128 | ); |
129 | } |
130 | return $user; |
131 | } |
132 | |
133 | /** |
134 | * Run wikitext through the parser's Pre-Save-Transform |
135 | * |
136 | * @param Title $title The title of the page to use as the parsing context |
137 | * @param string $wikitext The wikitext to transform |
138 | * @return string The transformed wikitext |
139 | */ |
140 | protected function pstWikitext( Title $title, $wikitext ) { |
141 | $content = ContentHandler::makeContent( $wikitext, $title, CONTENT_MODEL_WIKITEXT ); |
142 | return $this->contentTransformer->preSaveTransform( |
143 | $content, |
144 | $title, |
145 | $this->getUserForPreview(), |
146 | $this->wikiPageFactory->newFromTitle( $title )->makeParserOptions( $this->getContext() ) |
147 | ) |
148 | ->serialize( 'text/x-wiki' ); |
149 | } |
150 | |
151 | /** |
152 | * @inheritDoc |
153 | * @suppress PhanPossiblyUndeclaredVariable False positives |
154 | */ |
155 | public function execute() { |
156 | $user = $this->getUser(); |
157 | $params = $this->extractRequestParams(); |
158 | $permissionManager = $this->getPermissionManager(); |
159 | |
160 | $title = Title::newFromText( $params['page'] ); |
161 | if ( $title && $title->isSpecialPage() ) { |
162 | // Convert Special:CollabPad/MyPage to MyPage so we can parsefragment properly |
163 | [ $special, $subPage ] = $this->specialPageFactory->resolveAlias( $title->getDBkey() ); |
164 | if ( $special === 'CollabPad' ) { |
165 | $title = Title::newFromText( $subPage ); |
166 | } |
167 | } |
168 | if ( !$title ) { |
169 | $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] ); |
170 | } |
171 | if ( !$title->canExist() ) { |
172 | $this->dieWithError( 'apierror-pagecannotexist' ); |
173 | } |
174 | |
175 | wfDebugLog( 'visualeditor', "called on '$title' with paction: '{$params['paction']}'" ); |
176 | switch ( $params['paction'] ) { |
177 | case 'parse': |
178 | case 'wikitext': |
179 | case 'metadata': |
180 | // Dirty hack to provide the correct context for FlaggedRevs when it generates edit notices |
181 | // and save dialog checkboxes. (T307852) |
182 | // FIXME Don't write to globals! Eww. |
183 | RequestContext::getMain()->setTitle( $title ); |
184 | |
185 | $preloaded = false; |
186 | $restbaseHeaders = null; |
187 | |
188 | $section = $params['section'] ?? null; |
189 | |
190 | // Get information about current revision |
191 | if ( $title->exists() ) { |
192 | $latestRevision = $this->revisionLookup->getRevisionByTitle( $title ); |
193 | if ( !$latestRevision ) { |
194 | $this->dieWithError( |
195 | [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], |
196 | 'nosuchrevid' |
197 | ); |
198 | } |
199 | if ( isset( $params['oldid'] ) ) { |
200 | $revision = $this->revisionLookup->getRevisionById( $params['oldid'] ); |
201 | if ( !$revision ) { |
202 | $this->dieWithError( [ 'apierror-nosuchrevid', $params['oldid'] ] ); |
203 | } |
204 | } else { |
205 | $revision = $latestRevision; |
206 | } |
207 | |
208 | $baseTimestamp = $latestRevision->getTimestamp(); |
209 | $oldid = $revision->getId(); |
210 | |
211 | // If requested, request HTML from Parsoid/RESTBase |
212 | if ( $params['paction'] === 'parse' ) { |
213 | $wikitext = $params['wikitext'] ?? null; |
214 | if ( $wikitext !== null ) { |
215 | $stash = $params['stash']; |
216 | if ( $params['pst'] ) { |
217 | $wikitext = $this->pstWikitext( $title, $wikitext ); |
218 | } |
219 | if ( $section !== null ) { |
220 | $sectionContent = new WikitextContent( $wikitext ); |
221 | $page = $this->wikiPageFactory->newFromTitle( $title ); |
222 | $newSectionContent = $page->replaceSectionAtRev( |
223 | $section, $sectionContent, '', $oldid |
224 | ); |
225 | '@phan-var WikitextContent $newSectionContent'; |
226 | $wikitext = $newSectionContent->getText(); |
227 | } |
228 | $response = $this->transformWikitext( |
229 | $title, $wikitext, false, $oldid, $stash |
230 | ); |
231 | } else { |
232 | $response = $this->requestRestbasePageHtml( $revision ); |
233 | } |
234 | $content = $response['body']; |
235 | $restbaseHeaders = $response['headers']; |
236 | } elseif ( $params['paction'] === 'wikitext' ) { |
237 | $apiParams = [ |
238 | 'action' => 'query', |
239 | 'revids' => $oldid, |
240 | 'prop' => 'revisions', |
241 | 'rvprop' => 'content|ids' |
242 | ]; |
243 | |
244 | $apiParams['rvsection'] = $section; |
245 | |
246 | $context = new DerivativeContext( $this->getContext() ); |
247 | $context->setRequest( |
248 | new DerivativeRequest( |
249 | $context->getRequest(), |
250 | $apiParams, |
251 | /* was posted? */ true |
252 | ) |
253 | ); |
254 | $api = new ApiMain( |
255 | $context, |
256 | /* enable write? */ true |
257 | ); |
258 | $api->execute(); |
259 | $result = $api->getResult()->getResultData(); |
260 | $pid = $title->getArticleID(); |
261 | $content = false; |
262 | if ( isset( $result['query']['pages'][$pid]['revisions'] ) ) { |
263 | foreach ( $result['query']['pages'][$pid]['revisions'] as $revArr ) { |
264 | // Check 'revisions' is an array (T193718) |
265 | if ( is_array( $revArr ) && $revArr['revid'] === $oldid ) { |
266 | $content = $revArr['content']; |
267 | } |
268 | } |
269 | } |
270 | } |
271 | } else { |
272 | $revision = null; |
273 | } |
274 | |
275 | // Use $title as the context page in every processed message (T300184) |
276 | $localizerWithTitle = new class( $this, $title ) implements MessageLocalizer { |
277 | private MessageLocalizer $base; |
278 | private PageReference $page; |
279 | |
280 | public function __construct( MessageLocalizer $base, PageReference $page ) { |
281 | $this->base = $base; |
282 | $this->page = $page; |
283 | } |
284 | |
285 | /** |
286 | * @inheritDoc |
287 | */ |
288 | public function msg( $key, ...$params ) { |
289 | return $this->base->msg( $key, ...$params )->page( $this->page ); |
290 | } |
291 | }; |
292 | |
293 | if ( !$title->exists() || $section === 'new' ) { |
294 | if ( isset( $params['wikitext'] ) ) { |
295 | $content = $params['wikitext']; |
296 | if ( $params['pst'] ) { |
297 | $content = $this->pstWikitext( $title, $content ); |
298 | } |
299 | } else { |
300 | $contentObj = $this->preloadedContentBuilder->getPreloadedContent( |
301 | $title->toPageIdentity(), |
302 | $user, |
303 | $params['preload'], |
304 | $params['preloadparams'] ?? [], |
305 | $section |
306 | ); |
307 | $dfltContent = $section === 'new' ? null : |
308 | $this->preloadedContentBuilder->getDefaultContent( $title->toPageIdentity() ); |
309 | $preloaded = $dfltContent ? !$contentObj->equals( $dfltContent ) : !$contentObj->isEmpty(); |
310 | $content = $contentObj->serialize(); |
311 | } |
312 | |
313 | if ( $content !== '' && $params['paction'] !== 'wikitext' ) { |
314 | $response = $this->transformWikitext( $title, $content, false, null, true ); |
315 | $content = $response['body']; |
316 | $restbaseHeaders = $response['headers']; |
317 | } |
318 | $baseTimestamp = wfTimestampNow(); |
319 | $oldid = 0; |
320 | } |
321 | |
322 | // Look at protection status to set up notices + surface class(es) |
323 | $builder = new TextboxBuilder(); |
324 | $protectedClasses = $builder->getTextboxProtectionCSSClasses( $title ); |
325 | |
326 | // Simplified EditPage::getEditPermissionStatus() |
327 | // TODO: Use API |
328 | // action=query&prop=info&intestactions=edit&intestactionsdetail=full&errorformat=html&errorsuselocal=1 |
329 | $status = $permissionManager->getPermissionStatus( |
330 | 'edit', $this->getUserForPermissions(), $title, PermissionManager::RIGOR_FULL ); |
331 | if ( !$status->isGood() ) { |
332 | // Show generic permission errors, including page protection, user blocks, etc. |
333 | $notice = $this->getOutput()->formatPermissionStatus( $status, 'edit' ); |
334 | // That method returns wikitext (eww), hack to get it parsed: |
335 | $notice = ( new RawMessage( '$1', [ $notice ] ) )->page( $title )->parseAsBlock(); |
336 | // Invent a message key 'permissions-error' to store in $notices |
337 | // (This probably shouldn't use the notices system…) |
338 | $notices = [ 'permissions-error' => $notice ]; |
339 | } else { |
340 | $notices = $this->introMessageBuilder->getIntroMessages( |
341 | IntroMessageBuilder::LESS_FRAMES, |
342 | [ |
343 | // This message was not shown by VisualEditor before it was switched to use |
344 | // IntroMessageBuilder, and it may be unexpected to display it now, so skip it. |
345 | 'editpage-head-copy-warn', |
346 | // This message was not shown by VisualEditor previously, and on many Wikipedias it's |
347 | // technically non-empty but hidden with CSS, and not a real edit notice (T337633). |
348 | 'editnotice-notext', |
349 | ], |
350 | $localizerWithTitle, |
351 | $title->toPageIdentity(), |
352 | $revision, |
353 | $user, |
354 | $params['editintro'], |
355 | null, |
356 | false, |
357 | $section |
358 | ); |
359 | } |
360 | |
361 | // Will be false e.g. if user is blocked or page is protected |
362 | $canEdit = $status->isGood(); |
363 | |
364 | $blockinfo = null; |
365 | // Blocked user notice |
366 | if ( $permissionManager->isBlockedFrom( $user, $title, true ) ) { |
367 | $block = $user->getBlock(); |
368 | if ( $block ) { |
369 | // Already added to $notices via #getPermissionStatus above. |
370 | // Add block info for MobileFrontend: |
371 | $blockinfo = $this->getBlockDetails( $block ); |
372 | } |
373 | } |
374 | |
375 | // HACK: Build a fake EditPage so we can get checkboxes from it |
376 | // Deliberately omitting ,0 so oldid comes from request |
377 | $article = new Article( $title ); |
378 | $editPage = new EditPage( $article ); |
379 | $req = $this->getRequest(); |
380 | $req->setVal( 'format', $editPage->contentFormat ); |
381 | // By reference for some reason (T54466) |
382 | $editPage->importFormData( $req ); |
383 | $states = [ |
384 | 'minor' => $this->userOptionsLookup->getOption( $user, 'minordefault' ) && $title->exists(), |
385 | 'watch' => $this->userOptionsLookup->getOption( $user, 'watchdefault' ) || |
386 | ( $this->userOptionsLookup->getOption( $user, 'watchcreations' ) && !$title->exists() ) || |
387 | $this->watchlistManager->isWatched( $user, $title ), |
388 | ]; |
389 | $checkboxesDef = $editPage->getCheckboxesDefinition( $states ); |
390 | $checkboxesMessagesList = []; |
391 | foreach ( $checkboxesDef as &$options ) { |
392 | if ( isset( $options['tooltip'] ) ) { |
393 | $checkboxesMessagesList[] = "accesskey-{$options['tooltip']}"; |
394 | $checkboxesMessagesList[] = "tooltip-{$options['tooltip']}"; |
395 | } |
396 | if ( isset( $options['title-message'] ) ) { |
397 | $checkboxesMessagesList[] = $options['title-message']; |
398 | if ( !is_string( $options['title-message'] ) ) { |
399 | // Extract only the key. Any parameters are included in the fake message definition |
400 | // passed via $checkboxesMessages. (This changes $checkboxesDef by reference.) |
401 | $options['title-message'] = $this->msg( $options['title-message'] )->getKey(); |
402 | } |
403 | } |
404 | $checkboxesMessagesList[] = $options['label-message']; |
405 | if ( !is_string( $options['label-message'] ) ) { |
406 | // Extract only the key. Any parameters are included in the fake message definition |
407 | // passed via $checkboxesMessages. (This changes $checkboxesDef by reference.) |
408 | $options['label-message'] = $this->msg( $options['label-message'] )->getKey(); |
409 | } |
410 | } |
411 | $checkboxesMessages = []; |
412 | foreach ( $checkboxesMessagesList as $messageSpecifier ) { |
413 | // $messageSpecifier may be a string or a Message object |
414 | $message = $this->msg( $messageSpecifier ); |
415 | $checkboxesMessages[ $message->getKey() ] = $message->plain(); |
416 | } |
417 | |
418 | foreach ( $checkboxesDef as &$value ) { |
419 | // Don't convert the boolean to empty string with formatversion=1 |
420 | $value[ApiResult::META_BC_BOOLS] = [ 'default' ]; |
421 | } |
422 | |
423 | $copyrightWarning = EditPage::getCopyrightWarning( |
424 | $title, |
425 | 'parse', |
426 | $this |
427 | ); |
428 | |
429 | // Copied from EditPage::maybeActivateTempUserCreate |
430 | // Used by code in MobileFrontend and DiscussionTools. |
431 | // TODO Make them use API |
432 | // action=query&prop=info&intestactions=edit&intestactionsautocreate=1 |
433 | $wouldautocreate = |
434 | !$user->isRegistered() |
435 | && $this->tempUserCreator->isAutoCreateAction( 'edit' ) |
436 | && $permissionManager->userHasRight( $user, 'createaccount' ); |
437 | |
438 | $result = [ |
439 | 'result' => 'success', |
440 | 'notices' => $notices, |
441 | 'copyrightWarning' => $copyrightWarning, |
442 | 'checkboxesDef' => $checkboxesDef, |
443 | 'checkboxesMessages' => $checkboxesMessages, |
444 | 'protectedClasses' => implode( ' ', $protectedClasses ), |
445 | 'basetimestamp' => $baseTimestamp, |
446 | 'starttimestamp' => wfTimestampNow(), |
447 | 'oldid' => $oldid, |
448 | 'blockinfo' => $blockinfo, |
449 | 'wouldautocreate' => $wouldautocreate, |
450 | 'canEdit' => $canEdit, |
451 | ]; |
452 | if ( isset( $restbaseHeaders['etag'] ) ) { |
453 | $result['etag'] = $restbaseHeaders['etag']; |
454 | } |
455 | if ( isset( $params['badetag'] ) ) { |
456 | $badetag = $params['badetag']; |
457 | $goodetag = $result['etag'] ?? ''; |
458 | $this->getLogger()->info( |
459 | __METHOD__ . ": Client reported bad ETag: {badetag}, expected: {goodetag}", |
460 | [ |
461 | 'badetag' => $badetag, |
462 | 'goodetag' => $goodetag, |
463 | ] |
464 | ); |
465 | } |
466 | |
467 | if ( isset( $content ) ) { |
468 | Assert::postcondition( is_string( $content ), 'Content expected' ); |
469 | $result['content'] = $content; |
470 | $result['preloaded'] = $preloaded; |
471 | } |
472 | break; |
473 | |
474 | case 'templatesused': |
475 | // HACK: Build a fake EditPage so we can get checkboxes from it |
476 | // Deliberately omitting ,0 so oldid comes from request |
477 | $article = new Article( $title ); |
478 | $editPage = new EditPage( $article ); |
479 | $result = $editPage->makeTemplatesOnThisPageList( $editPage->getTemplates() ); |
480 | break; |
481 | |
482 | case 'parsefragment': |
483 | $wikitext = $params['wikitext']; |
484 | if ( $wikitext === null ) { |
485 | $this->dieWithError( [ 'apierror-missingparam', 'wikitext' ] ); |
486 | } |
487 | if ( $params['pst'] ) { |
488 | $wikitext = $this->pstWikitext( $title, $wikitext ); |
489 | } |
490 | $content = $this->transformWikitext( |
491 | $title, $wikitext, true |
492 | )['body']; |
493 | Assert::postcondition( is_string( $content ), 'Content expected' ); |
494 | $result = [ |
495 | 'result' => 'success', |
496 | 'content' => $content |
497 | ]; |
498 | break; |
499 | } |
500 | |
501 | $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
502 | } |
503 | |
504 | /** |
505 | * Check if the configured allowed namespaces include the specified namespace |
506 | * |
507 | * @param Config $config |
508 | * @param int $namespaceId Namespace ID |
509 | * @return bool |
510 | */ |
511 | public static function isAllowedNamespace( Config $config, int $namespaceId ): bool { |
512 | return in_array( $namespaceId, self::getAvailableNamespaceIds( $config ), true ); |
513 | } |
514 | |
515 | /** |
516 | * Get a list of allowed namespace IDs |
517 | * |
518 | * @param Config $config |
519 | * @return int[] |
520 | */ |
521 | public static function getAvailableNamespaceIds( Config $config ): array { |
522 | $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
523 | $configuredNamespaces = array_replace( |
524 | ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorAvailableNamespaces' ), |
525 | $config->get( 'VisualEditorAvailableNamespaces' ) |
526 | ); |
527 | $normalized = []; |
528 | foreach ( $configuredNamespaces as $id => $enabled ) { |
529 | // Convert canonical namespace names to IDs |
530 | $id = $namespaceInfo->getCanonicalIndex( strtolower( $id ) ) ?? $id; |
531 | $normalized[$id] = $enabled && $namespaceInfo->exists( $id ); |
532 | } |
533 | ksort( $normalized ); |
534 | return array_keys( array_filter( $normalized ) ); |
535 | } |
536 | |
537 | /** |
538 | * Check if the configured allowed content models include the specified content model |
539 | * |
540 | * @param Config $config |
541 | * @param string $contentModel Content model ID |
542 | * @return bool |
543 | */ |
544 | public static function isAllowedContentType( Config $config, string $contentModel ): bool { |
545 | $availableContentModels = array_merge( |
546 | ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorAvailableContentModels' ), |
547 | $config->get( 'VisualEditorAvailableContentModels' ) |
548 | ); |
549 | return (bool)( $availableContentModels[$contentModel] ?? false ); |
550 | } |
551 | |
552 | /** |
553 | * @inheritDoc |
554 | */ |
555 | public function getAllowedParams() { |
556 | return [ |
557 | 'page' => [ |
558 | ParamValidator::PARAM_REQUIRED => true, |
559 | ], |
560 | 'badetag' => null, |
561 | 'format' => [ |
562 | ParamValidator::PARAM_DEFAULT => 'jsonfm', |
563 | ParamValidator::PARAM_TYPE => [ 'json', 'jsonfm' ], |
564 | ], |
565 | 'paction' => [ |
566 | ParamValidator::PARAM_REQUIRED => true, |
567 | ParamValidator::PARAM_TYPE => [ |
568 | 'parse', |
569 | 'metadata', |
570 | 'templatesused', |
571 | 'wikitext', |
572 | 'parsefragment', |
573 | ], |
574 | ], |
575 | 'wikitext' => [ |
576 | ParamValidator::PARAM_TYPE => 'text', |
577 | ParamValidator::PARAM_DEFAULT => null, |
578 | ], |
579 | 'section' => null, |
580 | 'stash' => false, |
581 | 'oldid' => [ |
582 | ParamValidator::PARAM_TYPE => 'integer', |
583 | ], |
584 | 'editintro' => null, |
585 | 'pst' => false, |
586 | 'preload' => null, |
587 | 'preloadparams' => [ |
588 | ParamValidator::PARAM_ISMULTI => true, |
589 | ], |
590 | ]; |
591 | } |
592 | |
593 | /** |
594 | * @inheritDoc |
595 | */ |
596 | public function needsToken() { |
597 | return false; |
598 | } |
599 | |
600 | /** |
601 | * @inheritDoc |
602 | */ |
603 | public function isInternal() { |
604 | return true; |
605 | } |
606 | |
607 | /** |
608 | * @inheritDoc |
609 | */ |
610 | public function isWriteMode() { |
611 | return false; |
612 | } |
613 | } |