Commit | Line | Data |
---|---|---|
3a84c0ab TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
41498ac5 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
3a84c0ab | 5 | | | |
41498ac5 TO |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
3a84c0ab TO |
9 | +--------------------------------------------------------------------+ |
10 | */ | |
11 | ||
12 | namespace Civi\Core; | |
13 | ||
14 | /** | |
15 | * Class SettingsManager | |
16 | * @package Civi\Core | |
17 | * | |
5dbaf8de TO |
18 | * The SettingsManager is responsible for tracking settings across various |
19 | * domains and users. | |
20 | * | |
21 | * Generally, for any given setting, there are three levels where values | |
22 | * can be declared: | |
23 | * | |
24 | * - Mandatory values (which come from a global $civicrm_setting). | |
25 | * - Explicit values (which are chosen by the user and stored in the DB). | |
26 | * - Default values (which come from the settings metadata). | |
27 | * | |
28 | * Note: During the early stages of bootstrap, default values are not be available. | |
29 | * Loading the defaults requires loading metadata from various sources. However, | |
30 | * near the end of bootstrap, one calls SettingsManager::useDefaults() to fetch | |
31 | * and merge the defaults. | |
32 | * | |
33 | * Note: In a typical usage, there will only be one active domain and one | |
34 | * active contact (each having its own bag) within a given request. However, | |
35 | * in some edge-cases, you may need to work with multiple domains/contacts | |
36 | * at the same time. | |
37 | * | |
8eb5d071 TO |
38 | * Note: The global $civicrm_setting is meant to provide sysadmins with a way |
39 | * to override settings in `civicrm.settings.php`, but it has traditionally been | |
40 | * possible for extensions to manipulate $civicrm_setting in a hook. If you do | |
41 | * this, please call `useMandatory()` to tell SettingsManager to re-scan | |
42 | * $civicrm_setting. | |
43 | * | |
3a84c0ab TO |
44 | * @see SettingsManagerTest |
45 | */ | |
46 | class SettingsManager { | |
47 | ||
48 | /** | |
49 | * @var \CRM_Utils_Cache_Interface | |
50 | */ | |
51 | protected $cache; | |
52 | ||
53 | /** | |
34f3bbd9 | 54 | * @var array |
3a84c0ab TO |
55 | * Array (int $id => SettingsBag $bag). |
56 | */ | |
34f3bbd9 SL |
57 | protected $bagsByDomain = []; |
58 | ||
59 | ||
60 | /** | |
61 | * @var array | |
62 | * Array (int $id => SettingsBag $bag). | |
63 | */ | |
64 | protected $bagsByContact = []; | |
3a84c0ab TO |
65 | |
66 | /** | |
cc101011 | 67 | * @var array|null |
5dbaf8de TO |
68 | * Array(string $entity => array(string $settingName => mixed $value)). |
69 | * Ex: $mandatory['domain']['uploadDir']. | |
8eb5d071 | 70 | * NULL means "autoload from $civicrm_setting". |
3a84c0ab TO |
71 | */ |
72 | protected $mandatory = NULL; | |
73 | ||
5dbaf8de TO |
74 | /** |
75 | * Whether to use defaults. | |
76 | * | |
77 | * @var bool | |
78 | */ | |
79 | protected $useDefaults = FALSE; | |
80 | ||
3a84c0ab TO |
81 | /** |
82 | * @param \CRM_Utils_Cache_Interface $cache | |
5dbaf8de | 83 | * A semi-durable location to store metadata. |
3a84c0ab | 84 | */ |
8eb5d071 | 85 | public function __construct($cache) { |
3a84c0ab | 86 | $this->cache = $cache; |
3a84c0ab TO |
87 | } |
88 | ||
89 | /** | |
8eb5d071 | 90 | * Ensure that all defaults values are included with |
5dbaf8de TO |
91 | * all current and future bags. |
92 | * | |
4b350175 | 93 | * @return SettingsManager |
5dbaf8de TO |
94 | */ |
95 | public function useDefaults() { | |
96 | if (!$this->useDefaults) { | |
97 | $this->useDefaults = TRUE; | |
98 | ||
99 | if (!empty($this->bagsByDomain)) { | |
100 | foreach ($this->bagsByDomain as $bag) { | |
8eb5d071 | 101 | /** @var SettingsBag $bag */ |
5dbaf8de TO |
102 | $bag->loadDefaults($this->getDefaults('domain')); |
103 | } | |
104 | } | |
105 | ||
106 | if (!empty($this->bagsByContact)) { | |
107 | foreach ($this->bagsByContact as $bag) { | |
8eb5d071 | 108 | /** @var SettingsBag $bag */ |
5dbaf8de TO |
109 | $bag->loadDefaults($this->getDefaults('contact')); |
110 | } | |
111 | } | |
112 | } | |
113 | ||
114 | return $this; | |
115 | } | |
116 | ||
8eb5d071 TO |
117 | /** |
118 | * Ensure that mandatory values are included with | |
119 | * all current and future bags. | |
120 | * | |
121 | * If you call useMandatory multiple times, it will | |
122 | * re-scan the global $civicrm_setting. | |
123 | * | |
4b350175 | 124 | * @return SettingsManager |
8eb5d071 TO |
125 | */ |
126 | public function useMandatory() { | |
127 | $this->mandatory = NULL; | |
128 | ||
129 | foreach ($this->bagsByDomain as $bag) { | |
130 | /** @var SettingsBag $bag */ | |
131 | $bag->loadMandatory($this->getMandatory('domain')); | |
132 | } | |
133 | ||
134 | foreach ($this->bagsByContact as $bag) { | |
135 | /** @var SettingsBag $bag */ | |
136 | $bag->loadMandatory($this->getMandatory('contact')); | |
137 | } | |
138 | ||
139 | return $this; | |
140 | } | |
141 | ||
5dbaf8de | 142 | /** |
e97c66ff | 143 | * Get Settings by domain. |
144 | * | |
145 | * @param int|null $domainId | |
146 | * | |
3a84c0ab TO |
147 | * @return SettingsBag |
148 | */ | |
149 | public function getBagByDomain($domainId) { | |
150 | if ($domainId === NULL) { | |
151 | $domainId = \CRM_Core_Config::domainID(); | |
152 | } | |
153 | ||
154 | if (!isset($this->bagsByDomain[$domainId])) { | |
5dbaf8de | 155 | $this->bagsByDomain[$domainId] = new SettingsBag($domainId, NULL); |
8c74acc3 TO |
156 | if (\CRM_Core_Config::singleton()->dsn) { |
157 | $this->bagsByDomain[$domainId]->loadValues(); | |
158 | } | |
159 | $this->bagsByDomain[$domainId] | |
5dbaf8de TO |
160 | ->loadMandatory($this->getMandatory('domain')) |
161 | ->loadDefaults($this->getDefaults('domain')); | |
3a84c0ab TO |
162 | } |
163 | return $this->bagsByDomain[$domainId]; | |
164 | } | |
165 | ||
166 | /** | |
e97c66ff | 167 | * Get Settings by contact. |
168 | * | |
169 | * @param int|null $domainId | |
01d70c56 | 170 | * For the default domain, leave $domainID as NULL. |
e97c66ff | 171 | * @param int|null $contactId |
01d70c56 | 172 | * For the default/active user's contact, leave $domainID as NULL. |
e97c66ff | 173 | * |
3a84c0ab | 174 | * @return SettingsBag |
01d70c56 TO |
175 | * @throws \CRM_Core_Exception |
176 | * If there is no contact, then there's no SettingsBag, and we'll throw | |
177 | * an exception. | |
3a84c0ab TO |
178 | */ |
179 | public function getBagByContact($domainId, $contactId) { | |
180 | if ($domainId === NULL) { | |
181 | $domainId = \CRM_Core_Config::domainID(); | |
182 | } | |
01d70c56 TO |
183 | if ($contactId === NULL) { |
184 | $contactId = \CRM_Core_Session::getLoggedInContactID(); | |
185 | if (!$contactId) { | |
186 | throw new \CRM_Core_Exception("Cannot access settings subsystem - user or domain is unavailable"); | |
187 | } | |
188 | } | |
3a84c0ab TO |
189 | |
190 | $key = "$domainId:$contactId"; | |
191 | if (!isset($this->bagsByContact[$key])) { | |
5dbaf8de | 192 | $this->bagsByContact[$key] = new SettingsBag($domainId, $contactId); |
8c74acc3 TO |
193 | if (\CRM_Core_Config::singleton()->dsn) { |
194 | $this->bagsByContact[$key]->loadValues(); | |
195 | } | |
196 | $this->bagsByContact[$key] | |
5dbaf8de TO |
197 | ->loadDefaults($this->getDefaults('contact')) |
198 | ->loadMandatory($this->getMandatory('contact')); | |
3a84c0ab TO |
199 | } |
200 | return $this->bagsByContact[$key]; | |
201 | } | |
202 | ||
203 | /** | |
204 | * Determine the default settings. | |
205 | * | |
206 | * @param string $entity | |
207 | * Ex: 'domain' or 'contact'. | |
208 | * @return array | |
209 | * Array(string $settingName => mixed $value). | |
210 | */ | |
5dbaf8de TO |
211 | protected function getDefaults($entity) { |
212 | if (!$this->useDefaults) { | |
ac47f7ca | 213 | return self::getSystemDefaults($entity); |
5dbaf8de TO |
214 | } |
215 | ||
12eaada5 | 216 | $cacheKey = 'defaults_' . $entity; |
3a84c0ab TO |
217 | $defaults = $this->cache->get($cacheKey); |
218 | if (!is_array($defaults)) { | |
c64f69d9 | 219 | $specs = SettingsMetadata::getMetadata([ |
3a84c0ab | 220 | 'is_contact' => ($entity === 'contact' ? 1 : 0), |
c64f69d9 CW |
221 | ]); |
222 | $defaults = []; | |
3a84c0ab | 223 | foreach ($specs as $key => $spec) { |
9e10fb6b | 224 | $defaults[$key] = $spec['default'] ?? NULL; |
3a84c0ab | 225 | } |
ac47f7ca | 226 | \CRM_Utils_Array::extend($defaults, self::getSystemDefaults($entity)); |
3a84c0ab TO |
227 | $this->cache->set($cacheKey, $defaults); |
228 | } | |
229 | return $defaults; | |
230 | } | |
231 | ||
232 | /** | |
233 | * Get a list of mandatory/overriden settings. | |
234 | * | |
5dbaf8de TO |
235 | * @param string $entity |
236 | * Ex: 'domain' or 'contact'. | |
3a84c0ab TO |
237 | * @return array |
238 | * Array(string $settingName => mixed $value). | |
239 | */ | |
5dbaf8de | 240 | protected function getMandatory($entity) { |
3a84c0ab | 241 | if ($this->mandatory === NULL) { |
8eb5d071 | 242 | $this->mandatory = self::parseMandatorySettings(\CRM_Utils_Array::value('civicrm_setting', $GLOBALS)); |
3a84c0ab | 243 | } |
8eb5d071 | 244 | return $this->mandatory[$entity]; |
3a84c0ab TO |
245 | } |
246 | ||
247 | /** | |
5dbaf8de TO |
248 | * Parse mandatory settings. |
249 | * | |
250 | * In previous versions, settings were broken down into verbose+dynamic group names, e.g. | |
251 | * | |
252 | * $civicrm_settings['Foo Bar Preferences']['foo'] = 'bar'; | |
253 | * | |
254 | * We now simplify to two simple groups, 'domain' and 'contact'. | |
255 | * | |
256 | * $civicrm_settings['domain']['foo'] = 'bar'; | |
257 | * | |
258 | * However, the old groups are grand-fathered in as aliases. | |
3a84c0ab TO |
259 | * |
260 | * @param array $civicrm_setting | |
261 | * Ex: $civicrm_setting['Group Name']['field'] = 'value'. | |
262 | * Group names are an historical quirk; ignore them. | |
263 | * @return array | |
264 | */ | |
265 | public static function parseMandatorySettings($civicrm_setting) { | |
c64f69d9 CW |
266 | $result = [ |
267 | 'domain' => [], | |
268 | 'contact' => [], | |
269 | ]; | |
8eb5d071 | 270 | |
c64f69d9 | 271 | $rewriteGroups = [ |
8eb5d071 TO |
272 | //\CRM_Core_BAO_Setting::ADDRESS_STANDARDIZATION_PREFERENCES_NAME => 'domain', |
273 | //\CRM_Core_BAO_Setting::CAMPAIGN_PREFERENCES_NAME => 'domain', | |
274 | //\CRM_Core_BAO_Setting::CONTRIBUTE_PREFERENCES_NAME => 'domain', | |
275 | //\CRM_Core_BAO_Setting::DEVELOPER_PREFERENCES_NAME => 'domain', | |
276 | //\CRM_Core_BAO_Setting::DIRECTORY_PREFERENCES_NAME => 'domain', | |
277 | //\CRM_Core_BAO_Setting::EVENT_PREFERENCES_NAME => 'domain', | |
278 | //\CRM_Core_BAO_Setting::LOCALIZATION_PREFERENCES_NAME => 'domain', | |
279 | //\CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME => 'domain', | |
280 | //\CRM_Core_BAO_Setting::MAP_PREFERENCES_NAME => 'domain', | |
281 | //\CRM_Core_BAO_Setting::MEMBER_PREFERENCES_NAME => 'domain', | |
282 | //\CRM_Core_BAO_Setting::MULTISITE_PREFERENCES_NAME => 'domain', | |
283 | //\CRM_Core_BAO_Setting::PERSONAL_PREFERENCES_NAME => 'contact', | |
284 | 'Personal Preferences' => 'contact', | |
285 | //\CRM_Core_BAO_Setting::SEARCH_PREFERENCES_NAME => 'domain', | |
286 | //\CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME => 'domain', | |
287 | //\CRM_Core_BAO_Setting::URL_PREFERENCES_NAME => 'domain', | |
288 | 'domain' => 'domain', | |
289 | 'contact' => 'contact', | |
c64f69d9 | 290 | ]; |
5dbaf8de | 291 | |
3a84c0ab | 292 | if (is_array($civicrm_setting)) { |
8eb5d071 | 293 | foreach ($civicrm_setting as $oldGroup => $values) { |
2e1f50d6 | 294 | $newGroup = $rewriteGroups[$oldGroup] ?? 'domain'; |
8eb5d071 | 295 | $result[$newGroup] = array_merge($result[$newGroup], $values); |
3a84c0ab | 296 | } |
3a84c0ab | 297 | } |
8eb5d071 | 298 | return $result; |
3a84c0ab TO |
299 | } |
300 | ||
e1d39824 TO |
301 | /** |
302 | * Flush all in-memory and persistent caches related to settings. | |
303 | * | |
4b350175 | 304 | * @return SettingsManager |
e1d39824 TO |
305 | */ |
306 | public function flush() { | |
307 | $this->mandatory = NULL; | |
308 | ||
309 | $this->cache->flush(); | |
34f3bbd9 SL |
310 | // SettingsMetadata; not guaranteed to use same cache. |
311 | \Civi::cache('settings')->flush(); | |
e1d39824 TO |
312 | |
313 | foreach ($this->bagsByDomain as $bag) { | |
314 | /** @var SettingsBag $bag */ | |
315 | $bag->loadValues(); | |
316 | $bag->loadDefaults($this->getDefaults('domain')); | |
317 | $bag->loadMandatory($this->getMandatory('domain')); | |
318 | } | |
319 | ||
ac47f7ca | 320 | foreach ($this->bagsByContact as $bag) { |
e1d39824 TO |
321 | /** @var SettingsBag $bag */ |
322 | $bag->loadValues(); | |
323 | $bag->loadDefaults($this->getDefaults('contact')); | |
324 | $bag->loadMandatory($this->getMandatory('contact')); | |
325 | } | |
326 | ||
327 | return $this; | |
328 | } | |
329 | ||
ac47f7ca TO |
330 | /** |
331 | * Get a list of critical system defaults. | |
332 | * | |
333 | * The setting system can be modified by extensions, which means that it's not fully available | |
334 | * during bootstrap -- in particular, defaults cannot be loaded. For a very small number of settings, | |
335 | * we must define defaults before the system bootstraps. | |
336 | * | |
54957108 | 337 | * @param string $entity |
338 | * | |
ac47f7ca TO |
339 | * @return array |
340 | */ | |
341 | private static function getSystemDefaults($entity) { | |
c64f69d9 | 342 | $defaults = []; |
ac47f7ca TO |
343 | switch ($entity) { |
344 | case 'domain': | |
c64f69d9 | 345 | $defaults = [ |
801c5335 | 346 | 'installed' => FALSE, |
c64f69d9 | 347 | 'enable_components' => ['CiviEvent', 'CiviContribute', 'CiviMember', 'CiviMail', 'CiviReport', 'CiviPledge'], |
ac47f7ca TO |
348 | 'customFileUploadDir' => '[civicrm.files]/custom/', |
349 | 'imageUploadDir' => '[civicrm.files]/persist/contribute/', | |
350 | 'uploadDir' => '[civicrm.files]/upload/', | |
351 | 'imageUploadURL' => '[civicrm.files]/persist/contribute/', | |
9f965e18 TO |
352 | 'extensionsDir' => '[civicrm.files]/ext/', |
353 | 'extensionsURL' => '[civicrm.files]/ext/', | |
ac47f7ca TO |
354 | 'resourceBase' => '[civicrm.root]/', |
355 | 'userFrameworkResourceURL' => '[civicrm.root]/', | |
c64f69d9 | 356 | ]; |
ac47f7ca TO |
357 | break; |
358 | ||
359 | } | |
360 | return $defaults; | |
361 | } | |
362 | ||
3a84c0ab | 363 | } |