Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
54.08% |
179 / 331 |
|
35.71% |
5 / 14 |
CRAP | |
0.00% |
0 / 1 |
| ApiVisualEditor | |
54.08% |
179 / 331 |
|
35.71% |
5 / 14 |
604.29 | |
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 | |
52.56% |
123 / 234 |
|
0.00% |
0 / 1 |
403.79 | |||
| makeSafeHtmlForNfc | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| isAllowedNamespace | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getAvailableNamespaceIds | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
| isAllowedContentType | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| 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 MediaWiki\Api\ApiBase; |
| 14 | use MediaWiki\Api\ApiBlockInfoTrait; |
| 15 | use MediaWiki\Api\ApiMain; |
| 16 | use MediaWiki\Api\ApiResult; |
| 17 | use MediaWiki\Config\Config; |
| 18 | use MediaWiki\Content\ContentHandler; |
| 19 | use MediaWiki\Content\Transform\ContentTransformer; |
| 20 | use MediaWiki\Content\WikitextContent; |
| 21 | use MediaWiki\Context\DerivativeContext; |
| 22 | use MediaWiki\Context\RequestContext; |
| 23 | use MediaWiki\EditPage\EditPage; |
| 24 | use MediaWiki\EditPage\IntroMessageBuilder; |
| 25 | use MediaWiki\EditPage\PreloadedContentBuilder; |
| 26 | use MediaWiki\EditPage\TextboxBuilder; |
| 27 | use MediaWiki\Language\RawMessage; |
| 28 | use MediaWiki\Logger\LoggerFactory; |
| 29 | use MediaWiki\MediaWikiServices; |
| 30 | use MediaWiki\Page\Article; |
| 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\StatsFactory; |
| 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 | StatsFactory $statsFactory, |
| 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->setStatsFactory( $statsFactory ); |
| 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 | $params['paction'] === 'wikitext' ? 'veaction=editsource' : 'veaction=edit', |
| 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 | // phpcs:disable MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment |
| 439 | /** @phpcs-require-sorted-array */ |
| 440 | $result = [ |
| 441 | // -------------------------------------------------------------------------------- |
| 442 | // This should match ArticleTarget#getWikitextDataPromiseForDoc and ArticleTarget#storeDocState |
| 443 | // -------------------------------------------------------------------------------- |
| 444 | 'basetimestamp' => $baseTimestamp, |
| 445 | 'blockinfo' => $blockinfo, // only used by MobileFrontend EditorGateway |
| 446 | 'canEdit' => $canEdit, |
| 447 | 'checkboxesDef' => $checkboxesDef, |
| 448 | 'checkboxesMessages' => $checkboxesMessages, |
| 449 | // 'content' => ..., // optional, see below |
| 450 | 'copyrightWarning' => $copyrightWarning, |
| 451 | // 'etag' => ..., // optional, see below |
| 452 | 'notices' => $notices, |
| 453 | 'oldid' => $oldid, |
| 454 | // 'preloaded' => ..., // optional, see below |
| 455 | 'protectedClasses' => implode( ' ', $protectedClasses ), |
| 456 | 'result' => 'success', // probably unused? |
| 457 | 'starttimestamp' => wfTimestampNow(), |
| 458 | 'wouldautocreate' => $wouldautocreate, |
| 459 | ]; |
| 460 | // phpcs:enable |
| 461 | if ( isset( $restbaseHeaders['etag'] ) ) { |
| 462 | $result['etag'] = $restbaseHeaders['etag']; |
| 463 | } |
| 464 | if ( isset( $params['badetag'] ) ) { |
| 465 | $badetag = $params['badetag']; |
| 466 | $goodetag = $result['etag'] ?? ''; |
| 467 | $this->getLogger()->info( |
| 468 | __METHOD__ . ": Client reported bad ETag: {badetag}, expected: {goodetag}", |
| 469 | [ |
| 470 | 'badetag' => $badetag, |
| 471 | 'goodetag' => $goodetag, |
| 472 | ] |
| 473 | ); |
| 474 | } |
| 475 | |
| 476 | if ( isset( $content ) ) { |
| 477 | Assert::postcondition( is_string( $content ), 'Content expected' ); |
| 478 | $result['content'] = $content; |
| 479 | $result['preloaded'] = $preloaded; |
| 480 | } |
| 481 | break; |
| 482 | |
| 483 | case 'templatesused': |
| 484 | // HACK: Build a fake EditPage so we can get checkboxes from it |
| 485 | // Deliberately omitting ,0 so oldid comes from request |
| 486 | $article = new Article( $title ); |
| 487 | $editPage = new EditPage( $article ); |
| 488 | $result = $editPage->makeTemplatesOnThisPageList( $editPage->getTemplates() ); |
| 489 | break; |
| 490 | |
| 491 | case 'parsefragment': |
| 492 | $wikitext = $params['wikitext']; |
| 493 | if ( $wikitext === null ) { |
| 494 | $this->dieWithError( [ 'apierror-missingparam', 'wikitext' ] ); |
| 495 | } |
| 496 | if ( $params['pst'] ) { |
| 497 | $wikitext = $this->pstWikitext( $title, $wikitext ); |
| 498 | } |
| 499 | $content = $this->transformWikitext( |
| 500 | $title, $wikitext, true |
| 501 | )['body']; |
| 502 | Assert::postcondition( is_string( $content ), 'Content expected' ); |
| 503 | $result = [ |
| 504 | 'result' => 'success', |
| 505 | 'content' => $content |
| 506 | ]; |
| 507 | break; |
| 508 | } |
| 509 | |
| 510 | if ( |
| 511 | is_array( $result ) && |
| 512 | isset( $result['content'] ) && |
| 513 | is_string( $result['content'] ) |
| 514 | ) { |
| 515 | // Protect content from being corrupted by conversion to Unicode NFC. |
| 516 | // Without this, MediaWiki::Api::ApiResult::addValue can break html tags. |
| 517 | // See T382756 |
| 518 | $result['content'] = $this->makeSafeHtmlForNfc( $result['content'] ); |
| 519 | } |
| 520 | |
| 521 | $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
| 522 | } |
| 523 | |
| 524 | /** |
| 525 | * Protect html-like content from being corrupted by conversion to Unicode NFC. |
| 526 | * |
| 527 | * Encodes U+0338 COMBINING LONG SOLIDUS OVERLAY as an html numeric character reference. |
| 528 | * Otherwise, conversion to Unicode NFC can break html tags by converting |
| 529 | * '>' + U+0338 to U+226F (NOT GREATER THAN), and |
| 530 | * '<' + U+0338 to U+226E (NOT LESS THAN) |
| 531 | * |
| 532 | * Note we cannot just search for those two combinations, because sequences of combining |
| 533 | * characters can get reordered, e.g. '>' + U+0339 + U+0338 will become U+226F + U+0339. |
| 534 | * See https://unicode.org/reports/tr15/ |
| 535 | * |
| 536 | * @param string $html |
| 537 | * @return string |
| 538 | */ |
| 539 | public static function makeSafeHtmlForNfc( string $html ) { |
| 540 | $html = str_replace( "\u{0338}", '̸', $html ); |
| 541 | return $html; |
| 542 | } |
| 543 | |
| 544 | /** |
| 545 | * Check if the configured allowed namespaces include the specified namespace |
| 546 | * |
| 547 | * @deprecated Since 1.45. Use {@link VisualEditorAvailabilityLookup::isAllowedNamespace} instead |
| 548 | * @param Config $config |
| 549 | * @param int $namespaceId Namespace ID |
| 550 | * @return bool |
| 551 | */ |
| 552 | public static function isAllowedNamespace( Config $config, int $namespaceId ): bool { |
| 553 | wfDeprecated( __METHOD__, '1.45' ); |
| 554 | return in_array( $namespaceId, self::getAvailableNamespaceIds( $config ), true ); |
| 555 | } |
| 556 | |
| 557 | /** |
| 558 | * Get a list of allowed namespace IDs |
| 559 | * |
| 560 | * @deprecated Since 1.45. Use {@link VisualEditorAvailabilityLookup::getAvailableNamespaceIds} instead |
| 561 | * @param Config $config |
| 562 | * @return int[] |
| 563 | */ |
| 564 | public static function getAvailableNamespaceIds( Config $config ): array { |
| 565 | wfDeprecated( __METHOD__, '1.45' ); |
| 566 | $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
| 567 | $configuredNamespaces = array_replace( |
| 568 | ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorAvailableNamespaces' ), |
| 569 | $config->get( 'VisualEditorAvailableNamespaces' ) |
| 570 | ); |
| 571 | $normalized = []; |
| 572 | foreach ( $configuredNamespaces as $id => $enabled ) { |
| 573 | // Convert canonical namespace names to IDs |
| 574 | $id = $namespaceInfo->getCanonicalIndex( strtolower( $id ) ) ?? $id; |
| 575 | $normalized[$id] = $enabled && $namespaceInfo->exists( $id ); |
| 576 | } |
| 577 | ksort( $normalized ); |
| 578 | return array_keys( array_filter( $normalized ) ); |
| 579 | } |
| 580 | |
| 581 | /** |
| 582 | * Check if the configured allowed content models include the specified content model |
| 583 | * |
| 584 | * @deprecated Since 1.45. Use {@link VisualEditorAvailabilityLookup::isAllowedContentType} instead |
| 585 | * @param Config $config |
| 586 | * @param string $contentModel Content model ID |
| 587 | * @return bool |
| 588 | */ |
| 589 | public static function isAllowedContentType( Config $config, string $contentModel ): bool { |
| 590 | wfDeprecated( __METHOD__, '1.45' ); |
| 591 | $availableContentModels = array_merge( |
| 592 | ExtensionRegistry::getInstance()->getAttribute( 'VisualEditorAvailableContentModels' ), |
| 593 | $config->get( 'VisualEditorAvailableContentModels' ) |
| 594 | ); |
| 595 | return (bool)( $availableContentModels[$contentModel] ?? false ); |
| 596 | } |
| 597 | |
| 598 | /** |
| 599 | * @inheritDoc |
| 600 | */ |
| 601 | public function getAllowedParams() { |
| 602 | return [ |
| 603 | 'page' => [ |
| 604 | ParamValidator::PARAM_REQUIRED => true, |
| 605 | ], |
| 606 | 'badetag' => null, |
| 607 | 'format' => [ |
| 608 | ParamValidator::PARAM_DEFAULT => 'jsonfm', |
| 609 | ParamValidator::PARAM_TYPE => [ 'json', 'jsonfm' ], |
| 610 | ], |
| 611 | 'paction' => [ |
| 612 | ParamValidator::PARAM_REQUIRED => true, |
| 613 | ParamValidator::PARAM_TYPE => [ |
| 614 | 'parse', |
| 615 | 'metadata', |
| 616 | 'templatesused', |
| 617 | 'wikitext', |
| 618 | 'parsefragment', |
| 619 | ], |
| 620 | ], |
| 621 | 'wikitext' => [ |
| 622 | ParamValidator::PARAM_TYPE => 'text', |
| 623 | ParamValidator::PARAM_DEFAULT => null, |
| 624 | ], |
| 625 | 'section' => null, |
| 626 | 'stash' => false, |
| 627 | 'oldid' => [ |
| 628 | ParamValidator::PARAM_TYPE => 'integer', |
| 629 | ], |
| 630 | 'editintro' => null, |
| 631 | 'pst' => false, |
| 632 | 'preload' => null, |
| 633 | 'preloadparams' => [ |
| 634 | ParamValidator::PARAM_ISMULTI => true, |
| 635 | ], |
| 636 | ]; |
| 637 | } |
| 638 | |
| 639 | /** |
| 640 | * @inheritDoc |
| 641 | */ |
| 642 | public function needsToken() { |
| 643 | return false; |
| 644 | } |
| 645 | |
| 646 | /** |
| 647 | * @inheritDoc |
| 648 | */ |
| 649 | public function isInternal() { |
| 650 | return true; |
| 651 | } |
| 652 | |
| 653 | /** |
| 654 | * @inheritDoc |
| 655 | */ |
| 656 | public function isWriteMode() { |
| 657 | return false; |
| 658 | } |
| 659 | } |