Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 366 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
ApiVisualEditorEdit | |
0.00% |
0 / 366 |
|
0.00% |
0 / 15 |
3080 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getParsoidClient | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
saveWikitext | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
12 | |||
parseWikitext | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
30 | |||
getWikitext | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getWikitextNoCache | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
storeInSerializationCache | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
pruneExcessStashedEntries | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
trySerializationCache | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
diffWikitext | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
12 | |||
execute | |
0.00% |
0 / 105 |
|
0.00% |
0 / 1 |
506 | |||
getAllowedParams | |
0.00% |
0 / 72 |
|
0.00% |
0 / 1 |
2 | |||
needsToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isWriteMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
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 Deflate; |
14 | use DifferenceEngine; |
15 | use FlaggablePageView; |
16 | use MediaWiki\Api\ApiBase; |
17 | use MediaWiki\Api\ApiMain; |
18 | use MediaWiki\Content\ContentHandler; |
19 | use MediaWiki\Context\DerivativeContext; |
20 | use MediaWiki\Context\RequestContext; |
21 | use MediaWiki\HookContainer\HookContainer; |
22 | use MediaWiki\Logger\LoggerFactory; |
23 | use MediaWiki\MediaWikiServices; |
24 | use MediaWiki\Page\WikiPageFactory; |
25 | use MediaWiki\Parser\Sanitizer; |
26 | use MediaWiki\Registration\ExtensionRegistry; |
27 | use MediaWiki\Request\DerivativeRequest; |
28 | use MediaWiki\Revision\SlotRecord; |
29 | use MediaWiki\SpecialPage\SpecialPageFactory; |
30 | use MediaWiki\Storage\PageEditStash; |
31 | use MediaWiki\Title\Title; |
32 | use MediaWiki\User\UserIdentity; |
33 | use SkinFactory; |
34 | use Wikimedia\ObjectCache\BagOStuff; |
35 | use Wikimedia\ParamValidator\ParamValidator; |
36 | use Wikimedia\Rdbms\IDBAccessObject; |
37 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
38 | |
39 | class ApiVisualEditorEdit extends ApiBase { |
40 | use ApiParsoidTrait; |
41 | |
42 | private const MAX_CACHE_RECENT = 2; |
43 | private const MAX_CACHE_TTL = 900; |
44 | |
45 | private VisualEditorHookRunner $hookRunner; |
46 | private PageEditStash $pageEditStash; |
47 | private SkinFactory $skinFactory; |
48 | private WikiPageFactory $wikiPageFactory; |
49 | private SpecialPageFactory $specialPageFactory; |
50 | private VisualEditorParsoidClientFactory $parsoidClientFactory; |
51 | |
52 | public function __construct( |
53 | ApiMain $main, |
54 | string $name, |
55 | HookContainer $hookContainer, |
56 | IBufferingStatsdDataFactory $statsdDataFactory, |
57 | PageEditStash $pageEditStash, |
58 | SkinFactory $skinFactory, |
59 | WikiPageFactory $wikiPageFactory, |
60 | SpecialPageFactory $specialPageFactory, |
61 | VisualEditorParsoidClientFactory $parsoidClientFactory |
62 | ) { |
63 | parent::__construct( $main, $name ); |
64 | $this->setLogger( LoggerFactory::getInstance( 'VisualEditor' ) ); |
65 | $this->setStats( $statsdDataFactory ); |
66 | $this->hookRunner = new VisualEditorHookRunner( $hookContainer ); |
67 | $this->pageEditStash = $pageEditStash; |
68 | $this->skinFactory = $skinFactory; |
69 | $this->wikiPageFactory = $wikiPageFactory; |
70 | $this->specialPageFactory = $specialPageFactory; |
71 | $this->parsoidClientFactory = $parsoidClientFactory; |
72 | } |
73 | |
74 | /** |
75 | * @inheritDoc |
76 | */ |
77 | protected function getParsoidClient(): ParsoidClient { |
78 | return $this->parsoidClientFactory->createParsoidClient( |
79 | $this->getRequest()->getHeader( 'Cookie' ) |
80 | ); |
81 | } |
82 | |
83 | /** |
84 | * Attempt to save a given page's wikitext to MediaWiki's storage layer via its API |
85 | * |
86 | * @param Title $title The title of the page to write |
87 | * @param string $wikitext The wikitext to write |
88 | * @param array $params The edit parameters |
89 | * @return mixed The result of the save attempt |
90 | */ |
91 | protected function saveWikitext( Title $title, $wikitext, $params ) { |
92 | $apiParams = [ |
93 | 'action' => 'edit', |
94 | 'title' => $title->getPrefixedDBkey(), |
95 | 'text' => $wikitext, |
96 | 'summary' => $params['summary'], |
97 | 'basetimestamp' => $params['basetimestamp'], |
98 | 'starttimestamp' => $params['starttimestamp'], |
99 | 'token' => $params['token'], |
100 | 'watchlist' => $params['watchlist'], |
101 | // NOTE: Must use getText() to work; PHP array from $params['tags'] is not understood |
102 | // by the edit API. |
103 | 'tags' => $this->getRequest()->getText( 'tags' ), |
104 | 'section' => $params['section'], |
105 | 'sectiontitle' => $params['sectiontitle'], |
106 | 'captchaid' => $params['captchaid'], |
107 | 'captchaword' => $params['captchaword'], |
108 | 'returnto' => $params['returnto'], |
109 | 'returntoquery' => $params['returntoquery'], |
110 | 'returntoanchor' => $params['returntoanchor'], |
111 | 'errorformat' => 'html', |
112 | ( $params['minor'] !== null ? 'minor' : 'notminor' ) => true, |
113 | ]; |
114 | |
115 | // Pass any unrecognized query parameters to the internal action=edit API request. This is |
116 | // necessary to support extensions that add extra stuff to the edit form (e.g. FlaggedRevs) |
117 | // and allows passing any other query parameters to be used for edit tagging (e.g. T209132). |
118 | // Exclude other known params from here and ApiMain. |
119 | // TODO: This doesn't exclude params from the formatter |
120 | $allParams = $this->getRequest()->getValues(); |
121 | $knownParams = array_keys( $this->getAllowedParams() + $this->getMain()->getAllowedParams() ); |
122 | foreach ( $knownParams as $knownParam ) { |
123 | unset( $allParams[ $knownParam ] ); |
124 | } |
125 | |
126 | $context = new DerivativeContext( $this->getContext() ); |
127 | $context->setRequest( |
128 | new DerivativeRequest( |
129 | $context->getRequest(), |
130 | $apiParams + $allParams, |
131 | /* was posted? */ true |
132 | ) |
133 | ); |
134 | $api = new ApiMain( |
135 | $context, |
136 | /* enable write? */ true |
137 | ); |
138 | |
139 | $api->execute(); |
140 | |
141 | return $api->getResult()->getResultData(); |
142 | } |
143 | |
144 | /** |
145 | * Load into an array the output of MediaWiki's parser for a given revision |
146 | * |
147 | * @param int $newRevId The revision to load |
148 | * @param array $params Original request params |
149 | * @return array Some properties haphazardly extracted from an action=parse API response |
150 | */ |
151 | protected function parseWikitext( $newRevId, array $params ) { |
152 | $apiParams = [ |
153 | 'action' => 'parse', |
154 | 'oldid' => $newRevId, |
155 | 'prop' => 'text|revid|categorieshtml|sections|displaytitle|subtitle|modules|jsconfigvars', |
156 | 'usearticle' => true, |
157 | 'useskin' => $params['useskin'], |
158 | ]; |
159 | // Boolean parameters must be omitted completely to be treated as false. |
160 | // Param is added by hook in MobileFrontend, so it may be unset. |
161 | if ( isset( $params['mobileformat'] ) && $params['mobileformat'] ) { |
162 | $apiParams['mobileformat'] = '1'; |
163 | } |
164 | |
165 | $context = new DerivativeContext( $this->getContext() ); |
166 | $context->setRequest( |
167 | new DerivativeRequest( |
168 | $context->getRequest(), |
169 | $apiParams, |
170 | /* was posted? */ true |
171 | ) |
172 | ); |
173 | $api = new ApiMain( |
174 | $context, |
175 | /* enable write? */ true |
176 | ); |
177 | |
178 | $api->execute(); |
179 | $result = $api->getResult()->getResultData( null, [ |
180 | /* Transform content nodes to '*' */ 'BC' => [], |
181 | /* Add back-compat subelements */ 'Types' => [], |
182 | /* Remove any metadata keys from the links array */ 'Strip' => 'all', |
183 | ] ); |
184 | $content = $result['parse']['text']['*'] ?? false; |
185 | $categorieshtml = $result['parse']['categorieshtml']['*'] ?? false; |
186 | $sections = isset( $result['parse']['showtoc'] ) ? $result['parse']['sections'] : []; |
187 | $displaytitle = $result['parse']['displaytitle'] ?? false; |
188 | $subtitle = $result['parse']['subtitle'] ?? false; |
189 | $modules = array_merge( |
190 | $result['parse']['modules'] ?? [], |
191 | $result['parse']['modulestyles'] ?? [] |
192 | ); |
193 | $jsconfigvars = $result['parse']['jsconfigvars'] ?? []; |
194 | |
195 | if ( $displaytitle !== false ) { |
196 | // Escape entities as in OutputPage::setPageTitle() |
197 | $displaytitle = Sanitizer::removeSomeTags( $displaytitle ); |
198 | } |
199 | |
200 | return [ |
201 | 'content' => $content, |
202 | 'categorieshtml' => $categorieshtml, |
203 | 'sections' => $sections, |
204 | 'displayTitleHtml' => $displaytitle, |
205 | 'contentSub' => $subtitle, |
206 | 'modules' => $modules, |
207 | 'jsconfigvars' => $jsconfigvars |
208 | ]; |
209 | } |
210 | |
211 | /** |
212 | * Create and load the parsed wikitext of an edit, or from the serialisation cache if available. |
213 | * |
214 | * @param Title $title The title of the page |
215 | * @param array $params The edit parameters |
216 | * @param array $parserParams The parser parameters |
217 | * @return string The wikitext of the edit |
218 | */ |
219 | protected function getWikitext( Title $title, $params, $parserParams ) { |
220 | if ( $params['cachekey'] !== null ) { |
221 | $wikitext = $this->trySerializationCache( $params['cachekey'] ); |
222 | if ( !is_string( $wikitext ) ) { |
223 | $this->dieWithError( 'apierror-visualeditor-badcachekey', 'badcachekey' ); |
224 | } |
225 | } else { |
226 | $wikitext = $this->getWikitextNoCache( $title, $params, $parserParams ); |
227 | } |
228 | '@phan-var string $wikitext'; |
229 | return $wikitext; |
230 | } |
231 | |
232 | /** |
233 | * Create and load the parsed wikitext of an edit, ignoring the serialisation cache. |
234 | * |
235 | * @param Title $title The title of the page |
236 | * @param array $params The edit parameters |
237 | * @param array $parserParams The parser parameters |
238 | * @return string The wikitext of the edit |
239 | */ |
240 | protected function getWikitextNoCache( Title $title, $params, $parserParams ) { |
241 | $this->requireOnlyOneParameter( $params, 'html' ); |
242 | if ( Deflate::isDeflated( $params['html'] ) ) { |
243 | $status = Deflate::inflate( $params['html'] ); |
244 | if ( !$status->isGood() ) { |
245 | $this->dieWithError( 'deflate-invaliddeflate', 'invaliddeflate' ); |
246 | } |
247 | $html = $status->getValue(); |
248 | } else { |
249 | $html = $params['html']; |
250 | } |
251 | $wikitext = $this->transformHTML( |
252 | $title, $html, $parserParams['oldid'] ?? null, $params['etag'] ?? null |
253 | )['body']; |
254 | return $wikitext; |
255 | } |
256 | |
257 | /** |
258 | * Load the parsed wikitext of an edit into the serialisation cache. |
259 | * |
260 | * @param Title $title The title of the page |
261 | * @param string $wikitext The wikitext of the edit |
262 | * @return string|false The key of the wikitext in the serialisation cache |
263 | */ |
264 | protected function storeInSerializationCache( Title $title, $wikitext ) { |
265 | if ( $wikitext === false ) { |
266 | return false; |
267 | } |
268 | |
269 | $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance(); |
270 | |
271 | // Store the corresponding wikitext, referenceable by a new key |
272 | $hash = md5( $wikitext ); |
273 | $key = $cache->makeKey( 'visualeditor', 'serialization', $hash ); |
274 | $ok = $cache->set( $key, $wikitext, self::MAX_CACHE_TTL ); |
275 | if ( $ok ) { |
276 | $this->pruneExcessStashedEntries( $cache, $this->getUser(), $key ); |
277 | } |
278 | |
279 | $status = $ok ? 'ok' : 'failed'; |
280 | $this->getStats()->increment( "editstash.ve_serialization_cache.set_" . $status ); |
281 | |
282 | // Also parse and prepare the edit in case it might be saved later |
283 | $pageUpdater = $this->wikiPageFactory->newFromTitle( $title )->newPageUpdater( $this->getUser() ); |
284 | $content = ContentHandler::makeContent( $wikitext, $title, CONTENT_MODEL_WIKITEXT ); |
285 | |
286 | $status = $this->pageEditStash->parseAndCache( $pageUpdater, $content, $this->getUser(), '' ); |
287 | if ( $status === $this->pageEditStash::ERROR_NONE ) { |
288 | $logger = LoggerFactory::getInstance( 'StashEdit' ); |
289 | $logger->debug( "Cached parser output for VE content key '$key'." ); |
290 | } |
291 | $this->getStats()->increment( "editstash.ve_cache_stores.$status" ); |
292 | |
293 | return $hash; |
294 | } |
295 | |
296 | private function pruneExcessStashedEntries( BagOStuff $cache, UserIdentity $user, string $newKey ): void { |
297 | $key = $cache->makeKey( 'visualeditor-serialization-recent', $user->getName() ); |
298 | |
299 | $keyList = $cache->get( $key ) ?: []; |
300 | if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) { |
301 | $oldestKey = array_shift( $keyList ); |
302 | $cache->delete( $oldestKey ); |
303 | } |
304 | |
305 | $keyList[] = $newKey; |
306 | $cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL ); |
307 | } |
308 | |
309 | /** |
310 | * Load some parsed wikitext of an edit from the serialisation cache. |
311 | * |
312 | * @param string $hash The key of the wikitext in the serialisation cache |
313 | * @return string|false The wikitext |
314 | */ |
315 | protected function trySerializationCache( $hash ) { |
316 | $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance(); |
317 | $key = $cache->makeKey( 'visualeditor', 'serialization', $hash ); |
318 | $value = $cache->get( $key ); |
319 | |
320 | $status = ( $value !== false ) ? 'hit' : 'miss'; |
321 | $this->getStats()->increment( "editstash.ve_serialization_cache.get_$status" ); |
322 | |
323 | return $value; |
324 | } |
325 | |
326 | /** |
327 | * Calculate the different between the wikitext of an edit and an existing revision. |
328 | * |
329 | * @param Title $title The title of the page |
330 | * @param int|null $fromId The existing revision of the page to compare with |
331 | * @param string $wikitext The wikitext to compare against |
332 | * @param int|null $section Whether the wikitext refers to a given section or the whole page |
333 | * @return array The comparison, or `[ 'result' => 'nochanges' ]` if there are none |
334 | */ |
335 | protected function diffWikitext( Title $title, ?int $fromId, $wikitext, $section = null ) { |
336 | $apiParams = [ |
337 | 'action' => 'compare', |
338 | 'prop' => 'diff', |
339 | // Because we're just providing wikitext, we only care about the main slot |
340 | 'slots' => SlotRecord::MAIN, |
341 | 'fromtitle' => $title->getPrefixedDBkey(), |
342 | 'fromrev' => $fromId, |
343 | 'fromsection' => $section, |
344 | 'toslots' => SlotRecord::MAIN, |
345 | 'totext-main' => $wikitext, |
346 | 'topst' => true, |
347 | ]; |
348 | |
349 | $context = new DerivativeContext( $this->getContext() ); |
350 | $context->setRequest( |
351 | new DerivativeRequest( |
352 | $context->getRequest(), |
353 | $apiParams, |
354 | /* was posted? */ true |
355 | ) |
356 | ); |
357 | $api = new ApiMain( |
358 | $context, |
359 | /* enable write? */ false |
360 | ); |
361 | $api->execute(); |
362 | $result = $api->getResult()->getResultData(); |
363 | |
364 | if ( !isset( $result['compare']['bodies'][SlotRecord::MAIN] ) ) { |
365 | $this->dieWithError( 'apierror-visualeditor-difffailed', 'difffailed' ); |
366 | } |
367 | $diffRows = $result['compare']['bodies'][SlotRecord::MAIN]; |
368 | |
369 | $context = new DerivativeContext( $this->getContext() ); |
370 | $context->setTitle( $title ); |
371 | $engine = new DifferenceEngine( $context ); |
372 | return [ |
373 | 'result' => 'success', |
374 | 'diff' => $diffRows ? $engine->addHeader( |
375 | $diffRows, |
376 | $context->msg( 'currentrev' )->parse(), |
377 | $context->msg( 'yourtext' )->parse() |
378 | ) : '' |
379 | ]; |
380 | } |
381 | |
382 | /** |
383 | * @inheritDoc |
384 | */ |
385 | public function execute() { |
386 | $user = $this->getUser(); |
387 | $params = $this->extractRequestParams(); |
388 | |
389 | $result = []; |
390 | $title = Title::newFromText( $params['page'] ); |
391 | if ( $title && $title->isSpecialPage() ) { |
392 | // Convert Special:CollabPad/MyPage to MyPage so we can serialize properly |
393 | [ $special, $subPage ] = $this->specialPageFactory->resolveAlias( $title->getDBkey() ); |
394 | if ( $special === 'CollabPad' ) { |
395 | $title = Title::newFromText( $subPage ); |
396 | } |
397 | } |
398 | if ( !$title ) { |
399 | $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] ); |
400 | } |
401 | if ( !$title->canExist() ) { |
402 | $this->dieWithError( 'apierror-pagecannotexist' ); |
403 | } |
404 | $this->getErrorFormatter()->setContextTitle( $title ); |
405 | |
406 | $parserParams = []; |
407 | if ( isset( $params['oldid'] ) ) { |
408 | $parserParams['oldid'] = $params['oldid']; |
409 | } |
410 | |
411 | if ( isset( $params['wikitext'] ) ) { |
412 | $wikitext = str_replace( "\r\n", "\n", $params['wikitext'] ); |
413 | } else { |
414 | $wikitext = $this->getWikitext( $title, $params, $parserParams ); |
415 | } |
416 | |
417 | if ( $params['paction'] === 'serialize' ) { |
418 | $result = [ 'result' => 'success', 'content' => $wikitext ]; |
419 | } elseif ( $params['paction'] === 'serializeforcache' ) { |
420 | $key = $this->storeInSerializationCache( |
421 | $title, |
422 | $wikitext |
423 | ); |
424 | $result = [ 'result' => 'success', 'cachekey' => $key ]; |
425 | } elseif ( $params['paction'] === 'diff' ) { |
426 | $section = $params['section'] ?? null; |
427 | $result = $this->diffWikitext( $title, $params['oldid'], $wikitext, $section ); |
428 | } elseif ( $params['paction'] === 'save' ) { |
429 | $pluginData = []; |
430 | foreach ( $params['plugins'] ?? [] as $plugin ) { |
431 | $pluginData[$plugin] = $params['data-' . $plugin]; |
432 | } |
433 | $presaveHook = $this->hookRunner->onVisualEditorApiVisualEditorEditPreSave( |
434 | $title->toPageIdentity(), |
435 | $user, |
436 | $wikitext, |
437 | $params, |
438 | $pluginData, |
439 | $result |
440 | ); |
441 | |
442 | if ( $presaveHook === false ) { |
443 | $this->dieWithError( $result['message'], 'hookaborted', $result ); |
444 | } |
445 | |
446 | $saveresult = $this->saveWikitext( $title, $wikitext, $params ); |
447 | $editStatus = $saveresult['edit']['result']; |
448 | |
449 | // Error |
450 | if ( $editStatus !== 'Success' ) { |
451 | $result['result'] = 'error'; |
452 | $result['edit'] = $saveresult['edit']; |
453 | } else { |
454 | // Success |
455 | $result['result'] = 'success'; |
456 | |
457 | if ( $params['nocontent'] ) { |
458 | $result['nocontent'] = true; |
459 | } else { |
460 | if ( isset( $saveresult['edit']['newrevid'] ) ) { |
461 | $newRevId = intval( $saveresult['edit']['newrevid'] ); |
462 | } else { |
463 | $newRevId = $title->getLatestRevID(); |
464 | } |
465 | |
466 | // Return result of parseWikitext instead of saveWikitext so that the |
467 | // frontend can update the page rendering without a refresh. |
468 | $parseWikitextResult = $this->parseWikitext( $newRevId, $params ); |
469 | |
470 | $result = array_merge( $result, $parseWikitextResult ); |
471 | } |
472 | |
473 | $result['isRedirect'] = (string)$title->isRedirect(); |
474 | |
475 | if ( ExtensionRegistry::getInstance()->isLoaded( 'FlaggedRevs' ) ) { |
476 | $newContext = new DerivativeContext( RequestContext::getMain() ); |
477 | // Defeat !$this->isPageView( $request ) || $request->getVal( 'oldid' ) check in setPageContent |
478 | $newRequest = new DerivativeRequest( |
479 | $this->getRequest(), |
480 | [ |
481 | 'diff' => null, |
482 | 'oldid' => '', |
483 | 'title' => $title->getPrefixedText(), |
484 | 'action' => 'view' |
485 | ] + $this->getRequest()->getValues() |
486 | ); |
487 | $newContext->setRequest( $newRequest ); |
488 | $newContext->setTitle( $title ); |
489 | |
490 | // Must be after $globalContext->setTitle since FlaggedRevs constructor |
491 | // inspects global Title |
492 | $view = FlaggablePageView::newFromTitle( $title ); |
493 | // Most likely identical to $globalState, but not our concern |
494 | $originalContext = $view->getContext(); |
495 | $view->setContext( $newContext ); |
496 | |
497 | // The two parameters here are references but we don't care |
498 | // about what FlaggedRevs does with them. |
499 | $outputDone = null; |
500 | $useParserCache = null; |
501 | // @phan-suppress-next-line PhanTypeMismatchArgument |
502 | $view->setPageContent( $outputDone, $useParserCache ); |
503 | $view->displayTag(); |
504 | $view->setContext( $originalContext ); |
505 | } |
506 | |
507 | $lang = $this->getLanguage(); |
508 | |
509 | if ( isset( $saveresult['edit']['newtimestamp'] ) ) { |
510 | $ts = $saveresult['edit']['newtimestamp']; |
511 | |
512 | $result['lastModified'] = [ |
513 | 'date' => $lang->userDate( $ts, $user ), |
514 | 'time' => $lang->userTime( $ts, $user ) |
515 | ]; |
516 | } |
517 | |
518 | if ( isset( $saveresult['edit']['newrevid'] ) ) { |
519 | $result['newrevid'] = intval( $saveresult['edit']['newrevid'] ); |
520 | } |
521 | |
522 | if ( isset( $saveresult['edit']['tempusercreated'] ) ) { |
523 | $result['tempusercreated'] = $saveresult['edit']['tempusercreated']; |
524 | } |
525 | if ( isset( $saveresult['edit']['tempusercreatedredirect'] ) ) { |
526 | $result['tempusercreatedredirect'] = $saveresult['edit']['tempusercreatedredirect']; |
527 | } |
528 | |
529 | $result['watched'] = $saveresult['edit']['watched'] ?? false; |
530 | $result['watchlistexpiry'] = $saveresult['edit']['watchlistexpiry'] ?? null; |
531 | } |
532 | |
533 | // Refresh article ID (which is used by toPageIdentity()) in case we just created the page. |
534 | // Maybe it's not great to rely on this side-effect… |
535 | $title->getArticleID( IDBAccessObject::READ_LATEST ); |
536 | |
537 | $this->hookRunner->onVisualEditorApiVisualEditorEditPostSave( |
538 | $title->toPageIdentity(), |
539 | $user, |
540 | $wikitext, |
541 | $params, |
542 | $pluginData, |
543 | $saveresult, |
544 | $result |
545 | ); |
546 | } |
547 | $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
548 | } |
549 | |
550 | /** |
551 | * @inheritDoc |
552 | */ |
553 | public function getAllowedParams() { |
554 | return [ |
555 | 'paction' => [ |
556 | ParamValidator::PARAM_REQUIRED => true, |
557 | ParamValidator::PARAM_TYPE => [ |
558 | 'serialize', |
559 | 'serializeforcache', |
560 | 'diff', |
561 | 'save', |
562 | ], |
563 | ], |
564 | 'page' => [ |
565 | ParamValidator::PARAM_REQUIRED => true, |
566 | ], |
567 | 'token' => [ |
568 | ParamValidator::PARAM_REQUIRED => true, |
569 | ], |
570 | 'wikitext' => [ |
571 | ParamValidator::PARAM_TYPE => 'text', |
572 | ParamValidator::PARAM_DEFAULT => null, |
573 | ], |
574 | 'section' => null, |
575 | 'sectiontitle' => null, |
576 | 'basetimestamp' => [ |
577 | ParamValidator::PARAM_TYPE => 'timestamp', |
578 | ], |
579 | 'starttimestamp' => [ |
580 | ParamValidator::PARAM_TYPE => 'timestamp', |
581 | ], |
582 | 'oldid' => [ |
583 | ParamValidator::PARAM_TYPE => 'integer', |
584 | ], |
585 | 'minor' => null, |
586 | 'watchlist' => null, |
587 | 'html' => [ |
588 | // Use the 'raw' type to avoid Unicode NFC normalization. |
589 | // This makes the parameter binary safe, so that (a) if |
590 | // we use client-side compression it is not mangled, and/or |
591 | // (b) deprecated Unicode sequences explicitly encoded in |
592 | // wikitext (ie,  ) are not mangled. Wikitext is |
593 | // in Unicode Normal Form C, but because of explicit entities |
594 | // the output HTML is not guaranteed to be. |
595 | ParamValidator::PARAM_TYPE => 'raw', |
596 | ParamValidator::PARAM_DEFAULT => null, |
597 | ], |
598 | 'etag' => null, |
599 | 'summary' => null, |
600 | 'captchaid' => null, |
601 | 'captchaword' => null, |
602 | 'cachekey' => null, |
603 | 'nocontent' => false, |
604 | 'returnto' => [ |
605 | ParamValidator::PARAM_TYPE => 'title', |
606 | ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returnto', |
607 | ], |
608 | 'returntoquery' => [ |
609 | ParamValidator::PARAM_TYPE => 'string', |
610 | ParamValidator::PARAM_DEFAULT => '', |
611 | ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returntoquery', |
612 | ], |
613 | 'returntoanchor' => [ |
614 | ParamValidator::PARAM_TYPE => 'string', |
615 | ParamValidator::PARAM_DEFAULT => '', |
616 | ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returntoanchor', |
617 | ], |
618 | 'useskin' => [ |
619 | ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ), |
620 | ApiBase::PARAM_HELP_MSG => 'apihelp-parse-param-useskin', |
621 | ], |
622 | 'tags' => [ |
623 | ParamValidator::PARAM_ISMULTI => true, |
624 | ], |
625 | 'plugins' => [ |
626 | ParamValidator::PARAM_ISMULTI => true, |
627 | ParamValidator::PARAM_TYPE => 'string', |
628 | ], |
629 | // Additional data sent by the client. Not used directly in the ApiVisualEditorEdit workflows, but |
630 | // is passed alongside the other parameters to implementations of onApiVisualEditorEditPostSave and |
631 | // onApiVisualEditorEditPreSave |
632 | 'data-{plugin}' => [ |
633 | ApiBase::PARAM_TEMPLATE_VARS => [ 'plugin' => 'plugins' ] |
634 | ] |
635 | ]; |
636 | } |
637 | |
638 | /** |
639 | * @inheritDoc |
640 | */ |
641 | public function needsToken() { |
642 | return 'csrf'; |
643 | } |
644 | |
645 | /** |
646 | * @inheritDoc |
647 | */ |
648 | public function isInternal() { |
649 | return true; |
650 | } |
651 | |
652 | /** |
653 | * @inheritDoc |
654 | */ |
655 | public function isWriteMode() { |
656 | return true; |
657 | } |
658 | |
659 | } |