Added ApiStashEdit module for pre-emptive edit parsing
authorAaron Schulz <aschulz@wikimedia.org>
Thu, 20 Nov 2014 00:33:51 +0000 (16:33 -0800)
committerOri.livneh <ori@wikimedia.org>
Wed, 3 Dec 2014 22:38:41 +0000 (22:38 +0000)
* This lets edits be prepared while users enter edit summaries.
* The edit form will now make use of this API, controlled by
  $wgAjaxEditStash.

Change-Id: I4f4057bc0d1d4a66a8f7cfb7cdc26d443a8eb0c4

RELEASE-NOTES-1.25
autoload.php
includes/DefaultSettings.php
includes/EditPage.php
includes/api/ApiMain.php
includes/api/ApiStashEdit.php [new file with mode: 0644]
includes/page/WikiPage.php
resources/Resources.php
resources/src/mediawiki.action/mediawiki.action.edit.stash.js [new file with mode: 0644]

index 2b347b9..ce825e5 100644 (file)
@@ -27,6 +27,8 @@ production.
   longer be used. If extracts and page images are desired, the TextExtracts and
   PageImages extensions are required.
 * $wgOpenSearchTemplate is deprecated in favor of $wgOpenSearchTemplates.
+* Edits are now prepared via AJAX as users type edit summaries. This behavior
+  can be disabled via $wgAjaxEditStash.
 
 === New features in 1.25 ===
 * (T64861) Updated plural rules to CLDR 26. Includes incompatible changes
index 58e62b9..da1c97a 100644 (file)
@@ -120,6 +120,7 @@ $wgAutoloadLocalClasses = array(
        'ApiRollback' => __DIR__ . '/includes/api/ApiRollback.php',
        'ApiRsd' => __DIR__ . '/includes/api/ApiRsd.php',
        'ApiSetNotificationTimestamp' => __DIR__ . '/includes/api/ApiSetNotificationTimestamp.php',
+       'ApiStashEdit' => __DIR__ . '/includes/api/ApiStashEdit.php',
        'ApiTokens' => __DIR__ . '/includes/api/ApiTokens.php',
        'ApiUnblock' => __DIR__ . '/includes/api/ApiUnblock.php',
        'ApiUndelete' => __DIR__ . '/includes/api/ApiUndelete.php',
index ca41088..88bb441 100644 (file)
@@ -5024,6 +5024,13 @@ $wgRateLimits = array(
                'ip' => null,
                'subnet' => null,
        ),
+       'stashedit' => array( // stashing edits into cache before save
+               'anon' => null,
+               'user' => null,
+               'newbie' => null,
+               'ip' => null,
+               'subnet' => null,
+       )
 );
 
 /**
@@ -7037,6 +7044,12 @@ $wgAjaxUploadDestCheck = true;
  */
 $wgAjaxLicensePreview = true;
 
+/**
+ * Have clients send edits to be prepared when filling in edit summaries.
+ * This gives the server a head start on the expensive parsing operation.
+ */
+$wgAjaxEditStash = true;
+
 /**
  * Settings for incoming cross-site AJAX requests:
  * Newer browsers support cross-site AJAX when the target resource allows requests
index e51999d..4a013ef 100644 (file)
@@ -321,6 +321,9 @@ class EditPage {
        /** @var int */
        public $oldid = 0;
 
+       /** @var int */
+       public $parentRevId = 0;
+
        /** @var string */
        public $editintro = '';
 
@@ -881,6 +884,7 @@ class EditPage {
                }
 
                $this->oldid = $request->getInt( 'oldid' );
