Merge "resources: Deprecate module "jquery.appear""
[lhc/web/wiklou.git] / includes / revisiondelete / RevDelList.php
index b555592..48604e1 100644 (file)
@@ -81,14 +81,13 @@ abstract class RevDelList extends RevisionListBase {
        public function areAnySuppressed() {
                $bit = $this->getSuppressBit();
 
-               // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
-               for ( $this->reset(); $this->current(); $this->next() ) {
-                       // @codingStandardsIgnoreEnd
-                       $item = $this->current();
+               /** @var $item RevDelItem */
+               foreach ( $this as $item ) {
                        if ( $item->getBits() & $bit ) {
                                return true;
                        }
                }
+
                return false;
        }
 
@@ -104,6 +103,8 @@ abstract class RevDelList extends RevisionListBase {
         * @since 1.23 Added 'perItemStatus' param
         */
        public function setVisibility( array $params ) {
+               $status = Status::newGood();
+
                $bitPars = $params['value'];
                $comment = $params['comment'];
                $perItemStatus = isset( $params['perItemStatus'] ) ? $params['perItemStatus'] : false;
@@ -113,9 +114,17 @@ abstract class RevDelList extends RevisionListBase {
                $dbw = wfGetDB( DB_MASTER );
                $this->res = $this->doQuery( $dbw );
 
+               $status->merge( $this->acquireItemLocks() );
+               if ( !$status->isGood() ) {
+                       return $status;
+               }
+
                $dbw->startAtomic( __METHOD__ );
+               $dbw->onTransactionResolution( function () {
+                       // Release locks on commit or error
+                       $this->releaseItemLocks();
+               } );
 
-               $status = Status::newGood();
                $missing = array_flip( $this->ids );
                $this->clearFileOps();
                $idsForLog = [];
@@ -125,11 +134,19 @@ abstract class RevDelList extends RevisionListBase {
                        $status->itemStatuses = [];
                }
 
-               // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
-               for ( $this->reset(); $this->current(); $this->next() ) {
-                       // @codingStandardsIgnoreEnd
-                       /** @var $item RevDelItem */
-                       $item = $this->current();
+               // For multi-item deletions, set the old/new bitfields in log_params such that "hid X"
+               // shows in logs if field X was hidden from ANY item and likewise for "unhid Y". Note the
+               // form does not let the same field get hidden and unhidden in different items at once.
+               $virtualOldBits = 0;
+               $virtualNewBits = 0;
+               $logType = 'delete';
+
+               // Will be filled with id => [old, new bits] information and
+               // passed to doPostCommitUpdates().
+               $visibilityChangeMap = [];
+
+               /** @var $item RevDelItem */
+               foreach ( $this as $item ) {
                        unset( $missing[$item->getId()] );
 
                        if ( $perItemStatus ) {
@@ -144,7 +161,8 @@ abstract class RevDelList extends RevisionListBase {
                        $newBits = RevisionDeleter::extractBitfield( $bitPars, $oldBits );
 
                        if ( $oldBits == $newBits ) {
-                               $itemStatus->warning( 'revdelete-no-change', $item->formatDate(), $item->formatTime() );
+                               $itemStatus->warning(
+                                       'revdelete-no-change', $item->formatDate(), $item->formatTime() );
                                $status->failCount++;
                                continue;
                        } elseif ( $oldBits == 0 && $newBits != 0 ) {
@@ -157,21 +175,21 @@ abstract class RevDelList extends RevisionListBase {
 
                        if ( $item->isHideCurrentOp( $newBits ) ) {
                                // Cannot hide current version text
-                               $itemStatus->error( 'revdelete-hide-current', $item->formatDate(), $item->formatTime() );
+                               $itemStatus->error(
+                                       'revdelete-hide-current', $item->formatDate(), $item->formatTime() );
                                $status->failCount++;
                                continue;
-                       }
-                       if ( !$item->canView() ) {
+                       } elseif ( !$item->canView() ) {
                                // Cannot access this revision
                                $msg = ( $opType == 'show' ) ?
                                        'revdelete-show-no-access' : 'revdelete-modify-no-access';
                                $itemStatus->error( $msg, $item->formatDate(), $item->formatTime() );
                                $status->failCount++;
                                continue;
-                       }
                        // Cannot just "hide from Sysops" without hiding any fields
-                       if ( $newBits == Revision::DELETED_RESTRICTED ) {
-                               $itemStatus->warning( 'revdelete-only-restricted', $item->formatDate(), $item->formatTime() );
+                       } elseif ( $newBits == Revision::DELETED_RESTRICTED ) {
+                               $itemStatus->warning(
+                                       'revdelete-only-restricted', $item->formatDate(), $item->formatTime() );
                                $status->failCount++;
                                continue;
                        }
@@ -181,14 +199,32 @@ abstract class RevDelList extends RevisionListBase {
 
                        if ( $ok ) {
                                $idsForLog[] = $item->getId();
+                               // If any item field was suppressed or unsupressed
+                               if ( ( $oldBits | $newBits ) & $this->getSuppressBit() ) {
+                                       $logType = 'suppress';
+                               }
+                               // Track which fields where (un)hidden for each item
+                               $addedBits = ( $oldBits ^ $newBits ) & $newBits;
+                               $removedBits = ( $oldBits ^ $newBits ) & $oldBits;
+                               $virtualNewBits |= $addedBits;
+                               $virtualOldBits |= $removedBits;
+
                                $status->successCount++;
                                if ( $item->getAuthorId() > 0 ) {
                                        $authorIds[] = $item->getAuthorId();
                                } elseif ( IP::isIPAddress( $item->getAuthorName() ) ) {
                                        $authorIPs[] = $item->getAuthorName();
                                }
+
+                               // Save the old and new bits in $visibilityChangeMap for
+                               // later use.
+                               $visibilityChangeMap[$item->getId()] = [
+                                       'oldBits' => $oldBits,
+                                       'newBits' => $newBits,
+                               ];
                        } else {
-                               $itemStatus->error( 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() );
+                               $itemStatus->error(
+                                       'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() );
                                $status->failCount++;
                        }
                }
@@ -204,7 +240,7 @@ abstract class RevDelList extends RevisionListBase {
                }
 
                if ( $status->successCount == 0 ) {
-                       $dbw->rollback( __METHOD__ );
+                       $dbw->endAtomic( __METHOD__ );
                        return $status;
                }
 
@@ -214,35 +250,60 @@ abstract class RevDelList extends RevisionListBase {
                // Move files, if there are any
                $status->merge( $this->doPreCommitUpdates() );
                if ( !$status->isOK() ) {
-                       // Fatal error, such as no configured archive directory
-                       $dbw->rollback( __METHOD__ );
+                       // Fatal error, such as no configured archive directory or I/O failures
+                       wfGetLBFactory()->rollbackMasterChanges( __METHOD__ );
                        return $status;
                }
 
                // Log it
-               // @FIXME: $newBits/$oldBits set in for loop, makes IDE warnings too
-               $this->updateLog( [
-                       'title' => $this->title,
-                       'count' => $successCount,
-                       'newBits' => $newBits,
-                       'oldBits' => $oldBits,
-                       'comment' => $comment,
-                       'ids' => $idsForLog,
-                       'authorIds' => $authorIds,
-                       'authorIPs' => $authorIPs
-               ] );
-
-               // Clear caches
-               $that = $this;
-               $dbw->onTransactionIdle( function() use ( $that ) {
-                       $that->doPostCommitUpdates();
-               } );
+               $this->updateLog(
+                       $logType,
+                       [
+                               'title' => $this->title,
+                               'count' => $successCount,
+                               'newBits' => $virtualNewBits,
+                               'oldBits' => $virtualOldBits,
+                               'comment' => $comment,
+                               'ids' => $idsForLog,
+                               'authorIds' => $authorIds,
+                               'authorIPs' => $authorIPs
+                       ]
+               );
+
+               // Clear caches after commit
+               DeferredUpdates::addCallableUpdate(
+                       function () use ( $visibilityChangeMap ) {
+                               $this->doPostCommitUpdates( $visibilityChangeMap );
+                       },
+                       DeferredUpdates::PRESEND,
+                       $dbw
+               );
 
                $dbw->endAtomic( __METHOD__ );
 
                return $status;
        }
 
+       final protected function acquireItemLocks() {
+               $status = Status::newGood();
+               /** @var $item RevDelItem */
+               foreach ( $this as $item ) {
+                       $status->merge( $item->lock() );
+               }
+
+               return $status;
+       }
+
+       final protected function releaseItemLocks() {
+               $status = Status::newGood();
+               /** @var $item RevDelItem */
+               foreach ( $this as $item ) {
+                       $status->merge( $item->unlock() );
+               }
+
+               return $status;
+       }
+
        /**
         * Reload the list data from the master DB. This can be done after setVisibility()
         * to allow $item->getHTML() to show the new data.
@@ -254,6 +315,7 @@ abstract class RevDelList extends RevisionListBase {
 
        /**
         * Record a log entry on the action
+        * @param string $logType One of (delete,suppress)
         * @param array $params Associative array of parameters:
         *     newBits:         The new value of the *_deleted bitfield
         *     oldBits:         The old value of the *_deleted bitfield.
@@ -264,18 +326,12 @@ abstract class RevDelList extends RevisionListBase {
         *     authorsIPs:      The array of the IP/anon user offenders
         * @throws MWException
         */
-       protected function updateLog( $params ) {
+       private function updateLog( $logType, $params ) {
                // Get the URL param's corresponding DB field
                $field = RevisionDeleter::getRelationType( $this->getType() );
                if ( !$field ) {
                        throw new MWException( "Bad log URL param type!" );
                }
-               // Put things hidden from sysops in the suppression log
-               if ( ( $params['newBits'] | $params['oldBits'] ) & $this->getSuppressBit() ) {
-                       $logType = 'suppress';
-               } else {
-                       $logType = 'delete';
-               }
                // Add params for affected page and ids
                $logParams = $this->getLogParams( $params );
                // Actually add the deletion log entry
@@ -335,9 +391,10 @@ abstract class RevDelList extends RevisionListBase {
        /**
         * A hook for setVisibility(): do any necessary updates post-commit.
         * STUB
+        * @param array [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ]
         * @return Status
         */
-       public function doPostCommitUpdates() {
+       public function doPostCommitUpdates( array $visibilityChangeMap ) {
                return Status::newGood();
        }