Reuse page preview parses by using the edit stash system
authorAaron Schulz <aschulz@wikimedia.org>
Thu, 4 Dec 2014 09:42:20 +0000 (01:42 -0800)
committerAaron Schulz <aschulz@wikimedia.org>
Tue, 9 Dec 2014 22:43:01 +0000 (22:43 +0000)
* This also changes previews to render section edit tokens but
  remove them on output, avoiding cache fragmentation.
* Also shortened the resulting getStashKey() value.

Change-Id: Ic8fa87669106b960c76912b864788b781f6ee2e6

includes/EditPage.php
includes/api/ApiStashEdit.php
includes/parser/ParserOptions.php

index db2e442..993c20a 100644 (file)
@@ -3496,7 +3496,6 @@ HTML
                        }
 
                        $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
-                       $parserOptions->setEditSection( false );
                        $parserOptions->setIsPreview( true );
                        $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
 
@@ -3548,13 +3547,17 @@ HTML
                        # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
                        # But it's now deprecated, so never mind
 
-                       $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
-                       $parserOutput = $content->getParserOutput(
-                               $this->getArticle()->getTitle(),
-                               null,
-                               $parserOptions
+                       $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+                       $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
+
+                       # Try to stash the edit for the final submission step
+                       # @todo: different date format preferences cause cache misses
+                       ApiStashEdit::stashEditFromPreview(
+                               $this->getArticle(), $content, $pstContent,
+                               $parserOutput, $parserOptions, $parserOptions, wfTimestampNow()
                        );
 
+                       $parserOutput->setEditSectionTokens( false ); // no section edit links
                        $previewHTML = $parserOutput->getText();
                        $this->mParserOutput = $parserOutput;
                        $wgOut->addParserOutputMetadata( $parserOutput );
index 2bb0b5c..b32d332 100644 (file)
@@ -121,17 +121,10 @@ class ApiStashEdit extends ApiBase {
                }
 
                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
-                               );
+                       list( $stashInfo, $ttl ) = self::buildStashValue(
+                               $editInfo->pstContent, $editInfo->output, $editInfo->timestamp
+                       );
+                       if ( $stashInfo ) {
                                $ok = $wgMemc->set( $key, $stashInfo, $ttl );
                                if ( $ok ) {
                                        $status = 'stashed';
@@ -150,24 +143,69 @@ class ApiStashEdit extends ApiBase {
        }
 
        /**
-        * Get the temporary prepared edit stash key for a user
+        * Attempt to cache PST content and corresponding parser output in passing
         *
-        * @param Title $title
-        * @param Content $content
-        * @param User $user User to get parser options from
-        * @return string
+        * This method can be called when the output was already generated for other
+        * reasons. Parsing should not be done just to call this method, however.
+        * $pstOpts must be that of the user doing the edit preview. If $pOpts does
+        * not match the options of WikiPage::makeParserOptions( 'canonical' ), this
+        * will do nothing. Provided the values are cacheable, they will be stored
+        * in memcached so that final edit submission might make use of them.
+        *
+        * @param Article|WikiPage $page Page title
+        * @param Content $content Proposed page content
+        * @param Content $pstContent The result of preSaveTransform() on $content
+        * @param ParserOutput $pOut The result of getParserOutput() on $pstContent
+        * @param ParserOptions $pstOpts Options for $pstContent (MUST be for prospective author)
+        * @param ParserOptions $pOpts Options for $pOut
+        * @param string $timestamp TS_MW timestamp of parser output generation
+        * @return boolean Success
         */
-       protected static function getStashKey(
-               Title $title, Content $content, User $user
+       public static function stashEditFromPreview(
+               Page $page, Content $content, Content $pstContent, ParserOutput $pOut,
+               ParserOptions $pstOpts, ParserOptions $pOpts, $timestamp
        ) {
-               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
-               );
+               global $wgMemc;
+
+               // getIsPreview() controls parser function behavior that references things
+               // like user/revision that don't exists yet. The user/text should already
+               // be set correctly by callers, just double check the preview flag.
+               if ( !$pOpts->getIsPreview() ) {
+                       return false; // sanity
+               } elseif ( $pOpts->getIsSectionPreview() ) {
+                       return false; // short-circuit (need the full content)
+               }
+
+               // PST parser options are for the user (handles signatures, etc...)
+               $user = $pstOpts->getUser();
+               // Get a key based on the source text, format, and user preferences
+               $key = self::getStashKey( $page->getTitle(), $content, $user );
+
+               // Parser output options must match cannonical options.
+               // Treat some options as matching that are different but don't matter.
+               $canonicalPOpts = $page->makeParserOptions( 'canonical' );
+               $canonicalPOpts->setIsPreview( true ); // force match
+               $canonicalPOpts->setTimestamp( $pOpts->getTimestamp() ); // force match
+               if ( !$pOpts->matches( $canonicalPOpts ) ) {
+                       wfDebugLog( 'StashEdit', "Uncacheable preview output for key '$key' (options)." );
+                       return false;
+               }
+
+               // Build a value to cache with a proper TTL
+               list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp );
+               if ( !$stashInfo ) {
+                       wfDebugLog( 'StashEdit', "Uncacheable parser output for key '$key' (rev/TTL)." );
+                       return false;
+               }
+
+               $ok = $wgMemc->set( $key, $stashInfo, $ttl );
+               if ( !$ok ) {
+                       wfDebugLog( 'StashEdit', "Failed to cache preview parser output for key '$key'." );
+               } else {
+                       wfDebugLog( 'StashEdit', "Cached preview output for key '$key'." );
+               }
+
+               return $ok;
        }
 
        /**
@@ -253,6 +291,59 @@ class ApiStashEdit extends ApiBase {
                return $editInfo;
        }
 
+       /**
+        * Get the temporary prepared edit stash key for a user
+        *
+        * This key can be used for caching prepared edits provided:
+        *   - a) The $user was used for PST options
+        *   - b) The parser output was made from the PST using cannonical matching options
+        *
+        * @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 ) {
+               $hash = sha1( implode( ':', array(
+                       $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
+               ) ) );
+
+               return wfMemcKey( 'prepared-edit', md5( $title->getPrefixedDBkey() ), $hash );
+       }
+
+       /**
+        * Build a value to store in memcached based on the PST content and parser output
+        *
+        * This makes a simple version of WikiPage::prepareContentForEdit() as stash info
+        *
+        * @param Content $pstContent
+        * @param ParserOutput $parserOutput
+        * @param string $timestamp TS_MW
+        * @return array (stash info array, TTL in seconds) or (null, 0)
+        */
+       protected static function buildStashValue(
+               Content $pstContent, ParserOutput $parserOutput, $timestamp
+       ) {
+               // 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' => $pstContent,
+                               'output'     => $parserOutput,
+                               'timestamp'  => $timestamp
+                       );
+                       return array( $stashInfo, $ttl );
+               }
+
+               return array( null, 0 );
+       }
+
        public function getAllowedParams() {
                return array(
                        'title' => array(
index b570fa5..a61dbf0 100644 (file)
@@ -641,6 +641,7 @@ class ParserOptions {
 
                wfProfileIn( __METHOD__ );
 
+               // *UPDATE* ParserOptions::matches() if any of this changes as needed
                $this->mInterwikiMagic = $wgInterwikiMagic;
                $this->mAllowExternalImages = $wgAllowExternalImages;
                $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
@@ -666,6 +667,32 @@ class ParserOptions {
                wfProfileOut( __METHOD__ );
        }
 
+       /**
+        * Check if these options match that of another options set
+        *
+        * This ignores report limit settings that only affect HTML comments
+        *
+        * @return bool
+        * @since 1.25
+        */
+       public function matches( ParserOptions $other ) {
+               $fields = array_keys( get_class_vars( __CLASS__ ) );
+               $fields = array_diff( $fields, array(
+                       'mEnableLimitReport', // only effects HTML comments
+                       'onAccessCallback', // only used for ParserOutput option tracking
+               ) );
+               foreach ( $fields as $field ) {
+                       if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
+                               return false;
+                       }
+               }
+               // Check the object and lazy-loaded options
+               return (
+                       $this->mUserLang->getCode() === $other->mUserLang->getCode() &&
+                       $this->getDateFormat() === $other->getDateFormat()
+               );
+       }
+
        /**
         * Registers a callback for tracking which ParserOptions which are used.
         * This is a private API with the parser.