Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.07% covered (warning)
66.07%
37 / 56
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
LinksDeletionUpdate
66.07% covered (warning)
66.07%
37 / 56
0.00% covered (danger)
0.00%
0 / 3
12.16
0.00% covered (danger)
0.00%
0 / 1
 __construct
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
4.32
 doIncrementalUpdate
80.56% covered (warning)
80.56%
29 / 36
0.00% covered (danger)
0.00%
0 / 1
4.12
 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 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23namespace MediaWiki\Deferred\LinksUpdate;
24
25use InvalidArgumentException;
26use JobSpecification;
27use MediaWiki\Category\Category;
28use MediaWiki\Deferred\DeferredUpdates;
29use MediaWiki\Deferred\EnqueueableDataUpdate;
30use MediaWiki\MainConfigNames;
31use MediaWiki\MediaWikiServices;
32use MediaWiki\Parser\ParserOutput;
33use WikiPage;
34
35/**
36 * Update object handling the cleanup of links tables after a page was deleted.
37 */
38class LinksDeletionUpdate extends LinksUpdate implements EnqueueableDataUpdate {
39    /** @var WikiPage */
40    protected $page;
41    /** @var string */
42    protected $timestamp;
43
44    /**
45     * @param WikiPage $page Page we are updating
46     * @param int|null $pageId ID of the page we are updating [optional]
47     * @param string|null $timestamp TS_MW timestamp of deletion
48     */
49    public function __construct( WikiPage $page, $pageId = null, $timestamp = null ) {
50        $this->page = $page;
51        if ( $pageId ) {
52            $this->mId = $pageId; // page ID at time of deletion
53        } elseif ( $page->exists() ) {
54            $this->mId = $page->getId();
55        } else {
56            throw new InvalidArgumentException( "Page ID not known. Page doesn't exist?" );
57        }
58
59        $this->timestamp = $timestamp ?: wfTimestampNow();
60
61        $fakePO = new ParserOutput();
62        $fakePO->setCacheTime( $timestamp );
63        // Use immutable page identity to keep reference to the page id at time of deletion - T299244
64        $immutablePageIdentity = $page->getTitle()->toPageIdentity();
65        parent::__construct( $immutablePageIdentity, $fakePO, false );
66    }
67
68    protected function doIncrementalUpdate() {
69        $services = MediaWikiServices::getInstance();
70        $config = $services->getMainConfig();
71        $dbProvider = $services->getConnectionProvider();
72        $batchSize = $config->get( MainConfigNames::UpdateRowsPerQuery );
73
74        $id = $this->mId;
75        $title = $this->mTitle;
76
77        $dbw = $this->getDB(); // convenience
78
79        parent::doIncrementalUpdate();
80
81        // Typically, a category is empty when deleted, so check that we don't leave
82        // spurious row in the category table.
83        if ( $title->getNamespace() === NS_CATEGORY ) {
84            // T166757: do the update after the main job DB commit
85            DeferredUpdates::addCallableUpdate( static function () use ( $title ) {
86                $cat = Category::newFromName( $title->getDBkey() );
87                $cat->refreshCountsIfSmall();
88            } );
89        }
90
91        // Delete restrictions for the deleted page
92        $dbw->newDeleteQueryBuilder()
93            ->deleteFrom( 'page_restrictions' )
94            ->where( [ 'pr_page' => $id ] )
95            ->caller( __METHOD__ )->execute();
96
97        // Delete any redirect entry
98        $dbw->newDeleteQueryBuilder()
99            ->deleteFrom( 'redirect' )
100            ->where( [ 'rd_from' => $id ] )
101            ->caller( __METHOD__ )->execute();
102
103        // Find recentchanges entries to clean up...
104        // Select RC IDs just by curid, and not by title (see T307865 and T140960)
105        $rcIdsForPage = $dbw->newSelectQueryBuilder()
106            ->select( 'rc_id' )
107            ->from( 'recentchanges' )
108            ->where( [ 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ] )
109            ->caller( __METHOD__ )->fetchFieldValues();
110
111        // T98706: delete by PK to avoid lock contention with RC delete log insertions
112        $rcIdBatches = array_chunk( $rcIdsForPage, $batchSize );
113        foreach ( $rcIdBatches as $rcIdBatch ) {
114            $dbw->newDeleteQueryBuilder()
115                ->deleteFrom( 'recentchanges' )
116                ->where( [ 'rc_id' => $rcIdBatch ] )
117                ->caller( __METHOD__ )->execute();
118            if ( count( $rcIdBatches ) > 1 ) {
119                $dbProvider->commitAndWaitForReplication(
120                    __METHOD__, $this->ticket, [ 'domain' => $dbw->getDomainID() ]
121                );
122            }
123        }
124    }
125
126    public function getAsJobSpecification() {
127        return [
128            'domain' => $this->getDB()->getDomainID(),
129            'job' => new JobSpecification(
130                'deleteLinks',
131                [ 'pageId' => $this->mId, 'timestamp' => $this->timestamp ],
132                [ 'removeDuplicates' => true ],
133                $this->mTitle
134            )
135        ];
136    }
137}