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