+               $this->parentRevId = $request->getInt( 'parentRevId' );
 
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
@@ -2071,7 +2075,7 @@ class EditPage {
        }
 
        function setHeaders() {
-               global $wgOut, $wgUser;
+               global $wgOut, $wgUser, $wgAjaxEditStash;
 
                $wgOut->addModules( 'mediawiki.action.edit' );
                $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
@@ -2084,6 +2088,10 @@ class EditPage {
                        $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
                }
 
+               if ( $wgAjaxEditStash ) {
+                       $wgOut->addModules( 'mediawiki.action.edit.stash' );
+               }
+
                $wgOut->setRobotPolicy( 'noindex,nofollow' );
 
                # Enabled article-related sidebar, toplinks, etc.
@@ -2448,6 +2456,8 @@ class EditPage {
                $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
 
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
+               $wgOut->addHTML( Html::hidden( 'parentRevId',
+                       $this->parentRevId ?: $this->mArticle->getRevIdFetched() ) );
 
                $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
                $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
@@ -2856,7 +2866,7 @@ class EditPage {
                global $wgOut;
                $section = htmlspecialchars( $this->section );
                $wgOut->addHTML( <<<HTML
-<input type='hidden' value="{$section}" name="wpSection" />
+<input type='hidden' value="{$section}" name="wpSection"/>
 <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
 <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
 <input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
index 7600066..3d04f95 100644 (file)
@@ -54,6 +54,7 @@ class ApiMain extends ApiBase {
                'query' => 'ApiQuery',
                'expandtemplates' => 'ApiExpandTemplates',
                'parse' => 'ApiParse',
+               'stashedit' => 'ApiStashEdit',
                'opensearch' => 'ApiOpenSearch',
                'feedcontributions' => 'ApiFeedContributions',
                'feedrecentchanges' => 'ApiFeedRecentChanges',
diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php
new file mode 100644 (file)
index 0000000..d8c7077
--- /dev/null
@@ -0,0 +1,293 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Aaron Schulz
+ */
+
+/**
+ * Prepare and edit in shared cache so that it can be reused on edit
+ *
+ * This endpoint can be called via AJAX as the user focuses on the edit
+ * summary box. By the time of submission, the parse may have already
+ * finished, and can be immediately used on page save. Certain parser
+ * functions like {{REVISIONID}} or {{CURRENTTIME}} may cause the cache
+ * to not be used on edit. Template and files used are check for changes
+ * since the output was generated. The cache TTL is also kept low for sanity.
+ *
+ * @ingroup API
+ * @since 1.25
+ */
+class ApiStashEdit extends ApiBase {
+       public function execute() {
+               global $wgMemc;
+
+               $user = $this->getUser();
+               $params = $this->extractRequestParams();
+
+               $page = $this->getTitleOrPageId( $params );
+               $title = $page->getTitle();
+
+               if ( !ContentHandler::getForModelID( $params['contentmodel'] )
+                       ->isSupportedFormat( $params['contentformat'] )
+               ) {
+                       $this->dieUsage( "Unsupported content model/format", 'badmodelformat' );
+               }
+
+               $text = trim( $params['text'] ); // needed so the key SHA1's match
+               $textContent = ContentHandler::makeContent(
+                       $text, $title, $params['contentmodel'], $params['contentformat'] );
+
+               $page = WikiPage::factory( $title );
+               if ( $page->exists() ) {
+                       // Page exists: get the merged content with the proposed change
+                       $baseRev = Revision::newFromPageId( $page->getId(), $params['baserevid'] );
+                       if ( !$baseRev ) {
+                               $this->dieUsage( "No revision ID {$params['baserevid']}", 'missingrev' );
+                       }
+                       $currentRev = $page->getRevision();
+                       if ( !$currentRev ) {
+                               $this->dieUsage( "No current revision of page ID {$page->getId()}", 'missingrev' );
+                       }
+                       // Merge in the new version of the section to get the proposed version
+                       $editContent = $page->replaceSectionAtRev(
+                               $params['section'],
+                               $textContent,
+                               $params['sectiontitle'],
+                               $baseRev->getId()
+                       );
+                       if ( !$editContent ) {
+                               $this->dieUsage( "Could not merge updated section.", 'replacefailed' );
+                       }
+                       if ( $currentRev->getId() == $baseRev->getId() ) {
+                               // Base revision was still the latest; nothing to merge
+                               $content = $editContent;
+                       } else {
+                               // Merge the edit into the current version
+                               $baseContent = $baseRev->getContent();
+                               $currentContent = $currentRev->getContent();
+                               if ( !$baseContent || !$currentContent ) {
+                                       $this->dieUsage( "Missing content for page ID {$page->getId()}", 'missingrev' );
+                               }
+                               $handler = ContentHandler::getForModelID( $baseContent->getModel() );
+                               $content = $handler->merge3( $baseContent, $editContent, $currentContent );
+                       }
+               } else {
+                       // New pages: use the user-provided content model
+                       $content = $textContent;
+               }
+
+               if ( !$content ) { // merge3() failed
+                       $this->getResult()->addValue( null,
+                               $this->getModuleName(), array( 'status' => 'editconflict' ) );
+                       return;
+               }
+
+               // The user will abort the AJAX request by pressing "save", so ignore that
+               ignore_user_abort( true );
+
+               // Get a key based on the source text, format, and user preferences
+               $key = self::getStashKey( $title, $content, $user );
+               // De-duplicate requests on the same key
+               if ( $user->pingLimiter( 'stashedit' ) ) {
+                       $editInfo = false;
+                       $status = 'ratelimited';
+               } elseif ( $wgMemc->lock( $key, 0, 30 ) ) {
+                       $contentFormat = $content->getDefaultFormat();
+                       $editInfo = $page->prepareContentForEdit( $content, null, $user, $contentFormat );
+                       $wgMemc->unlock( $key );
+                       $status = 'error'; // default
+               } else {
+                       $editInfo = false;
+                       $status = 'busy';
+               }
+
+               if ( $editInfo && $editInfo->output ) {
+                       $parserOutput = $editInfo->output;
+                       // If an item is renewed, mind the cache TTL determined by config and parser functions
+                       $since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() );
+                       $ttl = min( $parserOutput->getCacheExpiry() - $since, 5 * 60 );
+                       if ( $ttl > 0 && !$parserOutput->getFlag( 'vary-revision' ) ) {
+                               // Only store what is actually needed
+                               $stashInfo = (object)array(
+                                       'pstContent' => $editInfo->pstContent,
+                                       'output' => $editInfo->output,
+                                       'timestamp' => $editInfo->timestamp
+                               );
+                               $ok = $wgMemc->set( $key, $stashInfo, $ttl );
+                               if ( $ok ) {
+                                       $status = 'stashed';
+                                       wfDebugLog( 'PreparedEdit', "Cached parser output for key '$key'." );
+                               } else {
+                                       $status = 'error';
+                                       wfDebugLog( 'PreparedEdit', "Failed to cache parser output for key '$key'." );
+                               }
+                       } else {
+                               $status = 'uncacheable';
+                               wfDebugLog( 'PreparedEdit', "Uncacheable parser output for key '$key'." );
+                       }
+               }
+
+               $this->getResult()->addValue( null, $this->getModuleName(), array( 'status' => $status ) );
+       }
+
+       /**
+        * Get the temporary prepared edit stash key for a user
+        *
+        * @param Title $title
+        * @param Content $content
+        * @param User $user User to get parser options from
+        * @return string
+        */
+       protected static function getStashKey(
+               Title $title, Content $content, User $user
+       ) {
+               return wfMemcKey( 'prepared-edit',
+                       md5( $title->getPrefixedDBkey() ), // handle rename races
+                       $content->getModel(),
+                       $content->getDefaultFormat(),
+                       sha1( $content->serialize( $content->getDefaultFormat() ) ),
+                       $user->getId() ?: md5( $user->getName() ), // account for user parser options
+                       $user->getId() ? $user->getTouched() : '-' // handle preference change races
+               );
+       }
+
+       /**
+        * Check that a prepared edit is in cache and still up-to-date
+        *
+        * This method blocks if the prepared edit is already being rendered,
+        * waiting until rendering finishes before doing final validity checks.
+        *
+        * The cache is rejected if template or file changes are detected.
+        * Note that foreign template or file transclusions are not checked.
+        *
+        * The result is a map (pstContent,output,timestamp) with fields
+        * extracted directly from WikiPage::prepareContentForEdit().
+        *
+        * @param Title $title
+        * @param Content $content
+        * @param User $user User to get parser options from
+        * @return stdClass|bool Returns false on cache miss
+        */
+       public static function checkCache( Title $title, Content $content, User $user ) {
+               global $wgMemc;
+
+               $key = self::getStashKey( $title, $content, $user );
+               $editInfo = $wgMemc->get( $key );
+               if ( !is_object( $editInfo ) ) {
+                       $start = microtime( true );
+                       // We ignore user aborts and keep parsing. Block on any prior parsing
+                       // so as to use it's results and make use of the time spent parsing.
+                       if ( $wgMemc->lock( $key, 30, 30 ) ) {
+                               $editInfo = $wgMemc->get( $key );
+                               $wgMemc->unlock( $key );
+                               $sec = microtime( true ) - $start;
+                               wfDebugLog( 'PreparedEdit', "Waited $sec seconds on '$key'." );
+                       }
+               }
+
+               if ( !is_object( $editInfo ) || !$editInfo->output ) {
+                       return false;
+               }
+
+               $time = wfTimestamp( TS_UNIX, $editInfo->output->getTimestamp() );
+               if ( ( time() - $time ) <= 3 ) {
+                       wfDebugLog( 'PreparedEdit', "Timestamp-based cache hit for key '$key'." );
+                       return $editInfo; // assume nothing changed
+               }
+
+               $dbr = wfGetDB( DB_SLAVE );
+               // Check that no templates used in the output changed...
+               $cWhr = array(); // conditions to find changes/creations
+               $dWhr = array(); // conditions to find deletions
+               foreach ( $editInfo->output->getTemplateIds() as $ns => $stuff ) {
+                       foreach ( $stuff as $dbkey => $revId ) {
+                               $cWhr[] = array( 'page_namespace' => $ns, 'page_title' => $dbkey,
+                                       'page_latest != ' . intval( $revId ) );
+                               $dWhr[] = array( 'page_namespace' => $ns, 'page_title' => $dbkey );
+                       }
+               }
+               $change = $dbr->selectField( 'page', '1', $dbr->makeList( $cWhr, LIST_OR ), __METHOD__ );
+               $n = $dbr->selectField( 'page', 'COUNT(*)', $dbr->makeList( $dWhr, LIST_OR ), __METHOD__ );
+               if ( $change || $n != count( $dWhr ) ) {
+                       wfDebugLog( 'PreparedEdit', "Stale cache for key '$key'; template changed." );
+                       return false;
+               }
+
+               // Check that no files used in the output changed...
+               $cWhr = array(); // conditions to find changes/creations
+               $dWhr = array(); // conditions to find deletions
+               foreach ( $editInfo->output->getFileSearchOptions() as $name => $options ) {
+                       $cWhr[] = array( 'img_name' => $dbkey,
+                               'img_sha1 != ' . $dbr->addQuotes( strval( $options['sha1'] ) ) );
+                       $dWhr[] = array( 'img_name' => $dbkey );
+               }
+               $change = $dbr->selectField( 'image', '1', $dbr->makeList( $cWhr, LIST_OR ), __METHOD__ );
+               $n = $dbr->selectField( 'image', 'COUNT(*)', $dbr->makeList( $dWhr, LIST_OR ), __METHOD__ );
+               if ( $change || $n != count( $dWhr ) ) {
+                       wfDebugLog( 'PreparedEdit', "Stale cache for key '$key'; file changed." );
+                       return false;
+               }
+
+               wfDebugLog( 'PreparedEdit', "Cache hit for key '$key'." );
+
+               return $editInfo;
+       }
+
+       public function getAllowedParams() {
+               return array(
+                       'title' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => true
+                       ),
+                       'section' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                       ),
+                       'sectiontitle' => array(
+                               ApiBase::PARAM_TYPE => 'string'
+                       ),
+                       'text' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => true
+                       ),
+                       'contentmodel' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+                               ApiBase::PARAM_REQUIRED => true
+                       ),
+                       'contentformat' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                               ApiBase::PARAM_REQUIRED => true
+                       ),
+                       'baserevid' => array(
+                               ApiBase::PARAM_TYPE => 'integer',
+                               ApiBase::PARAM_REQUIRED => true
+                       )
+               );
+       }
+
+       function needsToken() {
+               return 'csrf';
+       }
+
+       function mustBePosted() {
+               return true;
+       }
+
+       function isInternal() {
+               return true;
+       }
+}
index b99093c..8b26c23 100644 (file)
@@ -1696,7 +1696,7 @@ class WikiPage implements Page, IDBAccessObject {
         *
         * @param bool|int $baseRevId The revision ID this edit was based off, if any
         * @param User $user The user doing the edit
-        * @param string $serialisation_format Format for storing the content in the
+        * @param string $serialFormat Format for storing the content in the
         *   database.
         *
         * @throws MWException
@@ -1717,7 +1717,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @since 1.21
         */
        public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
-               User $user = null, $serialisation_format = null
+               User $user = null, $serialFormat = null
        ) {
                global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
 
@@ -1783,7 +1783,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
 
-               $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
+               $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, true );
                $serialized = $editInfo->pst;
 
                /**
@@ -1825,7 +1825,7 @@ class WikiPage implements Page, IDBAccessObject {
                                'user_text'  => $user->getName(),
                                'timestamp'  => $now,
                                'content_model' => $content->getModel(),
-                               'content_format' => $serialisation_format,
+                               'content_format' => $serialFormat,
                        ) ); // XXX: pass content object?!
 
                        $changed = !$content->equals( $old_content );
@@ -1951,7 +1951,7 @@ class WikiPage implements Page, IDBAccessObject {
                                        'user_text'  => $user->getName(),
                                        'timestamp'  => $now,
                                        'content_model' => $content->getModel(),
-                                       'content_format' => $serialisation_format,
+                                       'content_format' => $serialFormat,
                                ) );
                                $revisionId = $revision->insertOn( $dbw );
 
@@ -2068,49 +2068,71 @@ class WikiPage implements Page, IDBAccessObject {
         * @param Content $content
         * @param int|null $revid
         * @param User|null $user
-        * @param string|null $serialization_format
+        * @param string|null $serialFormat
+        * @param bool $useCache Check shared prepared edit cache
         *
-        * @return bool|object
+        * @return object
         *
         * @since 1.21
         */
