ApiStashEdit.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. */
  20. use MediaWiki\MediaWikiServices;
  21. use MediaWiki\Storage\PageEditStash;
  22. /**
  23. * Prepare an edit in shared cache so that it can be reused on edit
  24. *
  25. * This endpoint can be called via AJAX as the user focuses on the edit
  26. * summary box. By the time of submission, the parse may have already
  27. * finished, and can be immediately used on page save. Certain parser
  28. * functions like {{REVISIONID}} or {{CURRENTTIME}} may cause the cache
  29. * to not be used on edit. Template and files used are check for changes
  30. * since the output was generated. The cache TTL is also kept low for sanity.
  31. *
  32. * @ingroup API
  33. * @since 1.25
  34. */
  35. class ApiStashEdit extends ApiBase {
  36. const ERROR_NONE = PageEditStash::ERROR_NONE; // b/c
  37. const ERROR_PARSE = PageEditStash::ERROR_PARSE; // b/c
  38. const ERROR_CACHE = PageEditStash::ERROR_CACHE; // b/c
  39. const ERROR_UNCACHEABLE = PageEditStash::ERROR_UNCACHEABLE; // b/c
  40. const ERROR_BUSY = PageEditStash::ERROR_BUSY; // b/c
  41. public function execute() {
  42. $user = $this->getUser();
  43. $params = $this->extractRequestParams();
  44. if ( $user->isBot() ) { // sanity
  45. $this->dieWithError( 'apierror-botsnotsupported' );
  46. }
  47. $editStash = MediaWikiServices::getInstance()->getPageEditStash();
  48. $page = $this->getTitleOrPageId( $params );
  49. $title = $page->getTitle();
  50. if ( !ContentHandler::getForModelID( $params['contentmodel'] )
  51. ->isSupportedFormat( $params['contentformat'] )
  52. ) {
  53. $this->dieWithError(
  54. [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ],
  55. 'badmodelformat'
  56. );
  57. }
  58. $this->requireOnlyOneParameter( $params, 'stashedtexthash', 'text' );
  59. $text = null;
  60. $textHash = null;
  61. if ( $params['stashedtexthash'] !== null ) {
  62. // Load from cache since the client indicates the text is the same as last stash
  63. $textHash = $params['stashedtexthash'];
  64. if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) {
  65. $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
  66. }
  67. $text = $editStash->fetchInputText( $textHash );
  68. if ( !is_string( $text ) ) {
  69. $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
  70. }
  71. } else {
  72. // 'text' was passed. Trim and fix newlines so the key SHA1's
  73. // match (see WebRequest::getText())
  74. $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
  75. $textHash = sha1( $text );
  76. }
  77. $textContent = ContentHandler::makeContent(
  78. $text, $title, $params['contentmodel'], $params['contentformat'] );
  79. $page = WikiPage::factory( $title );
  80. if ( $page->exists() ) {
  81. // Page exists: get the merged content with the proposed change
  82. $baseRev = Revision::newFromPageId( $page->getId(), $params['baserevid'] );
  83. if ( !$baseRev ) {
  84. $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
  85. }
  86. $currentRev = $page->getRevision();
  87. if ( !$currentRev ) {
  88. $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
  89. }
  90. // Merge in the new version of the section to get the proposed version
  91. $editContent = $page->replaceSectionAtRev(
  92. $params['section'],
  93. $textContent,
  94. $params['sectiontitle'],
  95. $baseRev->getId()
  96. );
  97. if ( !$editContent ) {
  98. $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
  99. }
  100. if ( $currentRev->getId() == $baseRev->getId() ) {
  101. // Base revision was still the latest; nothing to merge
  102. $content = $editContent;
  103. } else {
  104. // Merge the edit into the current version
  105. $baseContent = $baseRev->getContent();
  106. $currentContent = $currentRev->getContent();
  107. if ( !$baseContent || !$currentContent ) {
  108. $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
  109. }
  110. $handler = ContentHandler::getForModelID( $baseContent->getModel() );
  111. $content = $handler->merge3( $baseContent, $editContent, $currentContent );
  112. }
  113. } else {
  114. // New pages: use the user-provided content model
  115. $content = $textContent;
  116. }
  117. if ( !$content ) { // merge3() failed
  118. $this->getResult()->addValue( null,
  119. $this->getModuleName(), [ 'status' => 'editconflict' ] );
  120. return;
  121. }
  122. if ( $user->pingLimiter( 'stashedit' ) ) {
  123. $status = 'ratelimited';
  124. } else {
  125. $status = $editStash->parseAndCache( $page, $content, $user, $params['summary'] );
  126. $editStash->stashInputText( $text, $textHash );
  127. }
  128. $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
  129. $stats->increment( "editstash.cache_stores.$status" );
  130. $ret = [ 'status' => $status ];
  131. // If we were rate-limited, we still return the pre-existing valid hash if one was passed
  132. if ( $status !== 'ratelimited' || $params['stashedtexthash'] !== null ) {
  133. $ret['texthash'] = $textHash;
  134. }
  135. $this->getResult()->addValue( null, $this->getModuleName(), $ret );
  136. }
  137. /**
  138. * @param WikiPage $page
  139. * @param Content $content Edit content
  140. * @param User $user
  141. * @param string $summary Edit summary
  142. * @return string ApiStashEdit::ERROR_* constant
  143. * @since 1.25
  144. * @deprecated Since 1.34
  145. */
  146. public function parseAndStash( WikiPage $page, Content $content, User $user, $summary ) {
  147. $editStash = MediaWikiServices::getInstance()->getPageEditStash();
  148. return $editStash->parseAndCache( $page, $content, $user, $summary );
  149. }
  150. public function getAllowedParams() {
  151. return [
  152. 'title' => [
  153. ApiBase::PARAM_TYPE => 'string',
  154. ApiBase::PARAM_REQUIRED => true
  155. ],
  156. 'section' => [
  157. ApiBase::PARAM_TYPE => 'string',
  158. ],
  159. 'sectiontitle' => [
  160. ApiBase::PARAM_TYPE => 'string'
  161. ],
  162. 'text' => [
  163. ApiBase::PARAM_TYPE => 'text',
  164. ApiBase::PARAM_DFLT => null
  165. ],
  166. 'stashedtexthash' => [
  167. ApiBase::PARAM_TYPE => 'string',
  168. ApiBase::PARAM_DFLT => null
  169. ],
  170. 'summary' => [
  171. ApiBase::PARAM_TYPE => 'string',
  172. ],
  173. 'contentmodel' => [
  174. ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
  175. ApiBase::PARAM_REQUIRED => true
  176. ],
  177. 'contentformat' => [
  178. ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
  179. ApiBase::PARAM_REQUIRED => true
  180. ],
  181. 'baserevid' => [
  182. ApiBase::PARAM_TYPE => 'integer',
  183. ApiBase::PARAM_REQUIRED => true
  184. ]
  185. ];
  186. }
  187. public function needsToken() {
  188. return 'csrf';
  189. }
  190. public function mustBePosted() {
  191. return true;
  192. }
  193. public function isWriteMode() {
  194. return true;
  195. }
  196. public function isInternal() {
  197. return true;
  198. }
  199. }