Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 125
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
FRPageConfig
0.00% covered (danger)
0.00%
0 / 125
0.00% covered (danger)
0.00%
0 / 8
1722
0.00% covered (danger)
0.00%
0 / 1
 getStabilitySettings
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getVisibilitySettingsFromRow
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
110
 getDefaultVisibilitySettings
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 setStabilitySettings
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
56
 configIsReset
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getProtectionLevel
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 isValidRestriction
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 purgeExpiredConfigurations
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3use MediaWiki\MediaWikiServices;
4use MediaWiki\Title\Title;
5use Wikimedia\Rdbms\IDBAccessObject;
6
7/**
8 * Page stability configuration functions
9 */
10class FRPageConfig {
11    /**
12     * Get visibility settings/restrictions for a page
13     * @param Title $title page title
14     * @param int $flags One of the IDBAccessObject::READ_… constants
15     * @return array [ 'override' => int, 'autoreview' => string, 'expiry' => string ]
16     */
17    public static function getStabilitySettings( Title $title, $flags = 0 ) {
18        if ( $flags & IDBAccessObject::READ_LATEST ) {
19            $db = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
20        } else {
21            $db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
22        }
23        $row = $db->newSelectQueryBuilder()
24            ->select( [ 'fpc_override', 'fpc_level', 'fpc_expiry' ] )
25            ->from( 'flaggedpage_config' )
26            ->where( [ 'fpc_page_id' => $title->getArticleID() ] )
27            ->caller( __METHOD__ )
28            ->fetchRow();
29        return self::getVisibilitySettingsFromRow( $row );
30    }
31
32    /**
33     * Get page configuration settings from a DB row
34     * @param stdClass|false $row
35     * @return array [ 'override' => int, 'autoreview' => string, 'expiry' => string ]
36     */
37    public static function getVisibilitySettingsFromRow( $row ) {
38        $expiry = false;
39        if ( $row ) {
40            $expiry = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase()
41                ->decodeExpiry( $row->fpc_expiry );
42            # Only apply the settings if they haven't expired
43            if ( !$expiry || $expiry < wfTimestampNow() ) {
44                $row = null; // expired
45            }
46        }
47        // Is there a non-expired row?
48        if ( !$row ) {
49            # Return the default config if this page doesn't have its own
50            return self::getDefaultVisibilitySettings();
51        }
52
53        $level = self::isValidRestriction( $row->fpc_level ) ? $row->fpc_level : '';
54        $config = [
55            'override' => $row->fpc_override ? 1 : 0,
56            'autoreview' => $level,
57            'expiry' => $expiry // TS_MW
58        ];
59        # If there are protection levels defined check if this is valid...
60        if ( FlaggedRevs::useProtectionLevels() ) {
61            $level = self::getProtectionLevel( $config );
62            if ( $level == 'invalid' || $level == 'none' ) {
63                // If 'none', make sure expiry is 'infinity'
64                return self::getDefaultVisibilitySettings(); // revert to default (none)
65            }
66        }
67        return $config;
68    }
69
70    /**
71     * Get default stability configuration settings
72     * @return array
73     */
74    public static function getDefaultVisibilitySettings() {
75        return [
76            # Keep this consistent: 1 => override, 0 => don't
77            'override'   => FlaggedRevs::isStableShownByDefault() ? 1 : 0,
78            'autoreview' => '',
79            'expiry'     => 'infinity'
80        ];
81    }
82
83    /**
84     * Set the stability configuration settings for a page
85     * @param Title $title
86     * @param array $config
87     * @return bool Row changed
88     */
89    public static function setStabilitySettings( Title $title, array $config ) {
90        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
91
92        # Purge expired entries on one in every 10 queries
93        if ( !mt_rand( 0, 10 ) ) {
94            self::purgeExpiredConfigurations();
95        }
96        # If setting to site default values and there is a row then erase it
97        if ( self::configIsReset( $config ) ) {
98            $dbw->newDeleteQueryBuilder()
99                ->deleteFrom( 'flaggedpage_config' )
100                ->where( [ 'fpc_page_id' => $title->getArticleID() ] )
101                ->caller( __METHOD__ )
102                ->execute();
103            $changed = ( $dbw->affectedRows() > 0 ); // did this do anything?
104        # Otherwise, add/replace row if we are not just setting it to the site default
105        } else {
106            $dbExpiry = $dbw->encodeExpiry( $config['expiry'] );
107            # Get current config...
108            $oldRow = $dbw->newSelectQueryBuilder()
109                ->select( [ 'fpc_override', 'fpc_level', 'fpc_expiry' ] )
110                ->from( 'flaggedpage_config' )
111                ->where( [ 'fpc_page_id' => $title->getArticleID() ] )
112                ->forUpdate() // lock
113                ->caller( __METHOD__ )
114                ->fetchRow();
115            # Check if this is not the same config as the existing (if any) row
116            $changed = ( !$oldRow // no previous config
117                || $oldRow->fpc_override != $config['override'] // ...override changed, or...
118                || $oldRow->fpc_level != $config['autoreview'] // ...autoreview level changed, or...
119                || $oldRow->fpc_expiry != $dbExpiry // ...expiry changed
120            );
121            # If the new config is different, replace the old row...
122            if ( $changed ) {
123                $dbw->newReplaceQueryBuilder()
124                    ->replaceInto( 'flaggedpage_config' )
125                    ->uniqueIndexFields( 'fpc_page_id' )
126                    ->row( [
127                        'fpc_page_id'  => $title->getArticleID(),
128                        'fpc_override' => (int)$config['override'],
129                        'fpc_level'    => $config['autoreview'],
130                        'fpc_expiry'   => $dbExpiry
131                    ] )
132                    ->caller( __METHOD__ )
133                    ->execute();
134            }
135        }
136        return $changed;
137    }
138
139    /**
140     * Does this config equal the default settings?
141     * @param array $config
142     * @return bool
143     */
144    public static function configIsReset( array $config ) {
145        if ( FlaggedRevs::useOnlyIfProtected() ) {
146            return ( $config['autoreview'] == '' );
147        } else {
148            return ( $config['override'] == FlaggedRevs::isStableShownByDefault()
149                && $config['autoreview'] == '' );
150        }
151    }
152
153    /**
154     * Find what protection level a config is in
155     * @param array $config
156     * @return string
157     */
158    public static function getProtectionLevel( array $config ) {
159        if ( !FlaggedRevs::useProtectionLevels() ) {
160            throw new LogicException( '$wgFlaggedRevsProtection is disabled' );
161        }
162        $defaultConfig = self::getDefaultVisibilitySettings();
163        # Check if the page is not protected at all...
164        if ( $config['override'] == $defaultConfig['override']
165            && $config['autoreview'] == ''
166        ) {
167            return "none"; // not protected
168        }
169        # All protection levels have 'override' on
170        if ( $config['override'] ) {
171            # The levels are defined by the 'autoreview' settings
172            if ( in_array( $config['autoreview'], FlaggedRevs::getRestrictionLevels() ) ) {
173                return $config['autoreview'];
174            }
175        }
176        return "invalid";
177    }
178
179    /**
180     * Check if an fpc_level value is valid
181     * @param string $right
182     * @return bool
183     */
184    private static function isValidRestriction( $right ) {
185        if ( $right == '' ) {
186            return true; // no restrictions (none)
187        }
188        return in_array( $right, FlaggedRevs::getRestrictionLevels(), true );
189    }
190
191    /**
192     * Purge expired restrictions from the flaggedpage_config table.
193     * The stable version of pages may change and invalidation may be required.
194     */
195    private static function purgeExpiredConfigurations() {
196        if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
197            return;
198        }
199        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
200
201        # Find pages with expired configs...
202        $config = self::getDefaultVisibilitySettings(); // config is to be reset
203        $cutoff = $dbw->timestamp();
204        $ret = $dbw->newSelectQueryBuilder()
205            ->select( [ 'fpc_page_id', 'page_namespace', 'page_title' ] )
206            ->from( 'flaggedpage_config' )
207            ->join( 'page', null, 'page_id = fpc_page_id' )
208            ->where( $dbw->expr( 'fpc_expiry', '<', $cutoff ) )
209            ->caller( __METHOD__ )
210            ->fetchResultSet();
211        # Figured out to do with each page...
212        $pagesClearConfig = [];
213        $pagesClearTracking = [];
214        $titlesClearTracking = [];
215        foreach ( $ret as $row ) {
216            # If FlaggedRevs got "turned off" (in protection config)
217            # for this page, then clear it from the tracking tables...
218            if ( FlaggedRevs::useOnlyIfProtected() && !$config['override'] ) {
219                $pagesClearTracking[] = $row->fpc_page_id; // no stable version
220                $titlesClearTracking[] = Title::newFromRow( $row ); // no stable version
221            }
222            $pagesClearConfig[] = $row->fpc_page_id; // page with expired config
223        }
224        # Clear the expired config for these pages...
225        if ( count( $pagesClearConfig ) ) {
226            $dbw->newDeleteQueryBuilder()
227                ->deleteFrom( 'flaggedpage_config' )
228                ->where( [ 'fpc_page_id' => $pagesClearConfig, $dbw->expr( 'fpc_expiry', '<', $cutoff ) ] )
229                ->caller( __METHOD__ )
230                ->execute();
231        }
232        # Clear the tracking rows and update page_touched for the
233        # pages in $pagesClearConfig that do now have a stable version...
234        if ( count( $pagesClearTracking ) ) {
235            FlaggedRevs::clearTrackingRows( $pagesClearTracking );
236            $dbw->newUpdateQueryBuilder()
237                ->update( 'page' )
238                ->set( [ 'page_touched' => $dbw->timestamp() ] )
239                ->where( [ 'page_id' => $pagesClearTracking ] )
240                ->caller( __METHOD__ )
241                ->execute();
242        }
243        # Also, clear their squid caches and purge other pages that use this page.
244        # NOTE: all of these updates are deferred via DeferredUpdates
245        foreach ( $titlesClearTracking as $title ) {
246            FlaggedRevs::purgeMediaWikiHtmlCdn( $title );
247            if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_STABLE ) {
248                FlaggedRevs::updateHtmlCaches( $title ); // purge pages that use this page
249            }
250        }
251    }
252}