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