Merge "Make WikiPage::doDeleteArticle more robust"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 29 Aug 2016 23:28:12 +0000 (23:28 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 29 Aug 2016 23:28:12 +0000 (23:28 +0000)
1  2 
docs/hooks.txt
includes/page/WikiPage.php

diff --combined docs/hooks.txt
@@@ -297,6 -297,16 +297,6 @@@ After a user account is created
  $user: the User object that was created. (Parameter added in 1.7)
  $byEmail: true when account was created "by email" (added in 1.12)
  
 -'AddNewAccountApiForm': Allow modifying internal login form when creating an
 -account via API.
 -$apiModule: the ApiCreateAccount module calling
 -$loginForm: the LoginForm used
 -
 -'AddNewAccountApiResult': Modify API output when creating a new account via API.
 -$apiModule: the ApiCreateAccount module calling
 -$loginForm: the LoginForm used
 -&$result: associative array for API result data
 -
  'AfterBuildFeedLinks': Executed in OutputPage.php after all feed links (atom, rss,...)
  are created. Can be used to omit specific feeds from being outputted. You must not use
  this hook to add feeds, use OutputPage::addFeedLink() instead.
@@@ -596,9 -606,8 +596,9 @@@ $outputPage: OutputPage that can be use
  &$user: the user that deleted the article
  $reason: the reason the article was deleted
  $id: id of the article that was deleted
- $content: the Content of the deleted page
+ $content: the Content of the deleted page (or null, when deleting a broken page)
  $logEntry: the ManualLogEntry used to record the deletion
 +$archivedRevisionCount: the number of revisions archived during the deletion
  
  'ArticleEditUpdateNewTalk': Before updating user_newtalk when a user talk page
  was changed.
@@@ -674,10 -683,6 +674,10 @@@ $oldPageID: the page ID of the revisio
  revisions of an article.
  $title: Title object of the article
  $ids: Ids to set the visibility for
 +$visibilityChangeMap: Map of revision id to oldBits and newBits.  This array can be
 +  examined to determine exactly what visibility bits have changed for each
 +  revision.  This array is of the form
 +  [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ]
  
  'ArticleRollbackComplete': After an article rollback is completed.
  $wikiPage: the WikiPage that was edited
@@@ -894,7 -899,6 +894,7 @@@ $image: Fil
  'BlockIpComplete': After an IP address or user is blocked.
  $block: the Block object that was saved
  $user: the user who did the block (not the one being blocked)
 +$priorBlock: the Block object for the prior block or null if there was none
  
  'BookInformation': Before information output on Special:Booksources.
  $isbn: ISBN to show information for
@@@ -1094,9 -1098,6 +1094,9 @@@ $row: the DB row for this lin
  $id: User identifier
  $title: User page title
  &$tools: Array of tool links
 +$specialPage: SpecialPage instance for context and services. Can be either
 +  SpecialContributions or DeletedContributionsPage. Extensions should type
 +  hint against a generic SpecialPage though.
  
  'ConvertContent': Called by AbstractContent::convert when a conversion to
  another content model is requested.
@@@ -1135,85 -1136,6 +1135,85 @@@ $page: SpecialPage object for DeletedCo
  $row: the DB row for this line
  &$classes: the classes to add to the surrounding <li>
  
 +'DifferenceEngineMarkPatrolledLink': Allows extensions to change the "mark as patrolled" link
 +which is shown both on the diff header as well as on the bottom of a page, usually
 +wrapped in a span element which has class="patrollink".
 +$differenceEngine: DifferenceEngine object
 +&$markAsPatrolledLink: The "mark as patrolled" link HTML (string)
 +$rcid: Recent change ID (rc_id) for this change (int)
 +$token: Patrol token; $rcid is used in generating this variable
 +
 +'DifferenceEngineMarkPatrolledRCID': Allows extensions to possibly change the rcid parameter.
 +For example the rcid might be set to zero due to the user being the same as the
 +performer of the change but an extension might still want to show it under certain
 +conditions.
 +&$rcid: rc_id (int) of the change or 0
 +$differenceEngine: DifferenceEngine object
 +$change: RecentChange object
 +$user: User object representing the current user
 +
 +'DifferenceEngineNewHeader': Allows extensions to change the $newHeader variable, which
 +contains information about the new revision, such as the revision's author, whether
 +the revision was marked as a minor edit or not, etc.
 +$differenceEngine: DifferenceEngine object
 +&$newHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
 +include things like revision author info, revision comment, RevisionDelete link and more
 +$formattedRevisionTools: Array containing revision tools, some of which may have
 +been injected with the DiffRevisionTools hook
 +$nextlink: String containing the link to the next revision (if any); also included in $newHeader
 +$rollback: Rollback link (string) to roll this revision back to the previous one, if any
 +$newminor: String indicating if the new revision was marked as a minor edit
 +$diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
 +whether we should show just the diff; passed in as a query string parameter to the
 +various URLs constructed here (i.e. $nextlink)
 +$rdel: RevisionDelete link for the new revision, if the current user is allowed
 +to use the RevisionDelete feature
 +$unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
 +
 +'DifferenceEngineOldHeader': Allows extensions to change the $oldHeader variable, which
 +contains information about the old revision, such as the revision's author, whether
 +the revision was marked as a minor edit or not, etc.
 +$differenceEngine: DifferenceEngine object
 +&$oldHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
 +include things like revision author info, revision comment, RevisionDelete link and more
 +$prevlink: String containing the link to the previous revision (if any); also included in $oldHeader
 +$oldminor: String indicating if the old revision was marked as a minor edit
 +$diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
 +whether we should show just the diff; passed in as a query string parameter to the
 +various URLs constructed here (i.e. $prevlink)
 +$ldel: RevisionDelete link for the old revision, if the current user is allowed
 +to use the RevisionDelete feature
 +$unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
 +
 +'DifferenceEngineOldHeaderNoOldRev': Change the $oldHeader variable in cases when
 +there is no old revision
 +&$oldHeader: empty string by default
 +
 +'DifferenceEngineRenderRevisionAddParserOutput': Allows extensions to change the parser output.
 +Return false to not add parser output via OutputPage's addParserOutput method.
 +$differenceEngine: DifferenceEngine object
 +$out: OutputPage object
 +$parserOutput: ParserOutput object
 +$wikiPage: WikiPage object
 +
 +DifferenceEngineRenderRevisionShowFinalPatrolLink': An extension can hook into this hook
 +point and return false to not show the final "mark as patrolled" link on the bottom
 +of a page.
 +This hook has no arguments.
 +
 +'DifferenceEngineShowDiff': Allows extensions to affect the diff text which
 +eventually gets sent to the OutputPage object.
 +$differenceEngine: DifferenceEngine object
 +
 +'DifferenceEngineShowEmptyOldContent': Allows extensions to change the diff table
 +body (without header) in cases when there is no old revision or the old and new
 +revisions are identical.
 +$differenceEngine: DifferenceEngine object
 +
 +'DifferenceEngineShowDiffPage': Add additional output via the available OutputPage
 +object into the diff view
 +$out: OutputPage object
 +
  'DiffRevisionTools': Override or extend the revision tools available from the
  diff view, i.e. undo, etc.
  $newRev: Revision object of the "new" revision
@@@ -1937,8 -1859,8 +1937,8 @@@ LinkRenderer, before processing starts
  processing and return $ret.
  $linkRenderer: the LinkRenderer object
  $target: the LinkTarget that the link is pointing to
 -&$html: the contents that the <a> tag should have (raw HTML); null means
 -  "default".
 +&$text: the contents that the <a> tag should have; either a plain, unescaped
 +  string or a HtmlArmor object; null means "default".
  &$customAttribs: the HTML attributes that the <a> tag should have, in
    associative array form, with keys and values unescaped.  Should be merged
    with default values, with a value of false meaning to suppress the
@@@ -1955,8 -1877,7 +1955,8 @@@ return false, $ret will be returned
  $linkRenderer: the LinkRenderer object
  $target: the LinkTarget object that the link is pointing to
  $isKnown: boolean indicating whether the page is known or not
 -&$html: the final (raw HTML) contents of the <a> tag, after processing.
 +&$text: the contents that the <a> tag should have; either a plain, unescaped
 +  string or a HtmlArmor object.
  &$attribs: the final HTML attributes of the <a> tag, after processing, in
    associative array form.
  &$ret: the value to return if your hook returns false.
@@@ -2060,6 -1981,13 +2060,6 @@@ $e: The exception (in case of a plain o
  $suppressed: true if the error was suppressed via
    error_reporting()/wfSuppressWarnings()
  
 -'LoginAuthenticateAudit': A login attempt for a valid user account either
 -succeeded or failed. No return data is accepted; this hook is for auditing only.
 -$user: the User object being authenticated against
 -$password: the password being submitted and found wanting
 -$retval: a LoginForm class constant with authenticateUserData() return
 -  value (SUCCESS, WRONG_PASS, etc.).
 -
  'LoginFormValidErrorMessages': Called in LoginForm when a function gets valid
  error messages. Allows to add additional error messages (except messages already
  in LoginForm::$validErrorMessages).
@@@ -2427,12 -2355,24 +2427,12 @@@ cache or return false to not use it
  &$parser: Parser object
  &$varCache: variable cache (array)
  
 -'ParserLimitReport': DEPRECATED! Use ParserLimitReportPrepare and
 -ParserLimitReportFormat instead.
 +'ParserLimitReport': DEPRECATED! Use ParserLimitReportPrepare instead.
  Called at the end of Parser:parse() when the parser will
  include comments about size of the text parsed.
  $parser: Parser object
  &$limitReport: text that will be included (without comment tags)
  
 -'ParserLimitReportFormat': Called for each row in the parser limit report that
 -needs formatting. If nothing handles this hook, the default is to use "$key" to
 -get the label, and "$key-value" or "$key-value-text"/"$key-value-html" to
 -format the value.
 -$key: Key for the limit report item (string)
 -&$value: Value of the limit report item
 -&$report: String onto which to append the data
 -$isHTML: If true, $report is an HTML table with two columns; if false, it's
 -  text intended for display in a monospaced font.
 -$localize: If false, $report should be output in English.
 -
  'ParserLimitReportPrepare': Called at the end of Parser:parse() when the parser
  will include comments about size of the text parsed. Hooks should use
  $output->setLimitReportData() to populate data. Functions for this hook should
@@@ -2687,18 -2627,6 +2687,18 @@@ search results
  $title: Current Title object being displayed in search results.
  &$id: Revision ID (default is false, for latest)
  
 +'SearchIndexFields': Add fields to search index mapping.
 +&$fields: Array of fields, all implement SearchIndexField
 +$engine: SearchEngine instance for which mapping is being built.
 +
 +'SearchDataForIndex': Add data to search document. Allows to add any data to
 +the field map used to index the document.
 +&$fields: Array of name => value pairs for fields
 +$handler: ContentHandler for the content being indexed
 +$page: WikiPage that is being indexed
 +$output: ParserOutput that is produced from the page
 +$engine: SearchEngine for which the indexing is intended
 +
  'SecondaryDataUpdates': Allows modification of the list of DataUpdates to
  perform when page content is modified. Currently called by
  AbstractContent::getSecondaryDataUpdates.
@@@ -3357,20 -3285,8 +3357,20 @@@ added to the descripto
  &$radio: Boolean, if source type should be shown as radio button
  $selectedSourceType: The selected source type
  
 -'UploadVerification': Additional chances to reject an uploaded file. Consider
 -using UploadVerifyFile instead.
 +'UploadStashFile': Before a file is stashed (uploaded to stash).
 +Note that code which has not been updated for MediaWiki 1.28 may not call this
 +hook. If your extension absolutely, positively must prevent some files from
 +being uploaded, use UploadVerifyFile or UploadVerifyUpload.
 +$upload: (object) An instance of UploadBase, with all info about the upload
 +$user: (object) An instance of User, the user uploading this file
 +$props: (array) File properties, as returned by FSFile::getPropsFromPath()
 +&$error: output: If the file stashing should be prevented, set this to the reason
 +  in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
 +  instance (you might want to use ApiMessage to provide machine-readable details
 +  for the API).
 +
 +'UploadVerification': DEPRECATED! Use UploadVerifyFile instead.
 +Additional chances to reject an uploaded file.
  $saveName: (string) destination file name
  $tempName: (string) filesystem path to the temporary file for checks
  &$error: (string) output: message key for message to show if upload canceled by
@@@ -3382,23 -3298,9 +3382,23 @@@ in most cases over UploadVerification
  $upload: (object) an instance of UploadBase, with all info about the upload
  $mime: (string) The uploaded file's MIME type, as detected by MediaWiki.
    Handlers will typically only apply for specific MIME types.
 -&$error: (object) output: true if the file is valid. Otherwise, an indexed array
 -  representing the problem with the file, where the first element is the message
 -  key and the remaining elements are used as parameters to the message.
 +&$error: (object) output: true if the file is valid. Otherwise, set this to the reason
 +  in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
 +  instance (you might want to use ApiMessage to provide machine-readable details
 +  for the API).
 +
 +'UploadVerifyUpload': Upload verification, based on both file properties like
 +MIME type (same as UploadVerifyFile) and the information entered by the user
 +(upload comment, file page contents etc.).
 +$upload: (object) An instance of UploadBase, with all info about the upload
 +$user: (object) An instance of User, the user uploading this file
 +$props: (array) File properties, as returned by FSFile::getPropsFromPath()
 +$comment: (string) Upload log comment (also used as edit summary)
 +$pageText: (string) File description page text (only used for new uploads)
 +&$error: output: If the file upload should be prevented, set this to the reason
 +  in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
 +  instance (you might want to use ApiMessage to provide machine-readable details
 +  for the API).
  
  'UserIsBot': when determining whether a user is a bot account
  $user: the user
@@@ -3546,9 -3448,6 +3546,9 @@@ $user: User object for the logged-in us
  For functionality that needs to run after any login (API or web) use UserLoggedIn.
  &$user: the user object that was created on login
  &$inject_html: Any HTML to inject after the "logged in" message.
 +$direct: (bool) The hook is called directly after a successful login. This will only happen once
 +  per login. A UserLoginComplete call with direct=false can happen when the user visits the login
 +  page while already logged in.
  
  'UserLoginForm': DEPRECATED! Create an AuthenticationProvider instead.
  Manipulate the login form.
@@@ -3650,16 -3549,6 +3650,16 @@@ $userId: User id of the current use
  $userText: User name of the current user
  &$items: Array of user tool links as HTML fragments
  
 +'UsersPagerDoBatchLookups': Called in UsersPager::doBatchLookups() to give
 +extensions providing user group data from an alternate source a chance to add
 +their data into the cache array so that things like global user groups are
 +displayed correctly in Special:ListUsers.
 +$dbr: Read-only database handle
 +$userIds: Array of user IDs whose groups we should look up
 +&$cache: Array of user ID -> internal user group name (e.g. 'sysop') mappings
 +&$groups: Array of group name -> bool true mappings for members of a given user
 +group
 +
  'ValidateExtendedMetadataCache': Called to validate the cached metadata in
  FormatMetadata::getExtendedMeta (return false means cache will be
  invalidated and GetExtendedMetadata hook called again).
@@@ -3732,7 -3621,8 +3732,8 @@@ a page is deleted. Called in WikiPage::
  specific to a content model should be provided by the respective Content's
  getDeletionUpdates() method.
  $page: the WikiPage
- $content: the Content to generate updates for
+ $content: the Content to generate updates for (or null, if the Content could not be loaded
+ due to an error)
  &$updates: the array of DataUpdate objects. Hook function may want to add to it.
  
  'XmlDumpWriterOpenPage': Called at the end of XmlDumpWriter::openPage, to allow
@@@ -20,8 -20,6 +20,8 @@@
   * @file
   */
  
 +use \MediaWiki\Logger\LoggerFactory;
 +
  /**
   * Class representing a MediaWiki article and history.
   *
@@@ -90,14 -88,6 +90,14 @@@ class WikiPage implements Page, IDBAcce
                $this->mTitle = $title;
        }
  
 +      /**
 +       * Makes sure that the mTitle object is cloned
 +       * to the newly cloned WikiPage.
 +       */
 +      public function __clone() {
 +              $this->mTitle = clone $this->mTitle;
 +      }
 +
        /**
         * Create a WikiPage object of the appropriate class for the given title.
         *
  
        /**
         * Loads page_touched and returns a value indicating if it should be used
 -       * @return bool True if not a redirect
 +       * @return bool True if this page exists and is not a redirect
         */
        public function checkTouched() {
                if ( !$this->mDataLoaded ) {
                        $this->loadPageData();
                }
 -              return !$this->mIsRedirect;
 +              return ( $this->mId && !$this->mIsRedirect );
        }
  
        /**
                // Update the DB post-send if the page has not cached since now
                $that = $this;
                $latest = $this->getLatest();
 -              DeferredUpdates::addCallableUpdate( function() use ( $that, $retval, $latest ) {
 -                      $that->insertRedirectEntry( $retval, $latest );
 -              } );
 +              DeferredUpdates::addCallableUpdate(
 +                      function () use ( $that, $retval, $latest ) {
 +                              $that->insertRedirectEntry( $retval, $latest );
 +                      },
 +                      DeferredUpdates::POSTSEND,
 +                      wfGetDB( DB_MASTER )
 +              );
  
                return $retval;
        }
         *
         * @since 1.19
         * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
 -       * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
 -       *   get the current revision (default value)
 -       *
 -       * @return ParserOutput|bool ParserOutput or false if the revision was not found
 +       * @param null|int      $oldid Revision ID to get the text from, passing null or 0 will
 +       *                             get the current revision (default value)
 +       * @param bool          $forceParse Force reindexing, regardless of cache settings
 +       * @return bool|ParserOutput ParserOutput or false if the revision was not found
         */
 -      public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
 -
 -              $useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid );
 +      public function getParserOutput(
 +              ParserOptions $parserOptions, $oldid = null, $forceParse = false
 +      ) {
 +              $useParserCache =
 +                      ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
                wfDebug( __METHOD__ .
                        ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
                if ( $parserOptions->getStubThreshold() ) {
                        return false;
                }
  
 -              $title = $this->mTitle;
 -              wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
 -                      // Invalidate the cache in auto-commit mode
 -                      $title->invalidateCache();
 -              } );
 -
 +              $this->mTitle->invalidateCache();
                // Send purge after above page_touched update was committed
                DeferredUpdates::addUpdate(
 -                      new CdnCacheUpdate( $title->getCdnUrls() ),
 +                      new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
                        DeferredUpdates::PRESEND
                );
  
         * @param IDatabase $dbw
         * @param int|null $pageId Custom page ID that will be used for the insert statement
         *
 -       * @return bool|int The newly created page_id key; false if the title already existed
 +       * @return bool|int The newly created page_id key; false if the row was not
 +       *   inserted, e.g. because the title already existed or because the specified
 +       *   page ID is already in use.
         */
        public function insertOn( $dbw, $pageId = null ) {
                $pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
                        $revisionId = $revision->insertOn( $dbw );
                        // Update page_latest and friends to reflect the new revision
                        if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
 -                              $dbw->rollback( __METHOD__ ); // sanity; this should never happen
                                throw new MWException( "Failed to update page row to use new revision." );
                        }
  
                }
  
                // Do secondary updates once the main changes have been committed...
 -              $that = $this;
 -              $dbw->onTransactionIdle(
 -                      function () use (
 -                              $dbw, &$that, $revision, &$user, $content, $summary, &$flags,
 -                              $changed, $meta, &$status
 -                      ) {
 -                              // Do per-page updates in a transaction
 -                              $dbw->setFlag( DBO_TRX );
 -                              // Update links tables, site stats, etc.
 -                              $that->doEditUpdates(
 -                                      $revision,
 -                                      $user,
 -                                      [
 -                                              'changed' => $changed,
 -                                              'oldcountable' => $meta['oldCountable'],
 -                                              'oldrevision' => $meta['oldRevision']
 -                                      ]
 -                              );
 -                              // Trigger post-save hook
 -                              $params = [ &$that, &$user, $content, $summary, $flags & EDIT_MINOR,
 -                                      null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
 -                              ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
 -                              Hooks::run( 'PageContentSaveComplete', $params );
 -                      }
 +              DeferredUpdates::addUpdate(
 +                      new AtomicSectionUpdate(
 +                              $dbw,
 +                              __METHOD__,
 +                              function () use (
 +                                      $revision, &$user, $content, $summary, &$flags,
 +                                      $changed, $meta, &$status
 +                              ) {
 +                                      // Update links tables, site stats, etc.
 +                                      $this->doEditUpdates(
 +                                              $revision,
 +                                              $user,
 +                                              [
 +                                                      'changed' => $changed,
 +                                                      'oldcountable' => $meta['oldCountable'],
 +                                                      'oldrevision' => $meta['oldRevision']
 +                                              ]
 +                                      );
 +                                      // Trigger post-save hook
 +                                      $params = [ &$this, &$user, $content, $summary, $flags & EDIT_MINOR,
 +                                              null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
 +                                      ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
 +                                      Hooks::run( 'PageContentSaveComplete', $params );
 +                              }
 +                      ),
 +                      DeferredUpdates::PRESEND
                );
  
                return $status;
                $revisionId = $revision->insertOn( $dbw );
                // Update the page record with revision data
                if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
 -                      $dbw->rollback( __METHOD__ ); // sanity; this should never happen
                        throw new MWException( "Failed to update page row to use new revision." );
                }
  
                $status->value['revision'] = $revision;
  
                // Do secondary updates once the main changes have been committed...
 -              $that = $this;
 -              $dbw->onTransactionIdle(
 -                      function () use (
 -                              &$that, $dbw, $revision, &$user, $content, $summary, &$flags, $meta, &$status
 -                      ) {
 -                              // Do per-page updates in a transaction
 -                              $dbw->setFlag( DBO_TRX );
 -                              // Update links, etc.
 -                              $that->doEditUpdates( $revision, $user, [ 'created' => true ] );
 -                              // Trigger post-create hook
 -                              $params = [ &$that, &$user, $content, $summary,
 -                                      $flags & EDIT_MINOR, null, null, &$flags, $revision ];
 -                              ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params );
 -                              Hooks::run( 'PageContentInsertComplete', $params );
 -                              // Trigger post-save hook
 -                              $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
 -                              ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
 -                              Hooks::run( 'PageContentSaveComplete', $params );
 +              DeferredUpdates::addUpdate(
 +                      new AtomicSectionUpdate(
 +                              $dbw,
 +                              __METHOD__,
 +                              function () use (
 +                                      $revision, &$user, $content, $summary, &$flags, $meta, &$status
 +                              ) {
 +                                      // Update links, etc.
 +                                      $this->doEditUpdates( $revision, $user, [ 'created' => true ] );
 +                                      // Trigger post-create hook
 +                                      $params = [ &$this, &$user, $content, $summary,
 +                                              $flags & EDIT_MINOR, null, null, &$flags, $revision ];
 +                                      ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params );
 +                                      Hooks::run( 'PageContentInsertComplete', $params );
 +                                      // Trigger post-save hook
 +                                      $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
 +                                      ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
 +                                      Hooks::run( 'PageContentSaveComplete', $params );
  
 -                      }
 +                              }
 +                      ),
 +                      DeferredUpdates::PRESEND
                );
  
                return $status;
                }
  
                if ( $this->mPreparedEdit
 -                      && $this->mPreparedEdit->newContent
 +                      && isset( $this->mPreparedEdit->newContent )
                        && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
                        && $this->mPreparedEdit->format == $serialFormat
                                                }
                                        }
                                );
 +                      } else {
 +                              // Try to avoid a second parse if {{REVISIONID}} is used
 +                              $edit->popts->setSpeculativeRevIdCallback( function () {
 +                                      return 1 + (int)wfGetDB( DB_MASTER )->selectField(
 +                                              'revision',
 +                                              'MAX(rev_id)',
 +                                              [],
 +                                              __METHOD__
 +                                      );
 +                              } );
                        }
                        $edit->output = $edit->pstContent
                                ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
                ];
                $content = $revision->getContent();
  
 +              $logger = LoggerFactory::getInstance( 'SaveParse' );
 +
                // See if the parser output before $revision was inserted is still valid
                $editInfo = false;
                if ( !$this->mPreparedEdit ) {
 -                      wfDebug( __METHOD__ . ": No prepared edit...\n" );
 +                      $logger->debug( __METHOD__ . ": No prepared edit...\n" );
                } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
 -                      wfDebug( __METHOD__ . ": Prepared edit has vary-revision...\n" );
 +                      $logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
 +              } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
 +                      && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
 +              ) {
 +                      $logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
                } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
 -                      wfDebug( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
 +                      $logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
                } else {
                        wfDebug( __METHOD__ . ": Using prepared edit...\n" );
                        $editInfo = $this->mPreparedEdit;
                        return $status;
                }
  
 +              // Given the lock above, we can be confident in the title and page ID values
 +              $namespace = $this->getTitle()->getNamespace();
 +              $dbKey = $this->getTitle()->getDBkey();
 +
                // At this point we are now comitted to returning an OK
                // status unless some DB query error or other exception comes up.
                // This way callers don't have to call rollback() if $status is bad
                // unless they actually try to catch exceptions (which is rare).
  
                // we need to remember the old content so we can use it to generate all deletion updates.
