ApiEditPage.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. <?php
  2. /**
  3. *
  4. *
  5. * Created on August 16, 2007
  6. *
  7. * Copyright © 2007 Iker Labarga "<Firstname><Lastname>@gmail.com"
  8. *
  9. * This program is free software; you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation; either version 2 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License along
  20. * with this program; if not, write to the Free Software Foundation, Inc.,
  21. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. * http://www.gnu.org/copyleft/gpl.html
  23. *
  24. * @file
  25. */
  26. /**
  27. * A module that allows for editing and creating pages.
  28. *
  29. * Currently, this wraps around the EditPage class in an ugly way,
  30. * EditPage.php should be rewritten to provide a cleaner interface,
  31. * see T20654 if you're inspired to fix this.
  32. *
  33. * @ingroup API
  34. */
  35. class ApiEditPage extends ApiBase {
  36. public function execute() {
  37. $this->useTransactionalTimeLimit();
  38. $user = $this->getUser();
  39. $params = $this->extractRequestParams();
  40. $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
  41. $pageObj = $this->getTitleOrPageId( $params );
  42. $titleObj = $pageObj->getTitle();
  43. $apiResult = $this->getResult();
  44. if ( $params['redirect'] ) {
  45. if ( $params['prependtext'] === null && $params['appendtext'] === null
  46. && $params['section'] !== 'new'
  47. ) {
  48. $this->dieWithError( 'apierror-redirect-appendonly' );
  49. }
  50. if ( $titleObj->isRedirect() ) {
  51. $oldTitle = $titleObj;
  52. $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
  53. ->getContent( Revision::FOR_THIS_USER, $user )
  54. ->getRedirectChain();
  55. // array_shift( $titles );
  56. $redirValues = [];
  57. /** @var Title $newTitle */
  58. foreach ( $titles as $id => $newTitle ) {
  59. if ( !isset( $titles[$id - 1] ) ) {
  60. $titles[$id - 1] = $oldTitle;
  61. }
  62. $redirValues[] = [
  63. 'from' => $titles[$id - 1]->getPrefixedText(),
  64. 'to' => $newTitle->getPrefixedText()
  65. ];
  66. $titleObj = $newTitle;
  67. }
  68. ApiResult::setIndexedTagName( $redirValues, 'r' );
  69. $apiResult->addValue( null, 'redirects', $redirValues );
  70. // Since the page changed, update $pageObj
  71. $pageObj = WikiPage::factory( $titleObj );
  72. }
  73. }
  74. if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) {
  75. $contentHandler = $pageObj->getContentHandler();
  76. } else {
  77. $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
  78. }
  79. $contentModel = $contentHandler->getModelID();
  80. $name = $titleObj->getPrefixedDBkey();
  81. $model = $contentHandler->getModelID();
  82. if ( $params['undo'] > 0 ) {
  83. // allow undo via api
  84. } elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
  85. $this->dieWithError( [ 'apierror-no-direct-editing', $model, $name ] );
  86. }
  87. if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
  88. $contentFormat = $contentHandler->getDefaultFormat();
  89. } else {
  90. $contentFormat = $params['contentformat'];
  91. }
  92. if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
  93. $this->dieWithError( [ 'apierror-badformat', $contentFormat, $model, $name ] );
  94. }
  95. if ( $params['createonly'] && $titleObj->exists() ) {
  96. $this->dieWithError( 'apierror-articleexists' );
  97. }
  98. if ( $params['nocreate'] && !$titleObj->exists() ) {
  99. $this->dieWithError( 'apierror-missingtitle' );
  100. }
  101. // Now let's check whether we're even allowed to do this
  102. $this->checkTitleUserPermissions(
  103. $titleObj,
  104. $titleObj->exists() ? 'edit' : [ 'edit', 'create' ]
  105. );
  106. $toMD5 = $params['text'];
  107. if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) {
  108. $content = $pageObj->getContent();
  109. if ( !$content ) {
  110. if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) {
  111. # If this is a MediaWiki:x message, then load the messages
  112. # and return the message value for x.
  113. $text = $titleObj->getDefaultMessageText();
  114. if ( $text === false ) {
  115. $text = '';
  116. }
  117. try {
  118. $content = ContentHandler::makeContent( $text, $this->getTitle() );
  119. } catch ( MWContentSerializationException $ex ) {
  120. $this->dieWithException( $ex, [
  121. 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
  122. ] );
  123. return;
  124. }
  125. } else {
  126. # Otherwise, make a new empty content.
  127. $content = $contentHandler->makeEmptyContent();
  128. }
  129. }
  130. // @todo Add support for appending/prepending to the Content interface
  131. if ( !( $content instanceof TextContent ) ) {
  132. $modelName = $contentHandler->getModelID();
  133. $this->dieWithError( [ 'apierror-appendnotsupported', $modelName ] );
  134. }
  135. if ( !is_null( $params['section'] ) ) {
  136. if ( !$contentHandler->supportsSections() ) {
  137. $modelName = $contentHandler->getModelID();
  138. $this->dieWithError( [ 'apierror-sectionsnotsupported', $modelName ] );
  139. }
  140. if ( $params['section'] == 'new' ) {
  141. // DWIM if they're trying to prepend/append to a new section.
  142. $content = null;
  143. } else {
  144. // Process the content for section edits
  145. $section = $params['section'];
  146. $content = $content->getSection( $section );
  147. if ( !$content ) {
  148. $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
  149. }
  150. }
  151. }
  152. if ( !$content ) {
  153. $text = '';
  154. } else {
  155. $text = $content->serialize( $contentFormat );
  156. }
  157. $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
  158. $toMD5 = $params['prependtext'] . $params['appendtext'];
  159. }
  160. if ( $params['undo'] > 0 ) {
  161. if ( $params['undoafter'] > 0 ) {
  162. if ( $params['undo'] < $params['undoafter'] ) {
  163. list( $params['undo'], $params['undoafter'] ) =
  164. [ $params['undoafter'], $params['undo'] ];
  165. }
  166. $undoafterRev = Revision::newFromId( $params['undoafter'] );
  167. }
  168. $undoRev = Revision::newFromId( $params['undo'] );
  169. if ( is_null( $undoRev ) || $undoRev->isDeleted( Revision::DELETED_TEXT ) ) {
  170. $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
  171. }
  172. if ( $params['undoafter'] == 0 ) {
  173. $undoafterRev = $undoRev->getPrevious();
  174. }
  175. if ( is_null( $undoafterRev ) || $undoafterRev->isDeleted( Revision::DELETED_TEXT ) ) {
  176. $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
  177. }
  178. if ( $undoRev->getPage() != $pageObj->getId() ) {
  179. $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
  180. $titleObj->getPrefixedText() ] );
  181. }
  182. if ( $undoafterRev->getPage() != $pageObj->getId() ) {
  183. $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
  184. $titleObj->getPrefixedText() ] );
  185. }
  186. $newContent = $contentHandler->getUndoContent(
  187. $pageObj->getRevision(),
  188. $undoRev,
  189. $undoafterRev
  190. );
  191. if ( !$newContent ) {
  192. $this->dieWithError( 'undo-failure', 'undofailure' );
  193. }
  194. if ( empty( $params['contentmodel'] )
  195. && empty( $params['contentformat'] )
  196. ) {
  197. // If we are reverting content model, the new content model
  198. // might not support the current serialization format, in
  199. // which case go back to the old serialization format,
  200. // but only if the user hasn't specified a format/model
  201. // parameter.
  202. if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
  203. $contentFormat = $undoafterRev->getContentFormat();
  204. }
  205. // Override content model with model of undid revision.
  206. $contentModel = $newContent->getModel();
  207. }
  208. $params['text'] = $newContent->serialize( $contentFormat );
  209. // If no summary was given and we only undid one rev,
  210. // use an autosummary
  211. if ( is_null( $params['summary'] ) &&
  212. $titleObj->getNextRevisionID( $undoafterRev->getId() ) == $params['undo']
  213. ) {
  214. $params['summary'] = wfMessage( 'undo-summary' )
  215. ->params( $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text();
  216. }
  217. }
  218. // See if the MD5 hash checks out
  219. if ( !is_null( $params['md5'] ) && md5( $toMD5 ) !== $params['md5'] ) {
  220. $this->dieWithError( 'apierror-badmd5' );
  221. }
  222. // EditPage wants to parse its stuff from a WebRequest
  223. // That interface kind of sucks, but it's workable
  224. $requestArray = [
  225. 'wpTextbox1' => $params['text'],
  226. 'format' => $contentFormat,
  227. 'model' => $contentModel,
  228. 'wpEditToken' => $params['token'],
  229. 'wpIgnoreBlankSummary' => true,
  230. 'wpIgnoreBlankArticle' => true,
  231. 'wpIgnoreSelfRedirect' => true,
  232. 'bot' => $params['bot'],
  233. 'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
  234. ];
  235. if ( !is_null( $params['summary'] ) ) {
  236. $requestArray['wpSummary'] = $params['summary'];
  237. }
  238. if ( !is_null( $params['sectiontitle'] ) ) {
  239. $requestArray['wpSectionTitle'] = $params['sectiontitle'];
  240. }
  241. // TODO: Pass along information from 'undoafter' as well
  242. if ( $params['undo'] > 0 ) {
  243. $requestArray['wpUndidRevision'] = $params['undo'];
  244. }
  245. // Watch out for basetimestamp == '' or '0'
  246. // It gets treated as NOW, almost certainly causing an edit conflict
  247. if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
  248. $requestArray['wpEdittime'] = $params['basetimestamp'];
  249. } else {
  250. $requestArray['wpEdittime'] = $pageObj->getTimestamp();
  251. }
  252. if ( $params['starttimestamp'] !== null ) {
  253. $requestArray['wpStarttime'] = $params['starttimestamp'];
  254. } else {
  255. $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
  256. }
  257. if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) {
  258. $requestArray['wpMinoredit'] = '';
  259. }
  260. if ( $params['recreate'] ) {
  261. $requestArray['wpRecreate'] = '';
  262. }
  263. if ( !is_null( $params['section'] ) ) {
  264. $section = $params['section'];
  265. if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
  266. $this->dieWithError( 'apierror-invalidsection' );
  267. }
  268. $content = $pageObj->getContent();
  269. if ( $section !== '0' && $section != 'new'
  270. && ( !$content || !$content->getSection( $section ) )
  271. ) {
  272. $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
  273. }
  274. $requestArray['wpSection'] = $params['section'];
  275. } else {
  276. $requestArray['wpSection'] = '';
  277. }
  278. $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj );
  279. // Deprecated parameters
  280. if ( $params['watch'] ) {
  281. $watch = true;
  282. } elseif ( $params['unwatch'] ) {
  283. $watch = false;
  284. }
  285. if ( $watch ) {
  286. $requestArray['wpWatchthis'] = '';
  287. }
  288. // Apply change tags
  289. if ( count( $params['tags'] ) ) {
  290. $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
  291. if ( $tagStatus->isOK() ) {
  292. $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
  293. } else {
  294. $this->dieStatus( $tagStatus );
  295. }
  296. }
  297. // Pass through anything else we might have been given, to support extensions
  298. // This is kind of a hack but it's the best we can do to make extensions work
  299. $requestArray += $this->getRequest()->getValues();
  300. global $wgTitle, $wgRequest;
  301. $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
  302. // Some functions depend on $wgTitle == $ep->mTitle
  303. // TODO: Make them not or check if they still do
  304. $wgTitle = $titleObj;
  305. $articleContext = new RequestContext;
  306. $articleContext->setRequest( $req );
  307. $articleContext->setWikiPage( $pageObj );
  308. $articleContext->setUser( $this->getUser() );
  309. /** @var Article $articleObject */
  310. $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
  311. $ep = new EditPage( $articleObject );
  312. $ep->setApiEditOverride( true );
  313. $ep->setContextTitle( $titleObj );
  314. $ep->importFormData( $req );
  315. $content = $ep->textbox1;
  316. // Run hooks
  317. // Handle APIEditBeforeSave parameters
  318. $r = [];
  319. // Deprecated in favour of EditFilterMergedContent
  320. if ( !Hooks::run( 'APIEditBeforeSave', [ $ep, $content, &$r ], '1.28' ) ) {
  321. if ( count( $r ) ) {
  322. $r['result'] = 'Failure';
  323. $apiResult->addValue( null, $this->getModuleName(), $r );
  324. return;
  325. }
  326. $this->dieWithError( 'hookaborted' );
  327. }
  328. // Do the actual save
  329. $oldRevId = $articleObject->getRevIdFetched();
  330. $result = null;
  331. // Fake $wgRequest for some hooks inside EditPage
  332. // @todo FIXME: This interface SUCKS
  333. $oldRequest = $wgRequest;
  334. $wgRequest = $req;
  335. $status = $ep->attemptSave( $result );
  336. $wgRequest = $oldRequest;
  337. switch ( $status->value ) {
  338. case EditPage::AS_HOOK_ERROR:
  339. case EditPage::AS_HOOK_ERROR_EXPECTED:
  340. if ( isset( $status->apiHookResult ) ) {
  341. $r = $status->apiHookResult;
  342. $r['result'] = 'Failure';
  343. $apiResult->addValue( null, $this->getModuleName(), $r );
  344. return;
  345. }
  346. if ( !$status->getErrors() ) {
  347. $status->fatal( 'hookaborted' );
  348. }
  349. $this->dieStatus( $status );
  350. case EditPage::AS_BLOCKED_PAGE_FOR_USER:
  351. $this->dieWithError(
  352. 'apierror-blocked',
  353. 'blocked',
  354. [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
  355. );
  356. case EditPage::AS_READ_ONLY_PAGE:
  357. $this->dieReadOnly();
  358. case EditPage::AS_SUCCESS_NEW_ARTICLE:
  359. $r['new'] = true;
  360. // fall-through
  361. case EditPage::AS_SUCCESS_UPDATE:
  362. $r['result'] = 'Success';
  363. $r['pageid'] = intval( $titleObj->getArticleID() );
  364. $r['title'] = $titleObj->getPrefixedText();
  365. $r['contentmodel'] = $articleObject->getContentModel();
  366. $newRevId = $articleObject->getLatest();
  367. if ( $newRevId == $oldRevId ) {
  368. $r['nochange'] = true;
  369. } else {
  370. $r['oldrevid'] = intval( $oldRevId );
  371. $r['newrevid'] = intval( $newRevId );
  372. $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
  373. $pageObj->getTimestamp() );
  374. }
  375. break;
  376. default:
  377. if ( !$status->getErrors() ) {
  378. // EditPage sometimes only sets the status code without setting
  379. // any actual error messages. Supply defaults for those cases.
  380. switch ( $status->value ) {
  381. // Currently needed
  382. case EditPage::AS_IMAGE_REDIRECT_ANON:
  383. $status->fatal( 'apierror-noimageredirect-anon' );
  384. break;
  385. case EditPage::AS_IMAGE_REDIRECT_LOGGED:
  386. $status->fatal( 'apierror-noimageredirect-logged' );
  387. break;
  388. case EditPage::AS_CONTENT_TOO_BIG:
  389. case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
  390. $status->fatal( 'apierror-contenttoobig', $this->getConfig()->get( 'MaxArticleSize' ) );
  391. break;
  392. case EditPage::AS_READ_ONLY_PAGE_ANON:
  393. $status->fatal( 'apierror-noedit-anon' );
  394. break;
  395. case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
  396. $status->fatal( 'apierror-cantchangecontentmodel' );
  397. break;
  398. case EditPage::AS_ARTICLE_WAS_DELETED:
  399. $status->fatal( 'apierror-pagedeleted' );
  400. break;
  401. case EditPage::AS_CONFLICT_DETECTED:
  402. $status->fatal( 'editconflict' );
  403. break;
  404. // Currently shouldn't be needed, but here in case
  405. // hooks use them without setting appropriate
  406. // errors on the status.
  407. case EditPage::AS_SPAM_ERROR:
  408. $status->fatal( 'apierror-spamdetected', $result['spam'] );
  409. break;
  410. case EditPage::AS_READ_ONLY_PAGE_LOGGED:
  411. $status->fatal( 'apierror-noedit' );
  412. break;
  413. case EditPage::AS_RATE_LIMITED:
  414. $status->fatal( 'apierror-ratelimited' );
  415. break;
  416. case EditPage::AS_NO_CREATE_PERMISSION:
  417. $status->fatal( 'nocreate-loggedin' );
  418. break;
  419. case EditPage::AS_BLANK_ARTICLE:
  420. $status->fatal( 'apierror-emptypage' );
  421. break;
  422. case EditPage::AS_TEXTBOX_EMPTY:
  423. $status->fatal( 'apierror-emptynewsection' );
  424. break;
  425. case EditPage::AS_SUMMARY_NEEDED:
  426. $status->fatal( 'apierror-summaryrequired' );
  427. break;
  428. default:
  429. wfWarn( __METHOD__ . ": Unknown EditPage code {$status->value} with no message" );
  430. $status->fatal( 'apierror-unknownerror-editpage', $status->value );
  431. break;
  432. }
  433. }
  434. $this->dieStatus( $status );
  435. break;
  436. }
  437. $apiResult->addValue( null, $this->getModuleName(), $r );
  438. }
  439. public function mustBePosted() {
  440. return true;
  441. }
  442. public function isWriteMode() {
  443. return true;
  444. }
  445. public function getAllowedParams() {
  446. return [
  447. 'title' => [
  448. ApiBase::PARAM_TYPE => 'string',
  449. ],
  450. 'pageid' => [
  451. ApiBase::PARAM_TYPE => 'integer',
  452. ],
  453. 'section' => null,
  454. 'sectiontitle' => [
  455. ApiBase::PARAM_TYPE => 'string',
  456. ],
  457. 'text' => [
  458. ApiBase::PARAM_TYPE => 'text',
  459. ],
  460. 'summary' => null,
  461. 'tags' => [
  462. ApiBase::PARAM_TYPE => 'tags',
  463. ApiBase::PARAM_ISMULTI => true,
  464. ],
  465. 'minor' => false,
  466. 'notminor' => false,
  467. 'bot' => false,
  468. 'basetimestamp' => [
  469. ApiBase::PARAM_TYPE => 'timestamp',
  470. ],
  471. 'starttimestamp' => [
  472. ApiBase::PARAM_TYPE => 'timestamp',
  473. ],
  474. 'recreate' => false,
  475. 'createonly' => false,
  476. 'nocreate' => false,
  477. 'watch' => [
  478. ApiBase::PARAM_DFLT => false,
  479. ApiBase::PARAM_DEPRECATED => true,
  480. ],
  481. 'unwatch' => [
  482. ApiBase::PARAM_DFLT => false,
  483. ApiBase::PARAM_DEPRECATED => true,
  484. ],
  485. 'watchlist' => [
  486. ApiBase::PARAM_DFLT => 'preferences',
  487. ApiBase::PARAM_TYPE => [
  488. 'watch',
  489. 'unwatch',
  490. 'preferences',
  491. 'nochange'
  492. ],
  493. ],
  494. 'md5' => null,
  495. 'prependtext' => [
  496. ApiBase::PARAM_TYPE => 'text',
  497. ],
  498. 'appendtext' => [
  499. ApiBase::PARAM_TYPE => 'text',
  500. ],
  501. 'undo' => [
  502. ApiBase::PARAM_TYPE => 'integer'
  503. ],
  504. 'undoafter' => [
  505. ApiBase::PARAM_TYPE => 'integer'
  506. ],
  507. 'redirect' => [
  508. ApiBase::PARAM_TYPE => 'boolean',
  509. ApiBase::PARAM_DFLT => false,
  510. ],
  511. 'contentformat' => [
  512. ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
  513. ],
  514. 'contentmodel' => [
  515. ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
  516. ],
  517. 'token' => [
  518. // Standard definition automatically inserted
  519. ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
  520. ],
  521. ];
  522. }
  523. public function needsToken() {
  524. return 'csrf';
  525. }
  526. protected function getExamplesMessages() {
  527. return [
  528. 'action=edit&title=Test&summary=test%20summary&' .
  529. 'text=article%20content&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
  530. => 'apihelp-edit-example-edit',
  531. 'action=edit&title=Test&summary=NOTOC&minor=&' .
  532. 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
  533. => 'apihelp-edit-example-prepend',
  534. 'action=edit&title=Test&undo=13585&undoafter=13579&' .
  535. 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
  536. => 'apihelp-edit-example-undo',
  537. ];
  538. }
  539. public function getHelpUrls() {
  540. return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
  541. }
  542. }