-       public function prepareContentForEdit( Content $content, $revid = null, User $user = null,
-               $serialization_format = null
+       public function prepareContentForEdit(
+               Content $content, $revid = null, User $user = null, $serialFormat = null, $useCache = false
        ) {
                global $wgContLang, $wgUser;
+
                $user = is_null( $user ) ? $wgUser : $user;
                //XXX: check $user->getId() here???
 
-               // Use a sane default for $serialization_format, see bug 57026
-               if ( $serialization_format === null ) {
-                       $serialization_format = $content->getContentHandler()->getDefaultFormat();
+               // Use a sane default for $serialFormat, see bug 57026
+               if ( $serialFormat === null ) {
+                       $serialFormat = $content->getContentHandler()->getDefaultFormat();
                }
 
                if ( $this->mPreparedEdit
                        && $this->mPreparedEdit->newContent
                        && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
-                       && $this->mPreparedEdit->format == $serialization_format
+                       && $this->mPreparedEdit->format == $serialFormat
                        // XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
                }
 
+               // The edit may have already been prepared via api.php?action=stashedit
+               $cachedEdit = $useCache
+                       ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
+                       : false;
+
                $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
                wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) );
 
                $edit = (object)array();
+               if ( $cachedEdit ) {
+                       $edit->timestamp = $cachedEdit->timestamp;
+               } else {
+                       $edit->timestamp = wfTimestampNow();
+               }
+               // @note: $cachedEdit is not used if the rev ID was referenced in the text
                $edit->revid = $revid;
-               $edit->timestamp = wfTimestampNow();
 
-               $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null;
+               if ( $cachedEdit ) {
+                       $edit->pstContent = $cachedEdit->pstContent;
+               } else {
+                       $edit->pstContent = $content
+                               ? $content->preSaveTransform( $this->mTitle, $user, $popts )
+                               : null;
+               }
 
-               $edit->format = $serialization_format;
+               $edit->format = $serialFormat;
                $edit->popts = $this->makeParserOptions( 'canonical' );
-               $edit->output = $edit->pstContent
-                       ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
-                       : null;
+               if ( $cachedEdit ) {
+                       $edit->output = $cachedEdit->output;
+               } else {
+                       $edit->output = $edit->pstContent
+                               ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
+                               : null;
+               }
 
                $edit->newContent = $content;
                $edit->oldContent = $this->getContent( Revision::RAW );
@@ -2118,7 +2140,7 @@ class WikiPage implements Page, IDBAccessObject {
                // NOTE: B/C for hooks! don't use these fields!
                $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : '';
                $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
-               $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : '';
+               $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
 
                $this->mPreparedEdit = $edit;
                return $edit;
@@ -2288,14 +2310,14 @@ class WikiPage implements Page, IDBAccessObject {
         * @param User $user The relevant user
         * @param string $comment Comment submitted
         * @param bool $minor Whereas it's a minor modification
-        * @param string $serialisation_format Format for storing the content in the database
+        * @param string $serialFormat Format for storing the content in the database
         */
        public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false,
-               $serialisation_format = null
+               $serialFormat = null
        ) {
                wfProfileIn( __METHOD__ );
 
-               $serialized = $content->serialize( $serialisation_format );
+               $serialized = $content->serialize( $serialFormat );
 
                $dbw = wfGetDB( DB_MASTER );
                $revision = new Revision( array(
index 9755d25..72cc2ef 100644 (file)
@@ -1050,6 +1050,13 @@ return array(
                        'mediawiki.action.history.diff',
                ),
        ),
+       'mediawiki.action.edit.stash' => array(
+               'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.stash.js',
+               'dependencies' => array(
+                       'jquery.getAttrs',
+                       'mediawiki.api',
+               ),
+       ),
        'mediawiki.action.history' => array(
                'scripts' => 'resources/src/mediawiki.action/mediawiki.action.history.js',
                'styles' => 'resources/src/mediawiki.action/mediawiki.action.history.css',
diff --git a/resources/src/mediawiki.action/mediawiki.action.edit.stash.js b/resources/src/mediawiki.action/mediawiki.action.edit.stash.js
new file mode 100644 (file)
index 0000000..895cb03
--- /dev/null
@@ -0,0 +1,31 @@
+/*!
+ * Scripts for pre-emptive edit preparing on action=edit
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var api = new mw.Api(), pending = null, $form = $( '#editform' );
+
+               function stashEdit( token ) {
+                       var data = $form.serializeObject();
+
+                       pending = api.post( {
+                               action: 'stashedit',
+                               token: token,
+                               title: mw.config.get( 'wgPageName' ),
+                               section: data.wpSection,
+                               sectiontitle: data.wpSection === 'new' ? data.wpSummary : '',
+                               text: data.wpTextbox1,
+                               contentmodel: data.model,
+                               contentformat: data.format,
+                               baserevid: data.parentRevId
+                       } );
+               }
+
+               $form.on( 'change', function () {
+                       if ( pending ) {
+                               pending.abort();
+                       }
+                       api.getToken( 'edit' ).then( stashEdit );
+               } );
+       } );
+}( mediaWiki, jQuery ) );