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