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