Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.84% covered (warning)
69.84%
44 / 63
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
LinksDeletionUpdate
69.84% covered (warning)
69.84%
44 / 63
0.00% covered (danger)
0.00%
0 / 3
11.22
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
4.13
 doIncrementalUpdate
82.05% covered (warning)
82.05%
32 / 39
0.00% covered (danger)
0.00%
0 / 1
4.09
 getAsJobSpecification
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Updater for link tracking tables after a page edit.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Deferred\LinksUpdate;
10
11use InvalidArgumentException;
12use MediaWiki\Category\Category;
13use MediaWiki\Deferred\DeferredUpdates;
14use MediaWiki\Deferred\EnqueueableDataUpdate;
15use MediaWiki\JobQueue\JobSpecification;
16use MediaWiki\MainConfigNames;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Page\PageIdentity;
19use MediaWiki\Page\PageIdentityValue;
20use MediaWiki\Parser\ParserOutput;
21use MediaWiki\RecentChanges\RecentChange;
22
23/**
24 * Update object handling the cleanup of links tables after a page was deleted.
25 */
26class LinksDeletionUpdate extends LinksUpdate implements EnqueueableDataUpdate {
27    /** @var string */
28    protected $timestamp;
29
30    /**
31     * @param PageIdentity $page Page we are updating
32     * @param int|null $pageId ID of the page we are updating [optional]
33     * @param string|null $timestamp TS::MW timestamp of deletion
34     */
35    public function __construct( PageIdentity $page, $pageId = null, $timestamp = null ) {
36        if ( $pageId ) {
37            $this->mId = $pageId; // page ID at time of deletion
38        } elseif ( $page->exists() ) {
39            $this->mId = $page->getId();
40        } else {
41            throw new InvalidArgumentException( "Page ID not known. Page doesn't exist?" );
42        }
43
44        $this->timestamp = $timestamp ?: wfTimestampNow();
45
46        $fakePO = new ParserOutput();
47        $fakePO->setCacheTime( $timestamp );
48        // Use an immutable page identity to keep reference to the page id at time of deletion - T299244
49        $immutablePageIdentity = new PageIdentityValue(
50            $page->getId(),
51            $page->getNamespace(),
52            $page->getDBkey(),
53            $page->getWikiId()
54        );
55        parent::__construct( $immutablePageIdentity, $fakePO, false );
56    }
57
58    protected function doIncrementalUpdate() {
59        $services = MediaWikiServices::getInstance();
60        $config = $services->getMainConfig();
61        $dbProvider = $services->getConnectionProvider();
62        $batchSize = $config->get( MainConfigNames::UpdateRowsPerQuery );
63
64        $id = $this->mId;
65        $title = $this->mTitle;
66
67        $dbw = $this->getDB(); // convenience
68
69        parent::doIncrementalUpdate();
70
71        // Typically, a category is empty when deleted, so check that we don't leave
72        // spurious row in the category table.
73        if ( $title->getNamespace() === NS_CATEGORY ) {
74            // T166757: do the update after the main job DB commit
75            DeferredUpdates::addCallableUpdate( static function () use ( $title ) {
76                $cat = Category::newFromName( $title->getDBkey() );
77                $cat->refreshCountsIfSmall();
78            } );
79        }
80
81        // Delete restrictions for the deleted page
82        $dbw->newDeleteQueryBuilder()
83            ->deleteFrom( 'page_restrictions' )
84            ->where( [ 'pr_page' => $id ] )
85            ->caller( __METHOD__ )->execute();
86
87        // Delete any redirect entry
88        $dbw->newDeleteQueryBuilder()
89            ->deleteFrom( 'redirect' )
90            ->where( [ 'rd_from' => $id ] )
91            ->caller( __METHOD__ )->execute();
92
93        // Find recentchanges entries to clean up...
94        // Select RC IDs just by curid, and not by title (see T307865 and T140960)
95        $rcIdsForPage = $dbw->newSelectQueryBuilder()
96            ->select( 'rc_id' )
97            ->from( 'recentchanges' )
98            ->where( [
99                $dbw->expr( 'rc_source', '!=', RecentChange::SRC_LOG ),
100                'rc_cur_id' => $id
101            ] )
102            ->caller( __METHOD__ )->fetchFieldValues();
103
104        // T98706: delete by PK to avoid lock contention with RC delete log insertions
105        $rcIdBatches = array_chunk( $rcIdsForPage, $batchSize );
106        foreach ( $rcIdBatches as $rcIdBatch ) {
107            $dbw->newDeleteQueryBuilder()
108                ->deleteFrom( 'recentchanges' )
109                ->where( [ 'rc_id' => $rcIdBatch ] )
110                ->caller( __METHOD__ )->execute();
111            if ( count( $rcIdBatches ) > 1 ) {
112                $dbProvider->commitAndWaitForReplication(
113                    __METHOD__, $this->ticket, [ 'domain' => $dbw->getDomainID() ]
114                );
115            }
116        }
117    }
118
119    /** @inheritDoc */
120    public function getAsJobSpecification() {
121        return [
122            'domain' => $this->getDB()->getDomainID(),
123            'job' => new JobSpecification(
124                'deleteLinks',
125                [ 'pageId' => $this->mId, 'timestamp' => $this->timestamp ],
126                [ 'removeDuplicates' => true ],
127                $this->mTitle
128            )
129        ];
130    }
131}