Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.68% covered (warning)
67.68%
67 / 99
50.00% covered (danger)
50.00%
6 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevDelRevisionList
68.37% covered (warning)
68.37%
67 / 98
50.00% covered (danger)
50.00%
6 / 12
39.74
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelationType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRestriction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRevdelConstant
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 suggestTarget
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 doQuery
43.33% covered (danger)
43.33%
13 / 30
0.00% covered (danger)
0.00%
0 / 1
12.55
 newItem
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 getCurrent
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 emitEvents
88.89% covered (warning)
88.89%
24 / 27
0.00% covered (danger)
0.00%
0 / 1
3.01
 doPreCommitUpdates
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 doPostCommitUpdates
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 * @ingroup RevisionDelete
6 */
7
8namespace MediaWiki\RevisionDelete;
9
10use InvalidArgumentException;
11use LogEntry;
12use MediaWiki\Cache\HTMLCacheUpdater;
13use MediaWiki\Context\IContextSource;
14use MediaWiki\DomainEvent\DomainEventDispatcher;
15use MediaWiki\HookContainer\HookContainer;
16use MediaWiki\HookContainer\HookRunner;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Page\Event\PageHistoryVisibilityChangedEvent;
19use MediaWiki\Page\PageIdentity;
20use MediaWiki\Page\ProperPageIdentity;
21use MediaWiki\Revision\RevisionRecord;
22use MediaWiki\Revision\RevisionStore;
23use MediaWiki\Status\Status;
24use MediaWiki\Title\Title;
25use Wikimedia\Rdbms\FakeResultWrapper;
26use Wikimedia\Rdbms\IResultWrapper;
27use Wikimedia\Rdbms\LBFactory;
28
29/**
30 * List for revision table items
31 *
32 * This will check both the 'revision' table for live revisions and the
33 * 'archive' table for traditionally-deleted revisions that have an
34 * ar_rev_id saved.
35 *
36 * See RevDelRevisionItem and RevDelArchivedRevisionItem for items.
37 */
38class RevDelRevisionList extends RevDelList {
39
40    /** @var LBFactory */
41    private $lbFactory;
42
43    /** @var HookRunner */
44    private $hookRunner;
45
46    /** @var HTMLCacheUpdater */
47    private $htmlCacheUpdater;
48
49    /** @var RevisionStore */
50    private $revisionStore;
51
52    /** @var int */
53    public $currentRevId;
54    private DomainEventDispatcher $eventDispatcher;
55
56    public function __construct(
57        IContextSource $context,
58        PageIdentity $page,
59        array $ids,
60        LBFactory $lbFactory,
61        HookContainer $hookContainer,
62        HTMLCacheUpdater $htmlCacheUpdater,
63        RevisionStore $revisionStore,
64        DomainEventDispatcher $eventDispatcher
65    ) {
66        parent::__construct( $context, $page, $ids, $lbFactory );
67        $this->lbFactory = $lbFactory;
68        $this->hookRunner = new HookRunner( $hookContainer );
69        $this->htmlCacheUpdater = $htmlCacheUpdater;
70        $this->revisionStore = $revisionStore;
71        $this->eventDispatcher = $eventDispatcher;
72    }
73
74    /** @inheritDoc */
75    public function getType() {
76        return 'revision';
77    }
78
79    /** @inheritDoc */
80    public static function getRelationType() {
81        return 'rev_id';
82    }
83
84    /** @inheritDoc */
85    public static function getRestriction() {
86        return 'deleterevision';
87    }
88
89    /** @inheritDoc */
90    public static function getRevdelConstant() {
91        return RevisionRecord::DELETED_TEXT;
92    }
93
94    /** @inheritDoc */
95    public static function suggestTarget( $target, array $ids ) {
96        $revisionRecord = MediaWikiServices::getInstance()
97            ->getRevisionLookup()
98            ->getRevisionById( $ids[0] );
99
100        if ( $revisionRecord ) {
101            return Title::newFromPageIdentity( $revisionRecord->getPage() );
102        }
103        return $target;
104    }
105
106    /**
107     * @param \Wikimedia\Rdbms\IReadableDatabase $db
108     * @return IResultWrapper
109     */
110    public function doQuery( $db ) {
111        $ids = array_map( 'intval', $this->ids );
112        $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db )
113            ->joinComment()
114            ->joinUser()
115            ->joinPage()
116            ->where( [ 'rev_page' => $this->page->getId(), 'rev_id' => $ids ] )
117            ->orderBy( 'rev_id', \Wikimedia\Rdbms\SelectQueryBuilder::SORT_DESC )
118            // workaround for MySQL bug (T104313)
119            ->useIndex( [ 'revision' => 'PRIMARY' ] );
120
121        MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQueryBuilder( $queryBuilder, 'revision' );
122
123        $live = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
124        if ( $live->numRows() >= count( $ids ) ) {
125            // All requested revisions are live, keeps things simple!
126            return $live;
127        }
128
129        $queryBuilder = $this->revisionStore->newArchiveSelectQueryBuilder( $db )
130            ->joinComment()
131            ->where( [ 'ar_rev_id' => $ids ] )
132            ->orderBy( 'ar_rev_id', \Wikimedia\Rdbms\SelectQueryBuilder::SORT_DESC );
133
134        MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQueryBuilder( $queryBuilder, 'archive' );
135
136        // Check if any requested revisions are available fully deleted.
137        $archived = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
138
139        if ( $archived->numRows() == 0 ) {
140            return $live;
141        } elseif ( $live->numRows() == 0 ) {
142            return $archived;
143        } else {
144            // Combine the two! Whee
145            $rows = [];
146            foreach ( $live as $row ) {
147                $rows[$row->rev_id] = $row;
148            }
149            foreach ( $archived as $row ) {
150                $rows[$row->ar_rev_id] = $row;
151            }
152            krsort( $rows );
153            return new FakeResultWrapper( array_values( $rows ) );
154        }
155    }
156
157    /** @inheritDoc */
158    public function newItem( $row ) {
159        if ( isset( $row->rev_id ) ) {
160            return new RevDelRevisionItem( $this, $row );
161        } elseif ( isset( $row->ar_rev_id ) ) {
162            return new RevDelArchivedRevisionItem( $this, $row, $this->lbFactory );
163        } else {
164            // This shouldn't happen. :)
165            throw new InvalidArgumentException( 'Invalid row type in RevDelRevisionList' );
166        }
167    }
168
169    /** @inheritDoc */
170    public function getCurrent() {
171        if ( $this->currentRevId === null ) {
172            $dbw = $this->lbFactory->getPrimaryDatabase();
173            $this->currentRevId = $dbw->newSelectQueryBuilder()
174                ->select( 'page_latest' )
175                ->from( 'page' )
176                ->where( [ 'page_namespace' => $this->page->getNamespace(), 'page_title' => $this->page->getDBkey() ] )
177                ->caller( __METHOD__ )->fetchField();
178        }
179        return $this->currentRevId;
180    }
181
182    /**
183     * @param array $bitPars See RevisionDeleter::extractBitfield
184     * @param array $visibilityChangeMap [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ]
185     * @param array $tags
186     * @param LogEntry $logEntry
187     * @param bool $suppressed
188     */
189    protected function emitEvents(
190        array $bitPars,
191        array $visibilityChangeMap,
192        array $tags,
193        LogEntry $logEntry,
194        bool $suppressed
195    ) {
196        // Figure out which bits got set, and which got unset.
197        $bitsSet = RevisionDeleter::extractBitfield( $bitPars, 0 );
198        $bitsUnset = RevisionRecord::SUPPRESSED_ALL &
199            ( ~ RevisionDeleter::extractBitfield( $bitPars, RevisionRecord::SUPPRESSED_ALL ) );
200
201        $page = $this->getPage();
202        $performer = $this->getUser();
203
204        // Hack: make sure we have a *proper* PageIdentity
205        if ( !$page instanceof ProperPageIdentity ) {
206            if ( !$page instanceof Title ) {
207                $page = Title::newFromPageIdentity( $page );
208            }
209
210            $page = $page->toPageIdentity();
211        }
212
213        $flags = [
214            PageHistoryVisibilityChangedEvent::FLAG_SUPPRESSED => $suppressed
215        ];
216
217        $this->eventDispatcher->dispatch(
218            new PageHistoryVisibilityChangedEvent(
219                $page,
220                $performer,
221                $this->getCurrent(),
222                $bitsSet,
223                $bitsUnset,
224                $visibilityChangeMap,
225                $logEntry->getComment(),
226                $tags,
227                $flags,
228                $logEntry->getTimestamp()
229            ),
230            $this->lbFactory
231        );
232    }
233
234    /** @inheritDoc */
235    public function doPreCommitUpdates() {
236        Title::newFromPageIdentity( $this->page )->invalidateCache();
237        return Status::newGood();
238    }
239
240    /** @inheritDoc */
241    public function doPostCommitUpdates( array $visibilityChangeMap ) {
242        $this->htmlCacheUpdater->purgeTitleUrls(
243            $this->page,
244            HTMLCacheUpdater::PURGE_INTENT_TXROUND_REFLECTED
245        );
246        // Extensions that require referencing previous revisions may need this
247        $this->hookRunner->onArticleRevisionVisibilitySet(
248            Title::newFromPageIdentity( $this->page ),
249            $this->ids,
250            $visibilityChangeMap
251        );
252
253        return Status::newGood();
254    }
255}
256
257/** @deprecated class alias since 1.46 */
258class_alias( RevDelRevisionList::class, 'RevDelRevisionList' );