Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.47% covered (success)
97.47%
77 / 79
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Storage
97.47% covered (success)
97.47%
77 / 79
87.50% covered (warning)
87.50%
7 / 8
23
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 load
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 loadFromDB
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 save
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
11
 delete
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 getDatabase
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
3.07
 getCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This file contains only the Storage class.
4 * @package GlobalPreferences
5 */
6
7namespace GlobalPreferences;
8
9use MediaWiki\MediaWikiServices;
10use WANObjectCache;
11use Wikimedia\LightweightObjectStore\ExpirationAwareness;
12use Wikimedia\Rdbms\IDatabase;
13
14/**
15 * This class handles all database storage of global preferences.
16 * @package GlobalPreferences
17 */
18class Storage {
19
20    /** The non-prefixed name of the global preferences database table. */
21    public const TABLE_NAME = 'global_preferences';
22
23    /** Update this constant when making incompatible changes to caching */
24    private const CACHE_VERSION = 1;
25
26    /** Cache lifetime */
27    private const CACHE_TTL = ExpirationAwareness::TTL_WEEK;
28
29    /** Instructs preference loading code to load the preferences from cache directly */
30    public const SKIP_CACHE = true;
31
32    /** @var int The global user ID. */
33    protected $userId;
34
35    /**
36     * Create a new Global Preferences Storage object for a given user.
37     * @param int $userId The global user ID.
38     */
39    public function __construct( int $userId ) {
40        $this->userId = $userId;
41    }
42
43    /**
44     * Get the user's global preferences.
45     *
46     * @param bool $skipCache Whether the preferences should be loaded strictly from DB
47     * @return string[] Keyed by the preference name.
48     */
49    public function load( bool $skipCache = false ): array {
50        if ( $skipCache ) {
51            return $this->loadFromDB();
52        }
53
54        $cache = $this->getCache();
55        $key = $this->getCacheKey();
56
57        return $cache->getWithSetCallback( $key, self::CACHE_TTL, function () {
58            return $this->loadFromDB();
59        } );
60    }
61
62    /**
63     * @param int $dbType One of DB_* constants
64     * @return string[]
65     */
66    protected function loadFromDB( $dbType = DB_REPLICA ): array {
67        $dbr = $this->getDatabase( $dbType );
68        $res = $dbr->select(
69            static::TABLE_NAME,
70            [ 'gp_property', 'gp_value' ],
71            [ 'gp_user' => $this->userId ],
72            __METHOD__
73        );
74        $preferences = [];
75        foreach ( $res as $row ) {
76            $preferences[$row->gp_property] = $row->gp_value;
77        }
78        return $preferences;
79    }
80
81    /**
82     * Save a set of global preferences. All existing preferences will be deleted before the new
83     * ones are saved.
84     * @param string[] $newPrefs Keyed by the preference name.
85     * @param string[] $knownPrefs Only work with the preferences we know about.
86     * @param string[] $checkMatricesToClear List of check matrix controls that
87     *        need their rows purged
88     */
89    public function save( array $newPrefs, array $knownPrefs, array $checkMatricesToClear = []
90    ): void {
91        $currentPrefs = $this->loadFromDB( DB_PRIMARY );
92
93        // Find records needing an insert or update
94        $save = [];
95        $delete = [];
96        foreach ( $newPrefs as $prop => $value ) {
97            if ( $value !== null ) {
98                if ( !isset( $currentPrefs[$prop] ) || $currentPrefs[$prop] != $value ) {
99                    $save[$prop] = $value;
100                }
101            } else {
102                $delete[] = $prop;
103            }
104        }
105
106        // Assemble the records to save
107        $rows = [];
108        foreach ( $save as $prop => $value ) {
109            $rows[] = [
110                'gp_user' => $this->userId,
111                'gp_property' => $prop,
112                'gp_value' => $value,
113            ];
114        }
115        // Save
116        if ( $rows ) {
117            $dbw = $this->getDatabase( DB_PRIMARY );
118            $dbw->newReplaceQueryBuilder()
119                ->replaceInto( static::TABLE_NAME )
120                ->uniqueIndexFields( [ 'gp_user', 'gp_property' ] )
121                ->rows( $rows )
122                ->caller( __METHOD__ )
123                ->execute();
124        }
125
126        // Delete unneeded rows
127        $keys = array_keys( $currentPrefs );
128        // Only delete prefs present on the local wiki
129        $keys = array_intersect( $keys, $knownPrefs );
130        $keys = array_values( array_diff( $keys, array_keys( $newPrefs ) ) );
131        $delete = array_merge( $delete, $keys );
132
133        // And specifically nuke the rows of a deglobalized CheckMatrix
134        foreach ( $checkMatricesToClear as $matrix ) {
135            foreach ( array_keys( $currentPrefs ) as $pref ) {
136                if ( strpos( $pref, $matrix ) === 0 ) {
137                    $delete[] = $pref;
138                }
139            }
140        }
141        $delete = array_unique( $delete );
142
143        if ( $delete ) {
144            $this->delete( $delete );
145        }
146
147        $key = $this->getCacheKey();
148        // Because we don't have the full preferences, just clear the cache
149        $this->getCache()->delete( $key );
150    }
151
152    /**
153     * Delete all of this user's global preferences.
154     * @param string[]|null $knownPrefs Only delete the preferences we know about.
155     */
156    public function delete( ?array $knownPrefs = null ): void {
157        $db = $this->getDatabase( DB_PRIMARY );
158        $conds = [ 'gp_user' => $this->userId ];
159        if ( $knownPrefs !== null ) {
160            $conds['gp_property'] = $knownPrefs;
161        }
162        $db->newDeleteQueryBuilder()
163            ->deleteFrom( static::TABLE_NAME )
164            ->where( $conds )
165            ->caller( __METHOD__ )
166            ->execute();
167        $key = $this->getCacheKey();
168        $this->getCache()->delete( $key );
169    }
170
171    /**
172     * Get the database object pointing to the Global Preferences database.
173     * @param int $type One of the DB_* constants
174     * @return IDatabase
175     */
176    protected function getDatabase( int $type = DB_REPLICA ): IDatabase {
177        $config = MediaWikiServices::getInstance()->getMainConfig();
178        $globalPreferencesDB = (string)$config->get( 'GlobalPreferencesDB' );
179        $sharedDB = (string)$config->get( 'SharedDB' );
180        $lbf = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
181        if ( $globalPreferencesDB != '' ) {
182            $domainId = $globalPreferencesDB;
183        } elseif ( $sharedDB != '' ) {
184            $domainId = $sharedDB;
185        } else {
186            // local wiki
187            $domainId = false;
188        }
189
190        return $lbf->getMainLB( $domainId )->getConnection( $type, [], $domainId );
191    }
192
193    /**
194     * @return WANObjectCache
195     */
196    protected function getCache(): WANObjectCache {
197        return MediaWikiServices::getInstance()->getMainWANObjectCache();
198    }
199
200    /**
201     * @return string
202     */
203    protected function getCacheKey(): string {
204        return $this->getCache()
205            ->makeGlobalKey( 'globalpreferences', 'prefs', self::CACHE_VERSION, $this->userId );
206    }
207}