-               $content = $this->getContent( Revision::RAW );
+               try {
+                       $content = $this->getContent( Revision::RAW );
+               } catch ( Exception $ex ) {
+                       wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
+                               . $ex->getMessage() );
+                       $content = null;
+               }
  
                // Bitfields to further suppress the content
                if ( $suppress ) {
                        $bitfield = 'rev_deleted';
                }
  
 -              /**
 -               * For now, shunt the revision data into the archive table.
 -               * Text is *not* removed from the text table; bulk storage
 -               * is left intact to avoid breaking block-compression or
 -               * immutable storage schemes.
 -               *
 -               * For backwards compatibility, note that some older archive
 -               * table entries will have ar_text and ar_flags fields still.
 -               *
 -               * In the future, we may keep revisions and mark them with
 -               * the rev_deleted field, which is reserved for this purpose.
 -               */
 -
 -              $row = [
 -                      'ar_namespace'  => 'page_namespace',
 -                      'ar_title'      => 'page_title',
 -                      'ar_comment'    => 'rev_comment',
 -                      'ar_user'       => 'rev_user',
 -                      'ar_user_text'  => 'rev_user_text',
 -                      'ar_timestamp'  => 'rev_timestamp',
 -                      'ar_minor_edit' => 'rev_minor_edit',
 -                      'ar_rev_id'     => 'rev_id',
 -                      'ar_parent_id'  => 'rev_parent_id',
 -                      'ar_text_id'    => 'rev_text_id',
 -                      'ar_text'       => '\'\'', // Be explicit to appease
 -                      'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 -                      'ar_len'        => 'rev_len',
 -                      'ar_page_id'    => 'page_id',
 -                      'ar_deleted'    => $bitfield,
 -                      'ar_sha1'       => 'rev_sha1',
 -              ];
 +              // For now, shunt the revision data into the archive table.
 +              // Text is *not* removed from the text table; bulk storage
 +              // is left intact to avoid breaking block-compression or
 +              // immutable storage schemes.
 +              // In the future, we may keep revisions and mark them with
 +              // the rev_deleted field, which is reserved for this purpose.
  
 -              if ( $wgContentHandlerUseDB ) {
 -                      $row['ar_content_model'] = 'rev_content_model';
 -                      $row['ar_content_format'] = 'rev_content_format';
 +              // Get all of the page revisions
 +              $res = $dbw->select(
 +                      'revision',
 +                      Revision::selectFields(),
 +                      [ 'rev_page' => $id ],
 +                      __METHOD__,
 +                      'FOR UPDATE'
 +              );
 +              // Build their equivalent archive rows
 +              $rowsInsert = [];
 +              foreach ( $res as $row ) {
 +                      $rowInsert = [
 +                              'ar_namespace'  => $namespace,
 +                              'ar_title'      => $dbKey,
 +                              'ar_comment'    => $row->rev_comment,
 +                              'ar_user'       => $row->rev_user,
 +                              'ar_user_text'  => $row->rev_user_text,
 +                              'ar_timestamp'  => $row->rev_timestamp,
 +                              'ar_minor_edit' => $row->rev_minor_edit,
 +                              'ar_rev_id'     => $row->rev_id,
 +                              'ar_parent_id'  => $row->rev_parent_id,
 +                              'ar_text_id'    => $row->rev_text_id,
 +                              'ar_text'       => '',
 +                              'ar_flags'      => '',
 +                              'ar_len'        => $row->rev_len,
 +                              'ar_page_id'    => $id,
 +                              'ar_deleted'    => $bitfield,
 +                              'ar_sha1'       => $row->rev_sha1,
 +                      ];
 +                      if ( $wgContentHandlerUseDB ) {
 +                              $rowInsert['ar_content_model'] = $row->rev_content_model;
 +                              $rowInsert['ar_content_format'] = $row->rev_content_format;
 +                      }
 +                      $rowsInsert[] = $rowInsert;
                }
 +              // Copy them into the archive table
 +              $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
 +              // Save this so we can pass it to the ArticleDeleteComplete hook.
 +              $archivedRevisionCount = $dbw->affectedRows();
  
 -              // Copy all the page revisions into the archive table
 -              $dbw->insertSelect(
 -                      'archive',
 -                      [ 'page', 'revision' ],
 -                      $row,
 -                      [
 -                              'page_id' => $id,
 -                              'page_id = rev_page'
 -                      ],
 -                      __METHOD__
 -              );
 +              // Clone the title and wikiPage, so we have the information we need when
 +              // we log and run the ArticleDeleteComplete hook.
 +              $logTitle = clone $this->mTitle;
 +              $wikiPageBeforeDelete = clone $this;
  
                // Now that it's safely backed up, delete it
                $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
                        $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
                }
  
 -              // Clone the title, so we have the information we need when we log
 -              $logTitle = clone $this->mTitle;
 -
                // Log the deletion, if the page was suppressed, put it in the suppression log instead
                $logtype = $suppress ? 'suppress' : 'delete';
  
  
                $this->doDeleteUpdates( $id, $content );
  
 -              Hooks::run( 'ArticleDeleteComplete',
 -                      [ &$this, &$user, $reason, $id, $content, $logEntry ] );
 +              Hooks::run( 'ArticleDeleteComplete', [
 +                      &$wikiPageBeforeDelete,
 +                      &$user,
 +                      $reason,
 +                      $id,
 +                      $content,
 +                      $logEntry,
 +                      $archivedRevisionCount
 +              ] );
                $status->value = $logid;
  
                // Show log excerpt on 404 pages rather than just a link
         *   may already return null when the page proper was deleted.
         */
        public function doDeleteUpdates( $id, Content $content = null ) {
+               try {
+                       $countable = $this->isCountable();
+               } catch ( Exception $ex ) {
+                       // fallback for deleting broken pages for which we cannot load the content for
+                       // some reason. Note that doDeleteArticleReal() already logged this problem.
+                       $countable = false;
+               }
                // Update site status
-               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
+               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
  
                // Delete pagelinks, update secondary indexes, etc
                $updates = $this->getDeletionUpdates( $content );
                $title->touchLinks();
                $title->purgeSquid();
                $title->deleteTitleProtection();
 +
 +              if ( $title->getNamespace() == NS_CATEGORY ) {
 +                      // Load the Category object, which will schedule a job to create
 +                      // the category table row if necessary. Checking a slave is ok
 +                      // here, in the worst case it'll run an unnecessary recount job on
 +                      // a category that probably doesn't have many members.
 +                      Category::newFromTitle( $title )->getID();
 +              }
        }
  
        /**
                                        $cat = Category::newFromName( $catName );
                                        Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
                                }
 +
 +                              // Refresh counts on categories that should be empty now, to
 +                              // trigger possible deletion. Check master for the most
 +                              // up-to-date cat_pages.
 +                              if ( count( $deleted ) ) {
 +                                      $rows = $dbw->select(
 +                                              'category',
 +                                              [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
 +                                              [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
 +                                              $method
 +                                      );
 +                                      foreach ( $rows as $row ) {
 +                                              $cat = Category::newFromRow( $row );
 +                                              $cat->refreshCounts();
 +                                      }
 +                              }
                        }
                );
        }
                if ( !$content ) {
                        // load content object, which may be used to determine the necessary updates.
                        // XXX: the content may not be needed to determine the updates.
-                       $content = $this->getContent( Revision::RAW );
+                       try {
+                               $content = $this->getContent( Revision::RAW );
+                       } catch ( Exception $ex ) {
+                               // If we can't load the content, something is wrong. Perhaps that's why
+                               // the user is trying to delete the page, so let's not fail in that case.
+                               // Note that doDeleteArticleReal() will already have logged an issue with
+                               // loading the content.
+                       }
                }
  
                if ( !$content ) {