Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 165
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevDelList
0.00% covered (danger)
0.00%
0 / 165
0.00% covered (danger)
0.00%
0 / 16
1722
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getRelationType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 areAnySuppressed
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 setVisibility
0.00% covered (danger)
0.00%
0 / 114
0.00% covered (danger)
0.00%
0 / 1
420
 acquireItemLocks
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 releaseItemLocks
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 reloadFromPrimary
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 updateLog
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 getLogAction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLogParams
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 clearFileOps
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doPreCommitUpdates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doPostCommitUpdates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup RevisionDelete
20 */
21
22use MediaWiki\Context\IContextSource;
23use MediaWiki\Deferred\DeferredUpdates;
24use MediaWiki\Page\PageIdentity;
25use MediaWiki\Revision\RevisionRecord;
26use MediaWiki\RevisionList\RevisionListBase;
27use MediaWiki\Status\Status;
28use MediaWiki\Title\Title;
29use Wikimedia\Rdbms\LBFactory;
30
31/**
32 * Abstract base class for a list of deletable items. The list class
33 * needs to be able to make a query from a set of identifiers to pull
34 * relevant rows, to return RevDelItem subclasses wrapping them, and
35 * to wrap bulk update operations.
36 *
37 * @property RevDelItem $current
38 * @method RevDelItem next()
39 * @method RevDelItem reset()
40 * @method RevDelItem current()
41 */
42abstract class RevDelList extends RevisionListBase {
43
44    /** Flag used for suppression, depending on the type of log */
45    protected const SUPPRESS_BIT = RevisionRecord::DELETED_RESTRICTED;
46
47    /** @var LBFactory */
48    private $lbFactory;
49
50    /**
51     * @param IContextSource $context
52     * @param PageIdentity $page
53     * @param array $ids
54     * @param LBFactory $lbFactory
55     */
56    public function __construct(
57        IContextSource $context,
58        PageIdentity $page,
59        array $ids,
60        LBFactory $lbFactory
61    ) {
62        parent::__construct( $context, $page );
63
64        // ids is a protected variable in RevisionListBase
65        $this->ids = $ids;
66        $this->lbFactory = $lbFactory;
67    }
68
69    /**
70     * Get the DB field name associated with the ID list.
71     * This used to populate the log_search table for finding log entries.
72     * Override this function.
73     * @return string|null
74     */
75    public static function getRelationType() {
76        return null;
77    }
78
79    /**
80     * Get the user right required for this list type
81     * Override this function.
82     * @since 1.22
83     * @return string|null
84     */
85    public static function getRestriction() {
86        return null;
87    }
88
89    /**
90     * Get the revision deletion constant for this list type
91     * Override this function.
92     * @since 1.22
93     * @return int|null
94     */
95    public static function getRevdelConstant() {
96        return null;
97    }
98
99    /**
100     * Suggest a target for the revision deletion
101     * Optionally override this function.
102     * @since 1.22
103     * @param Title|null $target User-supplied target
104     * @param array $ids
105     * @return Title|null
106     */
107    public static function suggestTarget( $target, array $ids ) {
108        return $target;
109    }
110
111    /**
112     * Indicate whether any item in this list is suppressed
113     * @since 1.25
114     * @return bool
115     */
116    public function areAnySuppressed() {
117        /** @var RevDelItem $item */
118        foreach ( $this as $item ) {
119            if ( $item->getBits() & self::SUPPRESS_BIT ) {
120                return true;
121            }
122        }
123
124        return false;
125    }
126
127    /**
128     * Set the visibility for the revisions in this list. Logging and
129     * transactions are done here.
130     *
131     * @param array $params Associative array of parameters. Members are:
132     *     value:         ExtractBitParams() bitfield array
133     *     comment:       The log comment
134     *     perItemStatus: Set if you want per-item status reports
135     *     tags:          The array of change tags to apply to the log entry
136     * @return Status
137     * @since 1.23 Added 'perItemStatus' param
138     */
139    public function setVisibility( array $params ) {
140        $status = Status::newGood();
141
142        $bitPars = $params['value'];
143        $comment = $params['comment'];
144        $perItemStatus = $params['perItemStatus'] ?? false;
145
146        // CAS-style checks are done on the _deleted fields so the select
147        // does not need to use FOR UPDATE nor be in the atomic section
148        $dbw = $this->lbFactory->getPrimaryDatabase();
149        $this->res = $this->doQuery( $dbw );
150
151        $status->merge( $this->acquireItemLocks() );
152        if ( !$status->isGood() ) {
153            return $status;
154        }
155
156        $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
157        $dbw->onTransactionResolution(
158            function () {
159                // Release locks on commit or error
160                $this->releaseItemLocks();
161            },
162            __METHOD__
163        );
164
165        $missing = array_fill_keys( $this->ids, true );
166        $this->clearFileOps();
167        $idsForLog = [];
168        $authorActors = [];
169
170        if ( $perItemStatus ) {
171            $status->value['itemStatuses'] = [];
172        }
173
174        // For multi-item deletions, set the old/new bitfields in log_params such that "hid X"
175        // shows in logs if field X was hidden from ANY item and likewise for "unhid Y". Note the
176        // form does not let the same field get hidden and unhidden in different items at once.
177        $virtualOldBits = 0;
178        $virtualNewBits = 0;
179        $logType = 'delete';
180
181        // Will be filled with id => [old, new bits] information and
182        // passed to doPostCommitUpdates().
183        $visibilityChangeMap = [];
184
185        /** @var RevDelItem $item */
186        foreach ( $this as $item ) {
187            unset( $missing[$item->getId()] );
188
189            if ( $perItemStatus ) {
190                $itemStatus = Status::newGood();
191                $status->value['itemStatuses'][$item->getId()] = $itemStatus;
192            } else {
193                $itemStatus = $status;
194            }
195
196            $oldBits = $item->getBits();
197            // Build the actual new rev_deleted bitfield
198            $newBits = RevisionDeleter::extractBitfield( $bitPars, $oldBits );
199
200            if ( $oldBits == $newBits ) {
201                $itemStatus->warning(
202                    'revdelete-no-change', $item->formatDate(), $item->formatTime() );
203                $status->failCount++;
204                continue;
205            } elseif ( $oldBits == 0 && $newBits != 0 ) {
206                $opType = 'hide';
207            } elseif ( $oldBits != 0 && $newBits == 0 ) {
208                $opType = 'show';
209            } else {
210                $opType = 'modify';
211            }
212
213            if ( $item->isHideCurrentOp( $newBits ) ) {
214                // Cannot hide current version text
215                $itemStatus->error(
216                    'revdelete-hide-current', $item->formatDate(), $item->formatTime() );
217                $status->failCount++;
218                continue;
219            } elseif ( !$item->canView() ) {
220                // Cannot access this revision
221                $msg = ( $opType == 'show' ) ?
222                    'revdelete-show-no-access' : 'revdelete-modify-no-access';
223                $itemStatus->error( $msg, $item->formatDate(), $item->formatTime() );
224                $status->failCount++;
225                continue;
226            // Cannot just "hide from Sysops" without hiding any fields
227            } elseif ( $newBits == self::SUPPRESS_BIT ) {
228                $itemStatus->warning(
229                    'revdelete-only-restricted', $item->formatDate(), $item->formatTime() );
230                $status->failCount++;
231                continue;
232            }
233
234            // Update the revision
235            $ok = $item->setBits( $newBits );
236
237            if ( $ok ) {
238                $idsForLog[] = $item->getId();
239                // If any item field was suppressed or unsuppressed
240                if ( ( $oldBits | $newBits ) & self::SUPPRESS_BIT ) {
241                    $logType = 'suppress';
242                }
243                // Track which fields where (un)hidden for each item
244                $addedBits = ( $oldBits ^ $newBits ) & $newBits;
245                $removedBits = ( $oldBits ^ $newBits ) & $oldBits;
246                $virtualNewBits |= $addedBits;
247                $virtualOldBits |= $removedBits;
248
249                $status->successCount++;
250                $authorActors[] = $item->getAuthorActor();
251
252                // Save the old and new bits in $visibilityChangeMap for
253                // later use.
254                $visibilityChangeMap[$item->getId()] = [
255                    'oldBits' => $oldBits,
256                    'newBits' => $newBits,
257                ];
258            } else {
259                $itemStatus->error(
260                    'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() );
261                $status->failCount++;
262            }
263        }
264
265        // Handle missing revisions
266        foreach ( $missing as $id => $unused ) {
267            if ( $perItemStatus ) {
268                $status->value['itemStatuses'][$id] = Status::newFatal( 'revdelete-modify-missing', $id );
269            } else {
270                $status->error( 'revdelete-modify-missing', $id );
271            }
272            $status->failCount++;
273        }
274
275        if ( $status->successCount == 0 ) {
276            $dbw->endAtomic( __METHOD__ );
277            return $status;
278        }
279
280        // Save success count
281        $successCount = $status->successCount;
282
283        // Move files, if there are any
284        $status->merge( $this->doPreCommitUpdates() );
285        if ( !$status->isOK() ) {
286            // Fatal error, such as no configured archive directory or I/O failures
287            $dbw->cancelAtomic( __METHOD__ );
288            return $status;
289        }
290
291        // Log it
292        $authorFields = [];
293        $authorFields['authorActors'] = $authorActors;
294        $this->updateLog(
295            $logType,
296            [
297                'page' => $this->page,
298                'count' => $successCount,
299                'newBits' => $virtualNewBits,
300                'oldBits' => $virtualOldBits,
301                'comment' => $comment,
302                'ids' => $idsForLog,
303                'tags' => $params['tags'] ?? [],
304            ] + $authorFields
305        );
306
307        // Clear caches after commit
308        DeferredUpdates::addCallableUpdate(
309            function () use ( $visibilityChangeMap ) {
310                $this->doPostCommitUpdates( $visibilityChangeMap );
311            },
312            DeferredUpdates::PRESEND,
313            $dbw
314        );
315
316        $dbw->endAtomic( __METHOD__ );
317
318        return $status;
319    }
320
321    final protected function acquireItemLocks() {
322        $status = Status::newGood();
323        /** @var RevDelItem $item */
324        foreach ( $this as $item ) {
325            $status->merge( $item->lock() );
326        }
327
328        return $status;
329    }
330
331    final protected function releaseItemLocks() {
332        $status = Status::newGood();
333        /** @var RevDelItem $item */
334        foreach ( $this as $item ) {
335            $status->merge( $item->unlock() );
336        }
337
338        return $status;
339    }
340
341    /**
342     * Reload the list data from the primary DB. This can be done after setVisibility()
343     * to allow $item->getHTML() to show the new data.
344     * @since 1.37
345     */
346    public function reloadFromPrimary() {
347        $dbw = $this->lbFactory->getPrimaryDatabase();
348        $this->res = $this->doQuery( $dbw );
349    }
350
351    /**
352     * Record a log entry on the action
353     * @param string $logType One of (delete,suppress)
354     * @param array $params Associative array of parameters:
355     *     newBits:         The new value of the *_deleted bitfield
356     *     oldBits:         The old value of the *_deleted bitfield.
357     *     page:            The target page reference
358     *     ids:             The ID list
359     *     comment:         The log comment
360     *     authorActors:    The array of the actor IDs of the offenders
361     *     tags:            The array of change tags to apply to the log entry
362     */
363    private function updateLog( $logType, $params ) {
364        // Get the URL param's corresponding DB field
365        $field = RevisionDeleter::getRelationType( $this->getType() );
366        if ( !$field ) {
367            throw new UnexpectedValueException( "Bad log URL param type!" );
368        }
369        // Add params for affected page and ids
370        $logParams = $this->getLogParams( $params );
371        // Actually add the deletion log entry
372        $logEntry = new ManualLogEntry( $logType, $this->getLogAction() );
373        $logEntry->setTarget( $params['page'] );
374        $logEntry->setComment( $params['comment'] );
375        $logEntry->setParameters( $logParams );
376        $logEntry->setPerformer( $this->getUser() );
377        // Allow for easy searching of deletion log items for revision/log items
378        $relations = [
379            $field => $params['ids'],
380        ];
381        if ( isset( $params['authorActors'] ) ) {
382            $relations += [
383                'target_author_actor' => $params['authorActors'],
384            ];
385        }
386        $logEntry->setRelations( $relations );
387        // Apply change tags to the log entry
388        $logEntry->addTags( $params['tags'] );
389        $logId = $logEntry->insert();
390        $logEntry->publish( $logId );
391    }
392
393    /**
394     * Get the log action for this list type
395     * @return string
396     */
397    public function getLogAction() {
398        return 'revision';
399    }
400
401    /**
402     * Get log parameter array.
403     * @param array $params Associative array of log parameters, same as updateLog()
404     * @return array
405     */
406    public function getLogParams( $params ) {
407        return [
408            '4::type' => $this->getType(),
409            '5::ids' => $params['ids'],
410            '6::ofield' => $params['oldBits'],
411            '7::nfield' => $params['newBits'],
412        ];
413    }
414
415    /**
416     * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates()
417     * STUB
418     */
419    public function clearFileOps() {
420    }
421
422    /**
423     * A hook for setVisibility(): do batch updates pre-commit.
424     * STUB
425     * @return Status
426     */
427    public function doPreCommitUpdates() {
428        return Status::newGood();
429    }
430
431    /**
432     * A hook for setVisibility(): do any necessary updates post-commit.
433     * STUB
434     * @param array $visibilityChangeMap [id => ['oldBits' => $oldBits, 'newBits' => $newBits], ... ]
435     * @return Status
436     */
437    public function doPostCommitUpdates( array $visibilityChangeMap ) {
438        return Status::newGood();
439    }
440
441}