Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 129
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
FRDependencyUpdate
0.00% covered (danger)
0.00%
0 / 129
0.00% covered (danger)
0.00%
0 / 10
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 doUpdate
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
182
 getExistingDeps
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getDepInsertions
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getDepDeletions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 makeWhereFrom2d
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 addDependency
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCurrentVersionLinks
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getCurrentVersionTemplates
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getCurrentVersionCategories
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3use MediaWiki\MediaWikiServices;
4use MediaWiki\Parser\ParserOutput;
5use MediaWiki\Title\Title;
6use Wikimedia\Rdbms\IDBAccessObject;
7use Wikimedia\Rdbms\Platform\ISQLPlatform;
8
9/**
10 * Class containing update methods for tracking links that
11 * are only in the stable version of pages. Used only for caching.
12 */
13class FRDependencyUpdate {
14    /** @var Title */
15    private $title;
16    /** @var int[][] */
17    private $sLinks;
18    /** @var int[][] */
19    private $sTemplates;
20    /** @var string[] */
21    private $sCategoryNames;
22
23    // run updates now
24    public const IMMEDIATE = 0;
25    // use the job queue for updates
26    public const DEFERRED = 1;
27
28    /**
29     * @param Title $title
30     * @param ParserOutput $stableOutput
31     */
32    public function __construct( Title $title, ParserOutput $stableOutput ) {
33        $this->title = $title;
34        # Stable version links
35        $this->sLinks = $stableOutput->getLinks();
36        $this->sTemplates = $stableOutput->getTemplates();
37        $this->sCategoryNames = $stableOutput->getCategoryNames();
38    }
39
40    /**
41     * @param int $mode FRDependencyUpdate::IMMEDIATE/FRDependencyUpdate::DEFERRED
42     */
43    public function doUpdate( $mode = self::IMMEDIATE ) {
44        $deps = [];
45        # Get any links that are only in the stable version...
46        $cLinks = $this->getCurrentVersionLinks();
47        foreach ( $this->sLinks as $ns => $titles ) {
48            foreach ( $titles as $title => $pageId ) {
49                if ( !isset( $cLinks[$ns][$title] ) ) {
50                    $this->addDependency( $deps, $ns, $title );
51                }
52            }
53        }
54        # Get any templates that are only in the stable version...
55        $cTemplates = $this->getCurrentVersionTemplates();
56        foreach ( $this->sTemplates as $ns => $titles ) {
57            foreach ( $titles as $title => $id ) {
58                if ( !isset( $cTemplates[$ns][$title] ) ) {
59                    $this->addDependency( $deps, $ns, $title );
60                }
61            }
62        }
63        # Get any categories that are only in the stable version...
64        $cCategories = $this->getCurrentVersionCategories();
65        foreach ( $this->sCategoryNames as $category ) {
66            if ( !isset( $cCategories[$category] ) ) {
67                $this->addDependency( $deps, NS_CATEGORY, $category );
68            }
69        }
70        # Quickly check for any dependency tracking changes (use a replica DB)
71        if ( $this->getExistingDeps() != $deps ) {
72            if ( $mode === self::DEFERRED ) {
73                # Let the job queue parse and update
74                MediaWikiServices::getInstance()->getJobQueueGroup()->push(
75                    new FRExtraCacheUpdateJob(
76                        $this->title,
77                        [ 'type' => 'updatelinks' ]
78                    )
79                );
80
81                return;
82            }
83            # Determine any dependency tracking changes
84            $existing = $this->getExistingDeps( IDBAccessObject::READ_LATEST );
85            $insertions = $this->getDepInsertions( $existing, $deps );
86            $deletions = $this->getDepDeletions( $existing, $deps );
87            $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
88            # Delete removed links
89            if ( $deletions ) {
90                $dbw->newDeleteQueryBuilder()
91                    ->deleteFrom( 'flaggedrevs_tracking' )
92                    ->where( $deletions )
93                    ->caller( __METHOD__ )
94                    ->execute();
95            }
96            # Add any new links
97            if ( $insertions ) {
98                $dbw->newInsertQueryBuilder()
99                    ->insertInto( 'flaggedrevs_tracking' )
100                    ->ignore()
101                    ->rows( $insertions )
102                    ->caller( __METHOD__ )
103                    ->execute();
104            }
105        }
106    }
107
108    /**
109     * Get existing cache dependencies
110     * @param int $flags One of the IDBAccessObject::READ_… constants
111     * @return int[][] (ns => dbKey => 1)
112     */
113    private function getExistingDeps( $flags = 0 ) {
114        if ( $flags & IDBAccessObject::READ_LATEST ) {
115            $db = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
116        } else {
117            $db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
118        }
119        $res = $db->newSelectQueryBuilder()
120            ->select( [ 'ftr_namespace', 'ftr_title' ] )
121            ->from( 'flaggedrevs_tracking' )
122            ->where( [ 'ftr_from' => $this->title->getArticleID() ] )
123            ->caller( __METHOD__ )
124            ->fetchResultSet();
125        $arr = [];
126        foreach ( $res as $row ) {
127            $arr[$row->ftr_namespace][$row->ftr_title] = 1;
128        }
129        return $arr;
130    }
131
132    /**
133     * Get INSERT rows for cache dependencies in $new but not in $existing
134     * @param int[][] $existing
135     * @param int[][] $new
136     * @return array[]
137     */
138    private function getDepInsertions( array $existing, array $new ) {
139        $arr = [];
140        foreach ( $new as $ns => $dbkeys ) {
141            if ( isset( $existing[$ns] ) ) {
142                $diffs = array_diff_key( $dbkeys, $existing[$ns] );
143            } else {
144                $diffs = $dbkeys;
145            }
146            foreach ( $diffs as $dbk => $id ) {
147                $arr[] = [
148                    'ftr_from'      => $this->title->getArticleID(),
149                    'ftr_namespace' => $ns,
150                    'ftr_title'     => $dbk
151                ];
152            }
153        }
154        return $arr;
155    }
156
157    /**
158     * Get WHERE clause to delete items in $existing but not in $new
159     * @param int[][] $existing
160     * @param int[][] $new
161     * @return array|false
162     */
163    private function getDepDeletions( array $existing, array $new ) {
164        $del = [];
165        foreach ( $existing as $ns => $dbkeys ) {
166            if ( isset( $new[$ns] ) ) {
167                $delKeys = array_diff_key( $dbkeys, $new[$ns] );
168                if ( $delKeys ) {
169                    $del[$ns] = $delKeys;
170                }
171            } else {
172                $del[$ns] = $dbkeys;
173            }
174        }
175        if ( $del ) {
176            $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
177            $clause = $this->makeWhereFrom2d( $del, $dbw );
178            if ( $clause ) {
179                return [ $clause, 'ftr_from' => $this->title->getArticleID() ];
180            }
181        }
182        return false;
183    }
184
185    /**
186     * Make WHERE clause to match $arr titles
187     * @param array[] $arr
188     * @param ISQLPlatform $db
189     * @return string|bool
190     */
191    private function makeWhereFrom2d( $arr, ISQLPlatform $db ) {
192        $lb = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch();
193        $lb->setArray( $arr );
194        return $lb->constructSet( 'ftr', $db );
195    }
196
197    /**
198     * @param int[][] &$deps
199     * @param int $ns
200     * @param string $dbKey
201     */
202    private function addDependency( array &$deps, $ns, $dbKey ) {
203        $deps[$ns][$dbKey] = 1;
204    }
205
206    /**
207     * Get an array of existing links, as a 2-D array
208     * @return int[][] (ns => dbKey => 1)
209     */
210    private function getCurrentVersionLinks() {
211        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
212        $linksMigration = MediaWikiServices::getInstance()->getLinksMigration();
213        $queryInfo = $linksMigration->getQueryInfo( 'pagelinks' );
214        [ $nsField, $titleField ] = $linksMigration->getTitleFields( 'pagelinks' );
215        $res = $dbr->newSelectQueryBuilder()
216            ->tables( $queryInfo['tables'] )
217            ->fields( $queryInfo['fields'] )
218            ->where( [ 'pl_from' => $this->title->getArticleID() ] )
219            ->joinConds( $queryInfo['joins'] )
220            ->caller( __METHOD__ )
221            ->fetchResultSet();
222        $arr = [];
223        foreach ( $res as $row ) {
224            $arr[$row->$nsField][$row->$titleField] = 1;
225        }
226        return $arr;
227    }
228
229    /**
230     * Get an array of existing templates, as a 2-D array
231     * @return int[][] (ns => dbKey => 1)
232     */
233    private function getCurrentVersionTemplates() {
234        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
235        $linksMigration = MediaWikiServices::getInstance()->getLinksMigration();
236        $queryInfo = $linksMigration->getQueryInfo( 'templatelinks' );
237        [ $nsField, $titleField ] = $linksMigration->getTitleFields( 'templatelinks' );
238        $res = $dbr->newSelectQueryBuilder()
239            ->tables( $queryInfo['tables'] )
240            ->fields( $queryInfo['fields'] )
241            ->where( [ 'tl_from' => $this->title->getArticleID() ] )
242            ->joinConds( $queryInfo['joins'] )
243            ->caller( __METHOD__ )
244            ->fetchResultSet();
245        $arr = [];
246        foreach ( $res as $row ) {
247            $arr[$row->$nsField][$row->$titleField] = 1;
248        }
249        return $arr;
250    }
251
252    /**
253     * Get an array of existing categories, with the name in the key and sort key in the value.
254     * @return string[] (category => sortkey)
255     */
256    private function getCurrentVersionCategories() {
257        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
258        $res = $dbr->newSelectQueryBuilder()
259            ->select( [ 'cl_to', 'cl_sortkey' ] )
260            ->from( 'categorylinks' )
261            ->where( [ 'cl_from' => $this->title->getArticleID() ] )
262            ->caller( __METHOD__ )
263            ->fetchResultSet();
264        $arr = [];
265        foreach ( $res as $row ) {
266            $arr[$row->cl_to] = $row->cl_sortkey;
267        }
268        return $arr;
269    }
270}