Merge pull request #16545 from eileenmcnaughton/part_pend
[civicrm-core.git] / Civi / Core / SettingsManager.php
CommitLineData
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
12namespace 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 */
46class 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
TO
223 foreach ($specs as $key => $spec) {
224 $defaults[$key] = \CRM_Utils_Array::value('default', $spec);
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
TO
293 foreach ($civicrm_setting as $oldGroup => $values) {
294 $newGroup = isset($rewriteGroups[$oldGroup]) ? $rewriteGroups[$oldGroup] : 'domain';
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}