Merge "Clean up handling of 'infinity'"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 14 Apr 2015 18:57:16 +0000 (18:57 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 14 Apr 2015 18:57:16 +0000 (18:57 +0000)
1  2 
includes/Block.php
includes/Title.php
includes/api/ApiQueryLogEvents.php
includes/db/Database.php
includes/page/WikiPage.php
languages/Language.php

diff --combined includes/Block.php
@@@ -122,11 -122,7 +122,7 @@@ class Block 
                $this->mAuto = $auto;
                $this->isHardblock( !$anonOnly );
                $this->prevents( 'createaccount', $createAccount );
-               if ( $expiry == 'infinity' || $expiry == wfGetDB( DB_SLAVE )->getInfinity() ) {
-                       $this->mExpiry = 'infinity';
-               } else {
-                       $this->mExpiry = wfTimestamp( TS_MW, $expiry );
-               }
+               $this->mExpiry = wfGetDB( DB_SLAVE )->decodeExpiry( $expiry );
                $this->isAutoblocking( $enableAutoblock );
                $this->mHideName = $hideName;
                $this->prevents( 'sendemail', $blockEmail );
                $this->mParentBlockId = $row->ipb_parent_block_id;
  
                // I wish I didn't have to do this
-               $db = wfGetDB( DB_SLAVE );
-               if ( $row->ipb_expiry == $db->getInfinity() ) {
-                       $this->mExpiry = 'infinity';
-               } else {
-                       $this->mExpiry = wfTimestamp( TS_MW, $row->ipb_expiry );
-               }
+               $this->mExpiry = wfGetDB( DB_SLAVE )->decodeExpiry( $row->ipb_expiry );
  
                $this->isHardblock( !$row->ipb_anon_only );
                $this->isAutoblocking( $row->ipb_enable_autoblock );
                        $dbw = wfGetDB( DB_MASTER );
                }
  
 -              # Don't collide with expired blocks
 -              Block::purgeExpired();
 +              # Periodic purge via commit hooks
 +              if ( mt_rand( 0, 9 ) == 0 ) {
 +                      Block::purgeExpired();
 +              }
  
                $row = $this->getDatabaseArray();
                $row['ipb_id'] = $dbw->nextSequenceValue( "ipblocks_ipb_id_seq" );
  
 -              $dbw->insert(
 -                      'ipblocks',
 -                      $row,
 -                      __METHOD__,
 -                      array( 'IGNORE' )
 -              );
 +              $dbw->insert( 'ipblocks', $row, __METHOD__, array( 'IGNORE' ) );
                $affected = $dbw->affectedRows();
 +
 +              # Don't collide with expired blocks.
 +              # Do this after trying to insert to avoid pointless gap locks.
 +              if ( !$affected ) {
 +                      $dbw->delete( 'ipblocks',
 +                              array(
 +                                      'ipb_address' => $row['ipb_address'],
 +                                      'ipb_user' => $row['ipb_user'],
 +                                      'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() )
 +                              ),
 +                              __METHOD__
 +                      );
 +
 +                      $dbw->insert( 'ipblocks', $row, __METHOD__, array( 'IGNORE' ) );
 +                      $affected = $dbw->affectedRows();
 +              }
 +
                $this->mId = $dbw->insertId();
  
                if ( $affected ) {
diff --combined includes/Title.php
@@@ -29,6 -29,8 +29,6 @@@
   *       however, it does so inefficiently.
   * @note Consider using a TitleValue object instead. TitleValue is more lightweight
   *       and does not rely on global state or the database.
 - *
 - * @internal documentation reviewed 15 Mar 2010
   */
  class Title {
        /** @var MapCacheLRU */
         *   by a prefix.  If you want to force a specific namespace even if
         *   $text might begin with a namespace prefix, use makeTitle() or
         *   makeTitleSafe().
 -       * @throws MWException
 +       * @throws InvalidArgumentException
         * @return Title|null Title or null on an error.
         */
        public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
                if ( is_object( $text ) ) {
 -                      throw new MWException( 'Title::newFromText given an object' );
 +                      throw new InvalidArgumentException( '$text must be a string.' );
 +              } elseif ( !is_string( $text ) ) {
 +                      wfWarn( __METHOD__ . ': $text must be a string. This will throw an InvalidArgumentException in future.', 2 );
                }
  
                $cache = self::getTitleCache();
                                if ( $row['permission'] == 'autoconfirmed' ) {
                                        $row['permission'] = 'editsemiprotected'; // B/C
                                }
+                               $row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
                        }
                        $this->mTitleProtection = $row;
                }
         *        false.
         */
        public function getCascadeProtectionSources( $getPages = true ) {
-               global $wgContLang;
                $pagerestrictions = array();
  
                if ( $this->mCascadeSources !== null && $getPages ) {
                $now = wfTimestampNow();
  
                foreach ( $res as $row ) {
-                       $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW );
+                       $expiry = $dbr->decodeExpiry( $row->pr_expiry );
                        if ( $expiry > $now ) {
                                if ( $getPages ) {
                                        $page_id = $row->pr_page;
         *   restrictions from page table (pre 1.10)
         */
        public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
-               global $wgContLang;
                $dbr = wfGetDB( DB_SLAVE );
  
                $restrictionTypes = $this->getRestrictionTypes();
  
                foreach ( $restrictionTypes as $type ) {
                        $this->mRestrictions[$type] = array();
-                       $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW );
+                       $this->mRestrictionsExpiry[$type] = 'infinity';
                }
  
                $this->mCascadeRestriction = false;
  
                                // This code should be refactored, now that it's being used more generally,
                                // But I don't really see any harm in leaving it in Block for now -werdna
-                               $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW );
+                               $expiry = $dbr->decodeExpiry( $row->pr_expiry );
  
                                // Only apply the restrictions if they haven't expired!
                                if ( !$expiry || $expiry > $now ) {
         *   restrictions from page table (pre 1.10)
         */
        public function loadRestrictions( $oldFashionedRestrictions = null ) {
-               global $wgContLang;
                if ( !$this->mRestrictionsLoaded ) {
+                       $dbr = wfGetDB( DB_SLAVE );
                        if ( $this->exists() ) {
-                               $dbr = wfGetDB( DB_SLAVE );
                                $res = $dbr->select(
                                        'page_restrictions',
                                        array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ),
  
                                if ( $title_protection ) {
                                        $now = wfTimestampNow();
-                                       $expiry = $wgContLang->formatExpiry( $title_protection['expiry'], TS_MW );
+                                       $expiry = $dbr->decodeExpiry( $title_protection['expiry'] );
  
                                        if ( !$expiry || $expiry > $now ) {
                                                // Apply the restrictions
                                                $this->mTitleProtection = false;
                                        }
                                } else {
-                                       $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW );
+                                       $this->mRestrictionsExpiry['create'] = 'infinity';
                                }
                                $this->mRestrictionsLoaded = true;
                        }
        public function getEditNotices( $oldid = 0 ) {
                $notices = array();
  
 -              # Optional notices on a per-namespace and per-page basis
 +              // Optional notice for the entire namespace
                $editnotice_ns = 'editnotice-' . $this->getNamespace();
 -              $editnotice_ns_message = wfMessage( $editnotice_ns );
 -              if ( $editnotice_ns_message->exists() ) {
 -                      $notices[$editnotice_ns] = '<div class="mw-editnotice mw-editnotice-namespace ' .
 -                              Sanitizer::escapeClass( "mw-$editnotice_ns" ) . '">' .
 -                              $editnotice_ns_message->parseAsBlock() . '</div>';
 +              $msg = wfMessage( $editnotice_ns );
 +              if ( $msg->exists() ) {
 +                      $html = $msg->parseAsBlock();
 +                      // Edit notices may have complex logic, but output nothing (T91715)
 +                      if ( trim( $html ) !== '' ) {
 +                              $notices[$editnotice_ns] = Html::rawElement(
 +                                      'div',
 +                                      array( 'class' => array(
 +                                              'mw-editnotice',
 +                                              'mw-editnotice-namespace',
 +                                              Sanitizer::escapeClass( "mw-$editnotice_ns" )
 +                                      ) ),
 +                                      $html
 +                              );
 +                      }
                }
 +
                if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
 +                      // Optional notice for page itself and any parent page
                        $parts = explode( '/', $this->getDBkey() );
                        $editnotice_base = $editnotice_ns;
                        while ( count( $parts ) > 0 ) {
                                $editnotice_base .= '-' . array_shift( $parts );
 -                              $editnotice_base_msg = wfMessage( $editnotice_base );
 -                              if ( $editnotice_base_msg->exists() ) {
 -                                      $notices[$editnotice_base] = '<div class="mw-editnotice mw-editnotice-base ' .
 -                                              Sanitizer::escapeClass( "mw-$editnotice_base" ) . '">' .
 -                                              $editnotice_base_msg->parseAsBlock() . '</div>';
 +                              $msg = wfMessage( $editnotice_base );
 +                              if ( $msg->exists() ) {
 +                                      $html = $msg->parseAsBlock();
 +                                      if ( trim( $html ) !== '' ) {
 +                                              $notices[$editnotice_base] = Html::rawElement(
 +                                                      'div',
 +                                                      array( 'class' => array(
 +                                                              'mw-editnotice',
 +                                                              'mw-editnotice-base',
 +                                                              Sanitizer::escapeClass( "mw-$editnotice_base" )
 +                                                      ) ),
 +                                                      $html
 +                                              );
 +                                      }
                                }
                        }
                } else {
 -                      # Even if there are no subpages in namespace, we still don't want / in MW ns.
 +                      // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
                        $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() );
 -                      $editnoticeMsg = wfMessage( $editnoticeText );
 -                      if ( $editnoticeMsg->exists() ) {
 -                              $notices[$editnoticeText] = '<div class="mw-editnotice mw-editnotice-page ' .
 -                                      Sanitizer::escapeClass( "mw-$editnoticeText" ) . '">' .
 -                                      $editnoticeMsg->parseAsBlock() . '</div>';
 +                      $msg = wfMessage( $editnoticeText );
 +                      if ( $msg->exists() ) {
 +                              $html = $msg->parseAsBlock();
 +                              if ( trim( $html ) !== '' ) {
 +                                      $notices[$editnoticeText] = Html::rawElement(
 +                                              'div',
 +                                              array( 'class' => array(
 +                                                      'mw-editnotice',
 +                                                      'mw-editnotice-page',
 +                                                      Sanitizer::escapeClass( "mw-$editnoticeText" )
 +                                              ) ),
 +                                              $html
 +                                      );
 +                              }
                        }
                }
  
@@@ -322,7 -322,7 +322,7 @@@ class ApiQueryLogEvents extends ApiQuer
                                $vals2['flags'] = isset( $params[$flagsKey] ) ? $params[$flagsKey] : '';
  
                                // Indefinite blocks have no expiry time
-                               if ( SpecialBlock::parseExpiryInput( $params[$durationKey] ) !== wfGetDB( DB_SLAVE )->getInfinity() ) {
+                               if ( SpecialBlock::parseExpiryInput( $params[$durationKey] ) !== 'infinity' ) {
                                        $vals2['expiry'] = wfTimestamp( TS_ISO_8601,
                                                strtotime( $params[$durationKey], wfTimestamp( TS_UNIX, $ts ) ) );
                                }
                                                unset( $params[$idsKey] );
                                        }
                                        if ( isset( $params[$ofieldKey] ) ) {
 -                                              $params[] = $params[$ofieldKey];
 +                                              $params[] = 'ofield=' . $params[$ofieldKey];
                                                unset( $params[$ofieldKey] );
                                        }
                                        if ( isset( $params[$nfieldKey] ) ) {
 -                                              $params[] = $params[$nfieldKey];
 +                                              $params[] = 'nfield=' . $params[$nfieldKey];
                                                unset( $params[$nfieldKey] );
                                        }
                                }
diff --combined includes/db/Database.php
@@@ -1226,10 -1226,13 +1226,10 @@@ abstract class DatabaseBase implements 
         * @throws DBQueryError
         */
        public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
 -              # Ignore errors during error handling to avoid infinite recursion
 -              $ignore = $this->ignoreErrors( true );
                ++$this->mErrorCount;
  
 -              if ( $ignore || $tempIgnore ) {
 +              if ( $this->ignoreErrors() || $tempIgnore ) {
                        wfDebug( "SQL ERROR (ignored): $error\n" );
 -                      $this->ignoreErrors( $ignore );
                } else {
                        $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
                        wfLogDBError(
         * @return bool
         */
        public function deadlockLoop() {
 -              $this->begin( __METHOD__ );
                $args = func_get_args();
                $function = array_shift( $args );
 -              $oldIgnore = $this->ignoreErrors( true );
                $tries = self::DEADLOCK_TRIES;
 -
                if ( is_array( $function ) ) {
                        $fname = $function[0];
                } else {
                        $fname = $function;
                }
  
 -              do {
 -                      $retVal = call_user_func_array( $function, $args );
 -                      $error = $this->lastError();
 -                      $errno = $this->lastErrno();
 -                      $sql = $this->lastQuery();
 +              $this->begin( __METHOD__ );
  
 -                      if ( $errno ) {
 +              $e = null;
 +              do {
 +                      try {
 +                              $retVal = call_user_func_array( $function, $args );
 +                              break;
 +                      } catch ( DBQueryError $e ) {
 +                              $error = $this->lastError();
 +                              $errno = $this->lastErrno();
 +                              $sql = $this->lastQuery();
                                if ( $this->wasDeadlock() ) {
 -                                      # Retry
 +                                      // Retry after a randomized delay
                                        usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
                                } else {
 -                                      $this->reportQueryError( $error, $errno, $sql, $fname );
 +                                      // Throw the error back up
 +                                      throw $e;
                                }
                        }
 -              } while ( $this->wasDeadlock() && --$tries > 0 );
 -
 -              $this->ignoreErrors( $oldIgnore );
 +              } while ( --$tries > 0 );
  
                if ( $tries <= 0 ) {
 +                      // Too many deadlocks; give up
                        $this->rollback( __METHOD__ );
 -                      $this->reportQueryError( $error, $errno, $sql, $fname );
 -
 -                      return false;
 +                      throw $e;
                } else {
                        $this->commit( __METHOD__ );
  
                if ( !$this->mTrxLevel ) {
                        $this->begin( $fname );
                        $this->mTrxAutomatic = true;
 -                      $this->mTrxAutomaticAtomic = true;
 +                      // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
 +                      // in all changes being in one transaction to keep requests transactional.
 +                      if ( !$this->getFlag( DBO_TRX ) ) {
 +                              $this->mTrxAutomaticAtomic = true;
 +                      }
                }
  
                $this->mTrxAtomicLevels->push( $fname );
                        $this->runOnTransactionPreCommitCallbacks();
                        $this->doCommit( $fname );
                        if ( $this->mTrxDoneWrites ) {
 +                              $this->mDoneWrites = microtime( true );
                                $this->getTransactionProfiler()->transactionWritingOut(
                                        $this->mServer, $this->mDBname, $this->mTrxShortId );
                        }
                $this->runOnTransactionPreCommitCallbacks();
                $this->doCommit( $fname );
                if ( $this->mTrxDoneWrites ) {
 +                      $this->mDoneWrites = microtime( true );
                        $this->getTransactionProfiler()->transactionWritingOut(
                                $this->mServer, $this->mDBname, $this->mTrxShortId );
                }
         * @return string
         */
        public function decodeExpiry( $expiry, $format = TS_MW ) {
-               return ( $expiry == '' || $expiry == $this->getInfinity() )
+               return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
                        ? 'infinity'
                        : wfTimestamp( $format, $expiry );
        }
@@@ -31,6 -31,8 +31,6 @@@ interface Page 
   *
   * Some fields are public only for backwards-compatibility. Use accessors.
   * In the past, this class was part of Article.php and everything was public.
 - *
 - * @internal documentation reviewed 15 Mar 2010
   */
  class WikiPage implements Page, IDBAccessObject {
        // Constants for $mDataLoadedFrom and related
                        $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
                } elseif ( $from === self::READ_NORMAL ) {
                        $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle );
 -                      // Use a "last rev inserted" timestamp key to diminish the issue of slave lag.
 -                      // Note that DB also stores the master position in the session and checks it.
 -                      $touched = $this->getCachedLastEditTime();
 -                      if ( $touched ) { // key set
 -                              if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) {
 -                                      $from = self::READ_LATEST;
 -                                      $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
 -                              }
 +                      if ( !$data
 +                              && wfGetLB()->getServerCount() > 1
 +                              && wfGetLB()->hasOrMadeRecentMasterChanges()
 +                      ) {
 +                              $from = self::READ_LATEST;
 +                              $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
                        }
                } else {
                        // No idea from where the caller got this data, assume slave database.
                        return; // page doesn't exist or is missing page_latest info
                }
  
 -              // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the
 -              // latest changes committed. This is true even within REPEATABLE-READ transactions, where
 -              // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to
 -              // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row
 -              // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT.
 -              // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read.
 -              $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0;
 +              if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
 +                      // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always
 +                      // includes the latest changes committed. This is true even within REPEATABLE-READ
 +                      // transactions, where S1 normally only sees changes committed before the first S1
 +                      // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
 +                      // may not find it since a page row UPDATE and revision row INSERT by S2 may have
 +                      // happened after the first S1 SELECT.
 +                      // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read.
 +                      $flags = Revision::READ_LOCKING;
 +              } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
 +                      // Bug T93976: if page_latest was loaded from the master, fetch the
 +                      // revision from there as well, as it may not exist yet on a slave DB.
 +                      // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
 +                      $flags = Revision::READ_LATEST;
 +              } else {
 +                      $flags = 0;
 +              }
                $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
                if ( $revision ) { // sanity
                        $this->setLastEdit( $revision );
                }
        }
  
 -      /**
 -       * Get the cached timestamp for the last time the page changed.
 -       * This is only used to help handle slave lag by comparing to page_touched.
 -       * @return string MW timestamp
 -       */
 -      protected function getCachedLastEditTime() {
 -              global $wgMemc;
 -              $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
 -              return $wgMemc->get( $key );
 -      }
 -
 -      /**
 -       * Set the cached timestamp for the last time the page changed.
 -       * This is only used to help handle slave lag by comparing to page_touched.
 -       * @param string $timestamp
 -       * @return void
 -       */
 -      public function setCachedLastEditTime( $timestamp ) {
 -              global $wgMemc;
 -              $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
 -              $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60 * 15 );
 -      }
 -
        /**
         * Determine whether a page would be suitable for being counted as an
         * article in the site_stats table based on the title & its content
         * @return bool
         */
        public function doPurge() {
 -              global $wgUseSquid;
 -
                if ( !Hooks::run( 'ArticlePurge', array( &$this ) ) ) {
                        return false;
                }
  
 -              // Invalidate the cache
 -              $this->mTitle->invalidateCache();
 -
 -              if ( $wgUseSquid ) {
 -                      // Commit the transaction before the purge is sent
 -                      $dbw = wfGetDB( DB_MASTER );
 -                      $dbw->commit( __METHOD__ );
 -
 -                      // Send purge
 -                      $update = SquidUpdate::newSimplePurge( $this->mTitle );
 -                      $update->doUpdate();
 -              }
 +              $title = $this->mTitle;
 +              wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
 +                      global $wgUseSquid;
 +                      // Invalidate the cache in auto-commit mode
 +                      $title->invalidateCache();
 +                      if ( $wgUseSquid ) {
 +                              // Send purge now that page_touched update was committed above
 +                              $update = SquidUpdate::newSimplePurge( $title );
 +                              $update->doUpdate();
 +                      }
 +              } );
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
                        // @todo move this logic to MessageCache
 -
                        if ( $this->exists() ) {
                                // NOTE: use transclusion text for messages.
                                //       This is consistent with  MessageCache::getMsgFromNamespace()
  
                        MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
                }
 +
                return true;
        }
  
        ) {
                global $wgContentHandlerUseDB;
  
 +              // Assertion to try to catch T92046
 +              if ( (int)$revision->getId() === 0 ) {
 +                      throw new InvalidArgumentException(
 +                              __METHOD__ . ': Revision has ID ' . var_export( $revision->getId(), 1 )
 +                      );
 +              }
 +
                $content = $revision->getContent();
                $len = $content ? $content->getSize() : 0;
                $rt = $content ? $content->getUltimateRedirectTarget() : null;
                if ( $result ) {
                        $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
                        $this->setLastEdit( $revision );
 -                      $this->setCachedLastEditTime( $now );
                        $this->mLatest = $revision->getId();
                        $this->mIsRedirect = (bool)$rt;
                        // Update the LinkCache.
  
                $baseRevId = null;
                if ( $edittime && $sectionId !== 'new' ) {
 -                      $dbw = wfGetDB( DB_MASTER );
 -                      $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
 +                      $dbr = wfGetDB( DB_SLAVE );
 +                      $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
 +                      // Try the master if this thread may have just added it.
 +                      // This could be abstracted into a Revision method, but we don't want
 +                      // to encourage loading of revisions by timestamp.
 +                      if ( !$rev
 +                              && wfGetLB()->getServerCount() > 1
 +                              && wfGetLB()->hasOrMadeRecentMasterChanges()
 +                      ) {
 +                              $dbw = wfGetDB( DB_MASTER );
 +                              $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
 +                      }
                        if ( $rev ) {
                                $baseRevId = $rev->getId();
                        }
                        if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
                                $oldContent = $this->getContent();
                        } else {
 -                              // TODO: try DB_SLAVE first
 -                              $dbw = wfGetDB( DB_MASTER );
 -                              $rev = Revision::loadFromId( $dbw, $baseRevId );
 -
 +                              $rev = Revision::newFromId( $baseRevId );
                                if ( !$rev ) {
                                        wfDebug( __METHOD__ . " asked for bogus section (page: " .
                                                $this->getId() . "; section: $sectionId)\n" );
         * error will be returned. These two conditions are also possible with
         * auto-detection due to MediaWiki's performance-optimised locking strategy.
         *
 -       * @param bool|int $baseRevId The revision ID this edit was based off, if any
 +       * @param bool|int $baseRevId The revision ID this edit was based off, if any.
 +       *   This is not the parent revision ID, rather the revision ID for older
 +       *   content used as the source for a rollback, for example.
         * @param User $user The user doing the edit
         *
         * @throws MWException
         * error will be returned. These two conditions are also possible with
         * auto-detection due to MediaWiki's performance-optimised locking strategy.
         *
 -       * @param bool|int $baseRevId The revision ID this edit was based off, if any
 +       * @param bool|int $baseRevId The revision ID this edit was based off, if any.
 +       *   This is not the parent revision ID, rather the revision ID for older
 +       *   content used as the source for a rollback, for example.
         * @param User $user The user doing the edit
         * @param string $serialFormat Format for storing the content in the
         *   database.
                                $dbw->begin( __METHOD__ );
                                try {
  
 -                                      $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                                      $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
                                        $status->merge( $prepStatus );
  
                                        if ( !$status->isOK() ) {
                        $dbw->begin( __METHOD__ );
                        try {
  
 -                              $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                              $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
                                $status->merge( $prepStatus );
  
                                if ( !$status->isOK() ) {
                // Promote user to any groups they meet the criteria for
                $dbw->onTransactionIdle( function () use ( $user ) {
                        $user->addAutopromoteOnceGroups( 'onEdit' );
 +                      $user->addAutopromoteOnceGroups( 'onView' ); // b/c
                } );
  
                return $status;
                }
  
                // The edit may have already been prepared via api.php?action=stashedit
 -              $cachedEdit = $useCache && $wgAjaxEditStash
 +              $cachedEdit = $useCache && $wgAjaxEditStash && !$user->isAllowed( 'bot' )
                        ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
                        : false;
  
                $dbw = wfGetDB( DB_MASTER );
  
                foreach ( $restrictionTypes as $action ) {
-                       if ( !isset( $expiry[$action] ) ) {
-                               $expiry[$action] = $dbw->getInfinity();
+                       if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
+                               $expiry[$action] = 'infinity';
                        }
                        if ( !isset( $limit[$action] ) ) {
                                $limit[$action] = '';
         */
        protected function formatExpiry( $expiry ) {
                global $wgContLang;
-               $dbr = wfGetDB( DB_SLAVE );
  
-               $encodedExpiry = $dbr->encodeExpiry( $expiry );
-               if ( $encodedExpiry != 'infinity' ) {
+               if ( $expiry != 'infinity' ) {
                        return wfMessage(
                                'protect-expiring',
                                $wgContLang->timeanddate( $expiry, false, false ),
                }
  
                // Generate the edit summary if necessary
 -              $target = Revision::newFromId( $s->rev_id );
 +              $target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
                if ( empty( $summary ) ) {
                        if ( $from == '' ) { // no public user name
                                $summary = wfMessage( 'revertpage-nouser' );
         * Opportunistically enqueue link update jobs given fresh parser output if useful
         *
         * @param ParserOutput $parserOutput Current version page output
 -       * @return bool Whether a job was pushed
         * @since 1.25
         */
        public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
                if ( wfReadOnly() ) {
 -                      return false;
 +                      return;
 +              }
 +
 +              if ( !Hooks::run( 'OpportunisticLinksUpdate', array( $this, $this->mTitle, $parserOutput ) ) ) {
 +                      return;
                }
  
                if ( $this->mTitle->areRestrictionsCascading() ) {
                        $params = array();
                } else {
                        // If the inclusions are deterministic, the edit-triggered link jobs are enough
 -                      return false;
 +                      return;
                }
  
                // Check if the last link refresh was before page_touched
                        JobQueueGroup::singleton()->push( EnqueueJob::newFromLocalJobs(
                                new JobSpecification( 'refreshLinks', $params, array(), $this->mTitle )
                        ) );
 -                      return true;
 +                      return;
                }
  
 -              return false;
 +              return;
        }
  
        /**
diff --combined languages/Language.php
@@@ -2945,7 -2945,7 +2945,7 @@@ class Language 
                        }
  
                        // Break down Hangul syllables to grab the first jamo
 -                      $code = utf8ToCodepoint( $matches[1] );
 +                      $code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
                        if ( $code < 0xac00 || 0xd7a4 <= $code ) {
                                return $matches[1];
                        } elseif ( $code < 0xb098 ) {
         */
        function normalize( $s ) {
                global $wgAllUnicodeFixes;
 -              $s = UtfNormal::cleanUp( $s );
 +              $s = UtfNormal\Validator::cleanUp( $s );
                if ( $wgAllUnicodeFixes ) {
                        $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
                        $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
        /**
         * Decode an expiry (block, protection, etc) which has come from the DB
         *
-        * @todo FIXME: why are we returnings DBMS-dependent strings???
-        *
         * @param string $expiry Database expiry String
         * @param bool|int $format True to process using language functions, or TS_ constant
         *     to return the expiry in a given timestamp
+        * @param string $inifinity If $format is not true, use this string for infinite expiry
         * @return string
         * @since 1.18
         */
-       public function formatExpiry( $expiry, $format = true ) {
-               static $infinity;
-               if ( $infinity === null ) {
-                       $infinity = wfGetDB( DB_SLAVE )->getInfinity();
+       public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
+               static $dbInfinity;
+               if ( $dbInfinity === null ) {
+                       $dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
                }
  
-               if ( $expiry == '' || $expiry == $infinity ) {
+               if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
                        return $format === true
                                ? $this->getMessageFromDB( 'infiniteblock' )
                                : $infinity;