Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 316 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
ApiDiscussionToolsEdit | |
0.00% |
0 / 316 |
|
0.00% |
0 / 5 |
3422 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 221 |
|
0.00% |
0 / 1 |
2970 | |||
getAllowedParams | |
0.00% |
0 / 83 |
|
0.00% |
0 / 1 |
2 | |||
needsToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isWriteMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\DiscussionTools; |
4 | |
5 | use ApiBase; |
6 | use ApiMain; |
7 | use ApiUsageException; |
8 | use MediaWiki\Config\Config; |
9 | use MediaWiki\Config\ConfigFactory; |
10 | use MediaWiki\Context\DerivativeContext; |
11 | use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils; |
12 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem; |
13 | use MediaWiki\Extension\VisualEditor\ApiParsoidTrait; |
14 | use MediaWiki\Extension\VisualEditor\VisualEditorParsoidClientFactory; |
15 | use MediaWiki\Logger\LoggerFactory; |
16 | use MediaWiki\Request\DerivativeRequest; |
17 | use MediaWiki\Revision\RevisionLookup; |
18 | use MediaWiki\Title\Title; |
19 | use MediaWiki\User\TempUser\TempUserCreator; |
20 | use MediaWiki\User\UserFactory; |
21 | use SkinFactory; |
22 | use Wikimedia\Assert\Assert; |
23 | use Wikimedia\ParamValidator\ParamValidator; |
24 | use Wikimedia\ParamValidator\TypeDef\StringDef; |
25 | use Wikimedia\Parsoid\Utils\DOMCompat; |
26 | use Wikimedia\Parsoid\Utils\DOMUtils; |
27 | |
28 | class ApiDiscussionToolsEdit extends ApiBase { |
29 | use ApiDiscussionToolsTrait; |
30 | use ApiParsoidTrait; |
31 | |
32 | private CommentParser $commentParser; |
33 | private VisualEditorParsoidClientFactory $parsoidClientFactory; |
34 | private SubscriptionStore $subscriptionStore; |
35 | private TempUserCreator $tempUserCreator; |
36 | private UserFactory $userFactory; |
37 | private SkinFactory $skinFactory; |
38 | private Config $config; |
39 | private RevisionLookup $revisionLookup; |
40 | |
41 | public function __construct( |
42 | ApiMain $main, |
43 | string $name, |
44 | VisualEditorParsoidClientFactory $parsoidClientFactory, |
45 | CommentParser $commentParser, |
46 | SubscriptionStore $subscriptionStore, |
47 | TempUserCreator $tempUserCreator, |
48 | UserFactory $userFactory, |
49 | SkinFactory $skinFactory, |
50 | ConfigFactory $configFactory, |
51 | RevisionLookup $revisionLookup |
52 | ) { |
53 | parent::__construct( $main, $name ); |
54 | $this->parsoidClientFactory = $parsoidClientFactory; |
55 | $this->commentParser = $commentParser; |
56 | $this->subscriptionStore = $subscriptionStore; |
57 | $this->tempUserCreator = $tempUserCreator; |
58 | $this->userFactory = $userFactory; |
59 | $this->skinFactory = $skinFactory; |
60 | $this->config = $configFactory->makeConfig( 'discussiontools' ); |
61 | $this->revisionLookup = $revisionLookup; |
62 | $this->setLogger( LoggerFactory::getInstance( 'DiscussionTools' ) ); |
63 | } |
64 | |
65 | /** |
66 | * @inheritDoc |
67 | * @throws ApiUsageException |
68 | */ |
69 | public function execute() { |
70 | $params = $this->extractRequestParams(); |
71 | $title = Title::newFromText( $params['page'] ); |
72 | $result = null; |
73 | |
74 | $autoSubscribe = $params['autosubscribe'] === 'yes' || |
75 | ( $this->config->get( 'DiscussionToolsAutoTopicSubEditor' ) === 'discussiontoolsapi' && |
76 | HookUtils::shouldAddAutoSubscription( $this->getUser(), $title ) && |
77 | $params['autosubscribe'] === 'default' ); |
78 | $subscribableHeadingName = null; |
79 | $subscribableSectionTitle = ''; |
80 | |
81 | if ( !$title ) { |
82 | $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] ); |
83 | } |
84 | |
85 | $this->getErrorFormatter()->setContextTitle( $title ); |
86 | |
87 | $session = null; |
88 | $usedFormTokensKey = 'DiscussionTools:usedFormTokens'; |
89 | $formToken = $params['formtoken']; |
90 | if ( $formToken ) { |
91 | $session = $this->getContext()->getRequest()->getSession(); |
92 | $usedFormTokens = $session->get( $usedFormTokensKey ) ?? []; |
93 | if ( in_array( $formToken, $usedFormTokens, true ) ) { |
94 | $this->dieWithError( [ 'apierror-discussiontools-formtoken-used' ] ); |
95 | } |
96 | } |
97 | |
98 | $this->requireOnlyOneParameter( $params, 'wikitext', 'html' ); |
99 | if ( $params['paction'] === 'addtopic' ) { |
100 | $this->requireAtLeastOneParameter( $params, 'sectiontitle' ); |
101 | } |
102 | |
103 | // To determine if we need to add a signature, |
104 | // preview the comment without adding one and check if the result is signed properly. |
105 | $previewResult = $this->previewMessage( |
106 | $params['paction'] === 'addtopic' ? 'topic' : 'reply', |
107 | $title, |
108 | [ |
109 | 'wikitext' => $params['wikitext'], |
110 | 'html' => $params['html'], |
111 | 'sectiontitle' => $params['sectiontitle'], |
112 | ], |
113 | $params |
114 | ); |
115 | $previewResultHtml = $previewResult->getResultData( [ 'parse', 'text' ] ); |
116 | $previewContainer = DOMCompat::getBody( DOMUtils::parseHTML( $previewResultHtml ) ); |
117 | $previewThreadItemSet = $this->commentParser->parse( $previewContainer, $title->getTitleValue() ); |
118 | if ( CommentUtils::isSingleCommentSignedBy( |
119 | $previewThreadItemSet, $this->getUserForPreview()->getName(), $previewContainer |
120 | ) ) { |
121 | $signature = null; |
122 | } else { |
123 | $signature = $this->msg( 'discussiontools-signature-prefix' )->inContentLanguage()->text() . '~~~~'; |
124 | } |
125 | |
126 | switch ( $params['paction'] ) { |
127 | case 'addtopic': |
128 | $wikitext = $params['wikitext']; |
129 | $html = $params['html']; |
130 | |
131 | $previewHeading = null; |
132 | $previewHeadings = $previewThreadItemSet->getThreads(); |
133 | if ( count( $previewHeadings ) > 0 && !$previewHeadings[ 0 ]->isPlaceholderHeading() ) { |
134 | $previewHeading = $previewHeadings[ 0 ]; |
135 | } |
136 | |
137 | if ( !$params['allownosectiontitle'] ) { |
138 | // Check if the preview HTML starts with a section title. Note that even if the provided |
139 | // 'sectiontitle' param is empty, a heading could been included in the message body, and |
140 | // that's acceptable (T338390). Heading levels other than the default level 2 are also |
141 | // acceptable (T267288). |
142 | if ( !$previewHeading ) { |
143 | $this->dieWithError( [ 'discussiontools-newtopic-missing-title' ] ); |
144 | } |
145 | } |
146 | |
147 | if ( isset( $params['summary'] ) ) { |
148 | $summary = $params['summary']; |
149 | } else { |
150 | // Generate an edit summary from the heading in the preview HTML, rather than from the |
151 | // 'sectiontitle' param like the action=edit API would. This has two benefits: |
152 | // * Works when the heading is included in the message body instead of the param (T338390) |
153 | // * Works better for complicated markup in the heading, e.g. templates (T335200) |
154 | if ( $previewHeading ) { |
155 | $sectionTitle = $previewHeading->getLinkableTitle(); |
156 | $summary = $this->msg( 'newsectionsummary' )->plaintextParams( $sectionTitle ) |
157 | ->inContentLanguage()->text(); |
158 | } else { |
159 | // TODO: Should we generate something here? (T275702) |
160 | $summary = ''; |
161 | } |
162 | } |
163 | |
164 | if ( $wikitext !== null ) { |
165 | if ( $signature !== null ) { |
166 | $wikitext = CommentModifier::appendSignatureWikitext( $wikitext, $signature ); |
167 | } |
168 | } else { |
169 | $doc = DOMUtils::parseHTML( '' ); |
170 | $container = DOMUtils::parseHTMLToFragment( $doc, $html ); |
171 | if ( $signature !== null ) { |
172 | CommentModifier::appendSignature( $container, $signature ); |
173 | } |
174 | $html = DOMUtils::getFragmentInnerHTML( $container ); |
175 | $wikitext = $this->transformHTML( $title, $html )[ 'body' ]; |
176 | } |
177 | |
178 | $mobileFormatParams = []; |
179 | // Boolean parameters must be omitted completely to be treated as false. |
180 | // Param is added by hook in MobileFrontend, so it may be unset. |
181 | if ( isset( $params['mobileformat'] ) && $params['mobileformat'] ) { |
182 | $mobileFormatParams['mobileformat'] = '1'; |
183 | } |
184 | // As section=new this is append only so we don't need to |
185 | // worry about edit-conflict params such as oldid/baserevid/etag. |
186 | // Edit summary is also automatically generated when section=new |
187 | $context = new DerivativeContext( $this->getContext() ); |
188 | $context->setRequest( |
189 | new DerivativeRequest( |
190 | $context->getRequest(), |
191 | [ |
192 | 'action' => 'visualeditoredit', |
193 | 'paction' => 'save', |
194 | 'page' => $params['page'], |
195 | 'token' => $params['token'], |
196 | 'wikitext' => $wikitext, |
197 | 'summary' => $summary, |
198 | 'section' => 'new', |
199 | 'sectiontitle' => $params['sectiontitle'], |
200 | 'starttimestamp' => wfTimestampNow(), |
201 | 'useskin' => $params['useskin'], |
202 | 'watchlist' => $params['watchlist'], |
203 | 'captchaid' => $params['captchaid'], |
204 | 'captchaword' => $params['captchaword'], |
205 | // Always fetch content if auto-subscribing, it's needed below (T359751) |
206 | 'nocontent' => $autoSubscribe ? null : $params['nocontent'], |
207 | // NOTE: Must use getText() to work; PHP array from $params['tags'] is not understood |
208 | // by the visualeditoredit API. |
209 | 'tags' => $this->getRequest()->getText( 'tags' ), |
210 | 'returnto' => $params['returnto'], |
211 | 'returntoquery' => $params['returntoquery'], |
212 | 'returntoanchor' => $params['returntoanchor'], |
213 | ] + $mobileFormatParams, |
214 | /* was posted? */ true |
215 | ) |
216 | ); |
217 | $api = new ApiMain( |
218 | $context, |
219 | /* enable write? */ true |
220 | ); |
221 | |
222 | $api->execute(); |
223 | |
224 | $data = $api->getResult()->getResultData(); |
225 | $result = $data['visualeditoredit']; |
226 | |
227 | if ( $autoSubscribe && isset( $result['content'] ) ) { |
228 | // Determining the added topic's name directly is hard (we'd have to ensure we have the |
229 | // same timestamp, and replicate some CommentParser stuff). Just pull it out of the response. |
230 | $doc = DOMUtils::parseHTML( $result['content'] ); |
231 | $container = DOMCompat::getBody( $doc ); |
232 | $threadItemSet = $this->commentParser->parse( $container, $title->getTitleValue() ); |
233 | $threads = $threadItemSet->getThreads(); |
234 | if ( count( $threads ) ) { |
235 | $lastHeading = end( $threads ); |
236 | $subscribableHeadingName = $lastHeading->getName(); |
237 | $subscribableSectionTitle = $lastHeading->getLinkableTitle(); |
238 | } |
239 | if ( $params['nocontent'] ) { |
240 | // We had to fetch content even if not requested by the caller (T359751), but pretend we didn't |
241 | unset( $result['content'] ); |
242 | $result['nocontent'] = true; |
243 | } |
244 | } |
245 | |
246 | break; |
247 | |
248 | case 'addcomment': |
249 | $this->requireAtLeastOneParameter( $params, 'commentid', 'commentname' ); |
250 | |
251 | $commentId = $params['commentid'] ?? null; |
252 | $commentName = $params['commentname'] ?? null; |
253 | |
254 | if ( !$title->exists() ) { |
255 | // The page does not exist, so the comment we're trying to reply to can't exist either. |
256 | if ( $commentId ) { |
257 | $this->dieWithError( [ 'apierror-discussiontools-commentid-notfound', $commentId ] ); |
258 | } else { |
259 | $this->dieWithError( [ 'apierror-discussiontools-commentname-notfound', $commentName ] ); |
260 | } |
261 | } |
262 | |
263 | // Fetch the latest revision |
264 | $requestedRevision = $this->revisionLookup->getRevisionByTitle( $title ); |
265 | if ( !$requestedRevision ) { |
266 | $this->dieWithError( |
267 | [ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], |
268 | 'nosuchrevid' |
269 | ); |
270 | } |
271 | |
272 | $response = $this->requestRestbasePageHtml( $requestedRevision ); |
273 | |
274 | $headers = $response['headers']; |
275 | $doc = DOMUtils::parseHTML( $response['body'] ); |
276 | |
277 | // Validate that we got the revision we requested. |
278 | // Ported from ve.init.mw.ArticleTarget.prototype.parseMetadata |
279 | $docRevId = null; |
280 | $aboutDoc = $doc->documentElement->getAttribute( 'about' ); |
281 | if ( $aboutDoc ) { |
282 | preg_match( '/revision\\/([0-9]+)$/', $aboutDoc, $docRevIdMatches ); |
283 | if ( $docRevIdMatches ) { |
284 | $docRevId = (int)$docRevIdMatches[ 1 ]; |
285 | } |
286 | } |
287 | Assert::postcondition( $docRevId !== null, |
288 | 'Parsoid document had no revision information' ); |
289 | Assert::postcondition( $docRevId === $requestedRevision->getId(), |
290 | 'Parsoid revision did not match requested revision' ); |
291 | |
292 | $container = DOMCompat::getBody( $doc ); |
293 | |
294 | // Unwrap sections, so that transclusions overlapping section boundaries don't cause all |
295 | // comments in the sections to be treated as transcluded from another page. |
296 | CommentUtils::unwrapParsoidSections( $container ); |
297 | |
298 | $threadItemSet = $this->commentParser->parse( $container, $title->getTitleValue() ); |
299 | |
300 | if ( $commentId ) { |
301 | $comment = $threadItemSet->findCommentById( $commentId ); |
302 | |
303 | if ( !$comment || !( $comment instanceof ContentCommentItem ) ) { |
304 | $this->dieWithError( [ 'apierror-discussiontools-commentid-notfound', $commentId ] ); |
305 | } |
306 | |
307 | } else { |
308 | $comments = $threadItemSet->findCommentsByName( $commentName ); |
309 | $comment = $comments[ 0 ] ?? null; |
310 | |
311 | if ( count( $comments ) > 1 ) { |
312 | $this->dieWithError( [ 'apierror-discussiontools-commentname-ambiguous', $commentName ] ); |
313 | } elseif ( !$comment || !( $comment instanceof ContentCommentItem ) ) { |
314 | $this->dieWithError( [ 'apierror-discussiontools-commentname-notfound', $commentName ] ); |
315 | } |
316 | } |
317 | |
318 | if ( $comment->getTranscludedFrom() ) { |
319 | // Replying to transcluded comments should not be possible. We check this client-side, but |
320 | // the comment might have become transcluded in the meantime (T268069), so check again. We |
321 | // didn't have this check before T313100, since usually the reply would just disappear in |
322 | // Parsoid, but now it would be placed after the transclusion, which would be wrong. |
323 | $this->dieWithError( 'discussiontools-error-comment-not-saved', 'comment-became-transcluded' ); |
324 | } |
325 | |
326 | if ( $params['wikitext'] !== null ) { |
327 | CommentModifier::addWikitextReply( $comment, $params['wikitext'], $signature ); |
328 | } else { |
329 | CommentModifier::addHtmlReply( $comment, $params['html'], $signature ); |
330 | } |
331 | |
332 | if ( isset( $params['summary'] ) ) { |
333 | $summary = $params['summary']; |
334 | } else { |
335 | $sectionTitle = $comment->getHeading()->getLinkableTitle(); |
336 | $summary = ( $sectionTitle ? '/* ' . $sectionTitle . ' */ ' : '' ) . |
337 | $this->msg( 'discussiontools-defaultsummary-reply' )->inContentLanguage()->text(); |
338 | } |
339 | |
340 | if ( $autoSubscribe ) { |
341 | $heading = $comment->getSubscribableHeading(); |
342 | if ( $heading ) { |
343 | $subscribableHeadingName = $heading->getName(); |
344 | $subscribableSectionTitle = $heading->getLinkableTitle(); |
345 | } |
346 | } |
347 | |
348 | $context = new DerivativeContext( $this->getContext() ); |
349 | $context->setRequest( |
350 | new DerivativeRequest( |
351 | $context->getRequest(), |
352 | [ |
353 | 'action' => 'visualeditoredit', |
354 | 'paction' => 'save', |
355 | 'page' => $params['page'], |
356 | 'token' => $params['token'], |
357 | 'oldid' => $docRevId, |
358 | 'html' => DOMCompat::getOuterHTML( $doc->documentElement ), |
359 | 'summary' => $summary, |
360 | 'baserevid' => $docRevId, |
361 | 'starttimestamp' => wfTimestampNow(), |
362 | 'etag' => $headers['etag'] ?? null, |
363 | 'useskin' => $params['useskin'], |
364 | 'watchlist' => $params['watchlist'], |
365 | 'captchaid' => $params['captchaid'], |
366 | 'captchaword' => $params['captchaword'], |
367 | 'nocontent' => $params['nocontent'], |
368 | // NOTE: Must use getText() to work; PHP array from $params['tags'] is not understood |
369 | // by the visualeditoredit API. |
370 | 'tags' => $this->getRequest()->getText( 'tags' ), |
371 | 'returnto' => $params['returnto'], |
372 | 'returntoquery' => $params['returntoquery'], |
373 | 'returntoanchor' => $params['returntoanchor'], |
374 | ], |
375 | /* was posted? */ true |
376 | ) |
377 | ); |
378 | $api = new ApiMain( |
379 | $context, |
380 | /* enable write? */ true |
381 | ); |
382 | |
383 | $api->execute(); |
384 | |
385 | // TODO: Tags are only added by 'dttags' existing on the original request |
386 | // context (see Hook::onRecentChangeSave). What tags (if any) should be |
387 | // added in this API? |
388 | |
389 | $data = $api->getResult()->getResultData(); |
390 | $result = $data['visualeditoredit']; |
391 | break; |
392 | } |
393 | |
394 | if ( !isset( $result['newrevid'] ) && isset( $result['result'] ) && $result['result'] === 'success' ) { |
395 | // No new revision, so no changes were made to the page (null edit). |
396 | // Comment was not actually saved, so for this API, that's an error. |
397 | // This should not be possible after T313100. |
398 | $this->dieWithError( 'discussiontools-error-comment-not-saved', 'comment-comment-not-saved' ); |
399 | } |
400 | |
401 | if ( $autoSubscribe && $subscribableHeadingName ) { |
402 | $subsTitle = $title->createFragmentTarget( $subscribableSectionTitle ); |
403 | $this->subscriptionStore |
404 | ->addAutoSubscriptionForUser( $this->getUser(), $subsTitle, $subscribableHeadingName ); |
405 | } |
406 | |
407 | // Check the post was successful (could have been blocked by ConfirmEdit) before |
408 | // marking the form token as used. |
409 | if ( $formToken && isset( $result['result'] ) && $result['result'] === 'success' ) { |
410 | $usedFormTokens[] = $formToken; |
411 | // Set an arbitrary limit of the number of form tokens to |
412 | // store to prevent session storage from becoming full. |
413 | // It is unlikely that form tokens other than the few most |
414 | // recently used will be needed. |
415 | while ( count( $usedFormTokens ) > 50 ) { |
416 | // Discard the oldest tokens first |
417 | array_shift( $usedFormTokens ); |
418 | } |
419 | $session->set( $usedFormTokensKey, $usedFormTokens ); |
420 | } |
421 | |
422 | $this->getResult()->addValue( null, $this->getModuleName(), $result ); |
423 | } |
424 | |
425 | /** |
426 | * @inheritDoc |
427 | */ |
428 | public function getAllowedParams() { |
429 | return [ |
430 | 'paction' => [ |
431 | ParamValidator::PARAM_REQUIRED => true, |
432 | ParamValidator::PARAM_TYPE => [ |
433 | 'addcomment', |
434 | 'addtopic', |
435 | ], |
436 | ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-paction', |
437 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [], |
438 | ], |
439 | 'autosubscribe' => [ |
440 | ParamValidator::PARAM_TYPE => [ |
441 | 'yes', |
442 | 'no', |
443 | 'default' |
444 | ], |
445 | ParamValidator::PARAM_DEFAULT => 'default', |
446 | ], |
447 | 'page' => [ |
448 | ParamValidator::PARAM_REQUIRED => true, |
449 | ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-page', |
450 | ], |
451 | 'token' => [ |
452 | ParamValidator::PARAM_REQUIRED => true, |
453 | ], |
454 | 'formtoken' => [ |
455 | ParamValidator::PARAM_TYPE => 'string', |
456 | StringDef::PARAM_MAX_CHARS => 16, |
457 | ], |
458 | 'commentname' => null, |
459 | 'commentid' => null, |
460 | 'wikitext' => [ |
461 | ParamValidator::PARAM_TYPE => 'text', |
462 | ParamValidator::PARAM_DEFAULT => null, |
463 | ], |
464 | 'html' => [ |
465 | ParamValidator::PARAM_TYPE => 'text', |
466 | ParamValidator::PARAM_DEFAULT => null, |
467 | ], |
468 | 'summary' => [ |
469 | ParamValidator::PARAM_TYPE => 'string', |
470 | ParamValidator::PARAM_DEFAULT => null, |
471 | ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-summary', |
472 | ], |
473 | 'sectiontitle' => [ |
474 | ParamValidator::PARAM_TYPE => 'string', |
475 | ], |
476 | 'allownosectiontitle' => false, |
477 | 'useskin' => [ |
478 | ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ), |
479 | ApiBase::PARAM_HELP_MSG => 'apihelp-parse-param-useskin', |
480 | ], |
481 | 'watchlist' => [ |
482 | ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-watchlist', |
483 | ], |
484 | 'captchaid' => [ |
485 | ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-captchaid', |
486 | ], |
487 | 'captchaword' => [ |
488 | ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-captchaword', |
489 | ], |
490 | 'nocontent' => [ |
491 | ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-nocontent', |
492 | ], |
493 | 'tags' => [ |
494 | ParamValidator::PARAM_ISMULTI => true, |
495 | ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-tags', |
496 | ], |
497 | 'returnto' => [ |
498 | ParamValidator::PARAM_TYPE => 'title', |
499 | ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returnto', |
500 | ], |
501 | 'returntoquery' => [ |
502 | ParamValidator::PARAM_TYPE => 'string', |
503 | ParamValidator::PARAM_DEFAULT => '', |
504 | ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returntoquery', |
505 | ], |
506 | 'returntoanchor' => [ |
507 | ParamValidator::PARAM_TYPE => 'string', |
508 | ParamValidator::PARAM_DEFAULT => '', |
509 | ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returntoanchor', |
510 | ], |
511 | ]; |
512 | } |
513 | |
514 | /** |
515 | * @inheritDoc |
516 | */ |
517 | public function needsToken() { |
518 | return 'csrf'; |
519 | } |
520 | |
521 | /** |
522 | * @inheritDoc |
523 | */ |
524 | public function isWriteMode() { |
525 | return true; |
526 | } |
527 | } |