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
21 namespace MediaWiki\Api
;
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
;
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.
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(
60 IContentHandlerFactory
$contentHandlerFactory,
61 PageEditStash
$pageEditStash,
62 RevisionLookup
$revisionLookup,
63 StatsFactory
$statsFactory,
64 WikiPageFactory
$wikiPageFactory,
65 TempUserCreator
$tempUserCreator,
66 UserFactory
$userFactory
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'] )
96 [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ],
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' );
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(
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(
142 $params['sectiontitle'],
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;
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.
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 ]
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' ] );
188 if ( !$user->authorizeWrite( 'stashedit', $title ) ) {
189 $status = 'ratelimited';
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" )
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() )
221 public function getAllowedParams() {
224 ParamValidator
::PARAM_TYPE
=> 'string',
225 ParamValidator
::PARAM_REQUIRED
=> true
228 ParamValidator
::PARAM_TYPE
=> 'string',
231 ParamValidator
::PARAM_TYPE
=> 'string'
234 ParamValidator
::PARAM_TYPE
=> 'text',
235 ParamValidator
::PARAM_DEFAULT
=> null
237 'stashedtexthash' => [
238 ParamValidator
::PARAM_TYPE
=> 'string',
239 ParamValidator
::PARAM_DEFAULT
=> null
242 ParamValidator
::PARAM_TYPE
=> 'string',
243 ParamValidator
::PARAM_DEFAULT
=> ''
246 ParamValidator
::PARAM_TYPE
=> $this->contentHandlerFactory
->getContentModels(),
247 ParamValidator
::PARAM_REQUIRED
=> true
250 ParamValidator
::PARAM_TYPE
=> $this->contentHandlerFactory
->getAllContentFormats(),
251 ParamValidator
::PARAM_REQUIRED
=> true
254 ParamValidator
::PARAM_TYPE
=> 'integer',
255 ParamValidator
::PARAM_REQUIRED
=> true
260 public function needsToken() {
264 public function mustBePosted() {
268 public function isWriteMode() {
272 public function isInternal() {
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' );