Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / api / ApiStashEdit.php
blobf7bd10cff434f70bd00e1fc17ba08a90b91f2c11
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.
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.
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
18 * @file
21 namespace MediaWiki\Api;
23 use Exception;
24 use MediaWiki\Content\IContentHandlerFactory;
25 use MediaWiki\Page\WikiPageFactory;
26 use MediaWiki\Revision\RevisionLookup;
27 use MediaWiki\Revision\SlotRecord;
28 use MediaWiki\Storage\PageEditStash;
29 use MediaWiki\User\TempUser\TempUserCreator;
30 use MediaWiki\User\UserFactory;
31 use Wikimedia\ParamValidator\ParamValidator;
32 use Wikimedia\Stats\StatsFactory;
34 /**
35 * Prepare an edit in shared cache so that it can be reused on edit
37 * This endpoint can be called via AJAX as the user focuses on the edit
38 * summary box. By the time of submission, the parse may have already
39 * finished, and can be immediately used on page save. Certain parser
40 * functions like {{REVISIONID}} or {{CURRENTTIME}} may cause the cache
41 * to not be used on edit. Template and files used are checked for changes
42 * since the output was generated. The cache TTL is also kept low.
44 * @ingroup API
45 * @since 1.25
47 class ApiStashEdit extends ApiBase {
49 private IContentHandlerFactory $contentHandlerFactory;
50 private PageEditStash $pageEditStash;
51 private RevisionLookup $revisionLookup;
52 private StatsFactory $stats;
53 private WikiPageFactory $wikiPageFactory;
54 private TempUserCreator $tempUserCreator;
55 private UserFactory $userFactory;
57 public function __construct(
58 ApiMain $main,
59 string $action,
60 IContentHandlerFactory $contentHandlerFactory,
61 PageEditStash $pageEditStash,
62 RevisionLookup $revisionLookup,
63 StatsFactory $statsFactory,
64 WikiPageFactory $wikiPageFactory,
65 TempUserCreator $tempUserCreator,
66 UserFactory $userFactory
67 ) {
68 parent::__construct( $main, $action );
70 $this->contentHandlerFactory = $contentHandlerFactory;
71 $this->pageEditStash = $pageEditStash;
72 $this->revisionLookup = $revisionLookup;
73 $this->stats = $statsFactory;
74 $this->wikiPageFactory = $wikiPageFactory;
75 $this->tempUserCreator = $tempUserCreator;
76 $this->userFactory = $userFactory;
79 public function execute() {
80 $user = $this->getUser();
81 $params = $this->extractRequestParams();
83 if ( $user->isBot() ) {
84 $this->dieWithError( 'apierror-botsnotsupported' );
87 $page = $this->getTitleOrPageId( $params );
88 $title = $page->getTitle();
89 $this->getErrorFormatter()->setContextTitle( $title );
91 if ( !$this->contentHandlerFactory
92 ->getContentHandler( $params['contentmodel'] )
93 ->isSupportedFormat( $params['contentformat'] )
94 ) {
95 $this->dieWithError(
96 [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ],
97 'badmodelformat'
101 $this->requireOnlyOneParameter( $params, 'stashedtexthash', 'text' );
103 if ( $params['stashedtexthash'] !== null ) {
104 // Load from cache since the client indicates the text is the same as last stash
105 $textHash = $params['stashedtexthash'];
106 if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) {
107 $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
109 $text = $this->pageEditStash->fetchInputText( $textHash );
110 if ( !is_string( $text ) ) {
111 $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
113 } else {
114 // 'text' was passed. Trim and fix newlines so the key SHA1's
115 // match (see WebRequest::getText())
116 $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
117 $textHash = sha1( $text );
120 $textContent = $this->contentHandlerFactory
121 ->getContentHandler( $params['contentmodel'] )
122 ->unserializeContent( $text, $params['contentformat'] );
124 $page = $this->wikiPageFactory->newFromTitle( $title );
125 if ( $page->exists() ) {
126 // Page exists: get the merged content with the proposed change
127 $baseRev = $this->revisionLookup->getRevisionByPageId(
128 $page->getId(),
129 $params['baserevid']
131 if ( !$baseRev ) {
132 $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
134 $currentRev = $page->getRevisionRecord();
135 if ( !$currentRev ) {
136 $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
138 // Merge in the new version of the section to get the proposed version
139 $editContent = $page->replaceSectionAtRev(
140 $params['section'],
141 $textContent,
142 $params['sectiontitle'],
143 $baseRev->getId()
145 if ( !$editContent ) {
146 $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
148 if ( $currentRev->getId() == $baseRev->getId() ) {
149 // Base revision was still the latest; nothing to merge
150 $content = $editContent;
151 } else {
152 // Merge the edit into the current version
153 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
154 $currentContent = $currentRev->getContent( SlotRecord::MAIN );
155 if ( !$baseContent || !$currentContent ) {
156 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
159 $baseModel = $baseContent->getModel();
160 $currentModel = $currentContent->getModel();
162 // T255700: Put this in try-block because if the models of these three Contents
163 // happen to not be identical, the ContentHandler may throw exception here.
164 try {
165 $content = $this->contentHandlerFactory
166 ->getContentHandler( $baseModel )
167 ->merge3( $baseContent, $editContent, $currentContent );
168 } catch ( Exception $e ) {
169 $this->dieWithException( $e, [
170 'wrap' => ApiMessage::create(
171 [ 'apierror-contentmodel-mismatch', $currentModel, $baseModel ]
173 ] );
177 } else {
178 // New pages: use the user-provided content model
179 $content = $textContent;
182 if ( !$content ) { // merge3() failed
183 $this->getResult()->addValue( null,
184 $this->getModuleName(), [ 'status' => 'editconflict' ] );
185 return;
188 if ( !$user->authorizeWrite( 'stashedit', $title ) ) {
189 $status = 'ratelimited';
190 } else {
191 $user = $this->getUserForPreview();
192 $updater = $page->newPageUpdater( $user );
193 $status = $this->pageEditStash->parseAndCache( $updater, $content, $user, $params['summary'] );
194 $this->pageEditStash->stashInputText( $text, $textHash );
197 $this->stats->getCounter( 'editstash_cache_stores_total' )
198 ->setLabel( 'status', $status )
199 ->copyToStatsdAt( "editstash.cache_stores.$status" )
200 ->increment();
202 $ret = [ 'status' => $status ];
203 // If we were rate-limited, we still return the pre-existing valid hash if one was passed
204 if ( $status !== 'ratelimited' || $params['stashedtexthash'] !== null ) {
205 $ret['texthash'] = $textHash;
208 $this->getResult()->addValue( null, $this->getModuleName(), $ret );
211 private function getUserForPreview() {
212 $user = $this->getUser();
213 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
214 return $this->userFactory->newUnsavedTempUser(
215 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
218 return $user;
221 public function getAllowedParams() {
222 return [
223 'title' => [
224 ParamValidator::PARAM_TYPE => 'string',
225 ParamValidator::PARAM_REQUIRED => true
227 'section' => [
228 ParamValidator::PARAM_TYPE => 'string',
230 'sectiontitle' => [
231 ParamValidator::PARAM_TYPE => 'string'
233 'text' => [
234 ParamValidator::PARAM_TYPE => 'text',
235 ParamValidator::PARAM_DEFAULT => null
237 'stashedtexthash' => [
238 ParamValidator::PARAM_TYPE => 'string',
239 ParamValidator::PARAM_DEFAULT => null
241 'summary' => [
242 ParamValidator::PARAM_TYPE => 'string',
243 ParamValidator::PARAM_DEFAULT => ''
245 'contentmodel' => [
246 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
247 ParamValidator::PARAM_REQUIRED => true
249 'contentformat' => [
250 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
251 ParamValidator::PARAM_REQUIRED => true
253 'baserevid' => [
254 ParamValidator::PARAM_TYPE => 'integer',
255 ParamValidator::PARAM_REQUIRED => true
260 public function needsToken() {
261 return 'csrf';
264 public function mustBePosted() {
265 return true;
268 public function isWriteMode() {
269 return true;
272 public function isInternal() {
273 return true;
276 public function getHelpUrls() {
277 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Stashedit';
281 /** @deprecated class alias since 1.43 */
282 class_alias( ApiStashEdit::class, 'ApiStashEdit' );