SearchKit - Improve field/operator/value selection UI
[civicrm-core.git] / CRM / Admin / Form / SettingTrait.php
CommitLineData
946389fb 1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
946389fb 5 | |
bc77d7c0
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 |
946389fb 9 +--------------------------------------------------------------------+
10 */
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
946389fb 16 */
17
18/**
19 * This trait allows us to consolidate Preferences & Settings forms.
20 *
21 * It is intended mostly as part of a refactoring process to get rid of having 2.
22 */
23trait CRM_Admin_Form_SettingTrait {
24
51f35276
FG
25 /**
26 * The setting page filter.
27 *
28 * @var string
29 */
30 private $_filter;
31
946389fb 32 /**
33 * @var array
34 */
35 protected $settingsMetadata;
36
37 /**
38 * Get default entity.
39 *
40 * @return string
41 */
42 public function getDefaultEntity() {
43 return 'Setting';
44 }
45
6be2178c 46 /**
47 * Fields defined as read only.
48 *
49 * @var array
50 */
51 protected $readOnlyFields = [];
52
53 /**
54 * Have read only fields been defined on the form.
55 *
56 * @return bool
57 */
58 protected function hasReadOnlyFields(): bool {
59 return !empty($this->readOnlyFields);
60 }
61
946389fb 62 /**
63 * Get the metadata relating to the settings on the form, ordered by the keys in $this->_settings.
64 *
65 * @return array
66 */
6be2178c 67 protected function getSettingsMetaData(): array {
946389fb 68 if (empty($this->settingsMetadata)) {
4cc9fbc2 69 $this->settingsMetadata = \Civi\Core\SettingsMetadata::getMetadata(['name' => array_keys($this->_settings)], NULL, TRUE);
946389fb 70 // This array_merge re-orders to the key order of $this->_settings.
71 $this->settingsMetadata = array_merge($this->_settings, $this->settingsMetadata);
72 }
73 return $this->settingsMetadata;
74 }
75
76 /**
77 * Get the settings which can be stored based on metadata.
78 *
79 * @param array $params
80 * @return array
81 */
82 protected function getSettingsToSetByMetadata($params) {
74f89a9f 83 $setValues = array_intersect_key($params, $this->_settings);
84 // Checkboxes will be unset rather than empty so we need to add them back in.
85 // Handle quickform hateability just once, right here right now.
86 $unsetValues = array_diff_key($this->_settings, $params);
87 foreach ($unsetValues as $key => $unsetValue) {
9069085b
LS
88 $quickFormType = $this->getQuickFormType($this->getSettingMetadata($key));
89 if ($quickFormType === 'CheckBox') {
74f89a9f 90 $setValues[$key] = [$key => 0];
91 }
9069085b
LS
92 elseif ($quickFormType === 'CheckBoxes') {
93 $setValues[$key] = [];
94 }
74f89a9f 95 }
96 return $setValues;
946389fb 97 }
98
99 /**
100 * @param $params
101 */
102 protected function filterParamsSetByMetadata(&$params) {
103 foreach ($this->getSettingsToSetByMetadata($params) as $setting => $settingGroup) {
104 //@todo array_diff this
105 unset($params[$setting]);
106 }
107 }
108
6821aa1d 109 /**
110 * Get the metadata for a particular field.
111 *
112 * @param $setting
113 * @return mixed
114 */
115 protected function getSettingMetadata($setting) {
116 return $this->getSettingsMetaData()[$setting];
117 }
118
119 /**
120 * Get the metadata for a particular field for a particular item.
121 *
122 * e.g get 'serialize' key, if exists, for a field.
123 *
124 * @param $setting
62d3ee27 125 * @param $item
6821aa1d 126 * @return mixed
127 */
128 protected function getSettingMetadataItem($setting, $item) {
129 return CRM_Utils_Array::value($item, $this->getSettingsMetaData()[$setting]);
130 }
131
51f35276 132 /**
c7c449f8
MWMC
133 * This is public so we can retrieve the filter name via hooks etc. and apply conditional logic (eg. loading javascript conditionals).
134 *
51f35276
FG
135 * @return string
136 */
c7c449f8 137 public function getSettingPageFilter() {
51f35276
FG
138 if (!isset($this->_filter)) {
139 // Get the last URL component without modifying the urlPath property.
140 $urlPath = array_values($this->urlPath);
141 $this->_filter = end($urlPath);
142 }
143 return $this->_filter;
144 }
145
146 /**
147 * Returns a re-keyed copy of the settings, ordered by weight.
148 *
149 * @return array
150 */
151 protected function getSettingsOrderedByWeight() {
152 $settingMetaData = $this->getSettingsMetaData();
153 $filter = $this->getSettingPageFilter();
154
155 usort($settingMetaData, function ($a, $b) use ($filter) {
156 // Handle cases in which a comparison is impossible. Such will be considered ties.
157 if (
158 // A comparison can't be made unless both setting weights are declared.
159 !isset($a['settings_pages'][$filter]['weight'], $b['settings_pages'][$filter]['weight'])
160 // A pair of settings might actually have the same weight.
161 || $a['settings_pages'][$filter]['weight'] === $b['settings_pages'][$filter]['weight']
162 ) {
163 return 0;
164 }
165
166 return $a['settings_pages'][$filter]['weight'] > $b['settings_pages'][$filter]['weight'] ? 1 : -1;
167 });
168
169 return $settingMetaData;
170 }
171
e894ae15 172 /**
173 * Add fields in the metadata to the template.
76c8a771 174 *
175 * @throws \CRM_Core_Exception
71bbdae1 176 * @throws \CiviCRM_API3_Exception
e894ae15 177 */
178 protected function addFieldsDefinedInSettingsMetadata() {
71bbdae1 179 $this->addSettingsToFormFromMetadata();
e894ae15 180 $settingMetaData = $this->getSettingsMetaData();
181 $descriptions = [];
182 foreach ($settingMetaData as $setting => $props) {
c5af8245 183 $quickFormType = $this->getQuickFormType($props);
184 if (isset($quickFormType)) {
9c1bc317 185 $options = $props['options'] ?? NULL;
4cc9fbc2 186 if ($options) {
e78a7054 187 if ($quickFormType === 'Select' && isset($props['is_required']) && $props['is_required'] === FALSE && !isset($options[''])) {
1c8738dd 188 // If the spec specifies the field is not required add a null option.
189 // Why not if empty($props['is_required']) - basically this has been added to the spec & might not be set to TRUE
190 // when it is true.
191 $options = ['' => ts('None')] + $options;
192 }
e894ae15 193 }
c89a43b3 194 if ($props['type'] === 'Boolean') {
195 $options = [$props['title'] => $props['name']];
e894ae15 196 }
c89a43b3 197
e894ae15 198 //Load input as readonly whose values are overridden in civicrm.settings.php.
a0b8347a 199 if (Civi::settings()->getMandatory($setting) !== NULL) {
e894ae15 200 $props['html_attributes']['readonly'] = TRUE;
6be2178c 201 $this->readOnlyFields[] = $setting;
e894ae15 202 }
203
c5af8245 204 $add = 'add' . $quickFormType;
76c8a771 205 if ($add === 'addElement') {
e894ae15 206 $this->$add(
207 $props['html_type'],
208 $setting,
6dabf459 209 $props['title'],
c89a43b3 210 ($options !== NULL) ? $options : CRM_Utils_Array::value('html_attributes', $props, []),
e894ae15 211 ($options !== NULL) ? CRM_Utils_Array::value('html_attributes', $props, []) : NULL
212 );
213 }
76c8a771 214 elseif ($add === 'addSelect') {
6dabf459 215 $this->addElement('select', $setting, $props['title'], $options, CRM_Utils_Array::value('html_attributes', $props));
e894ae15 216 }
76c8a771 217 elseif ($add === 'addCheckBox') {
969afb18 218 $this->addCheckBox($setting, '', $options, NULL, CRM_Utils_Array::value('html_attributes', $props), NULL, NULL, ['&nbsp;&nbsp;']);
e894ae15 219 }
76c8a771 220 elseif ($add === 'addCheckBoxes') {
4e086328
CW
221 $newOptions = array_flip($options);
222 $classes = 'crm-checkbox-list';
223 if (!empty($props['sortable'])) {
224 $classes .= ' crm-sortable-list';
225 $newOptions = array_flip(self::reorderSortableOptions($setting, $options));
a55c9b35 226 }
4e086328 227 $settingMetaData[$setting]['wrapper_element'] = ['<ul class="' . $classes . '"><li>', '</li></ul>'];
a55c9b35 228 $this->addCheckBox($setting,
229 $props['title'],
230 $newOptions,
231 NULL, NULL, NULL, NULL,
bfd9c358 232 '</li><li>'
a55c9b35 233 );
234 }
76c8a771 235 elseif ($add === 'addChainSelect') {
0512a83f 236 $this->addChainSelect($setting, ['label' => $props['title']] + $props['chain_select_settings']);
e894ae15 237 }
76c8a771 238 elseif ($add === 'addMonthDay') {
6dabf459 239 $this->add('date', $setting, $props['title'], CRM_Core_SelectValues::date(NULL, 'M d'));
e894ae15 240 }
eba92929 241 elseif ($add === 'addEntityRef') {
6dabf459 242 $this->$add($setting, $props['title'], $props['entity_reference_options']);
eba92929 243 }
c7cd4e2c 244 elseif ($add === 'addYesNo' && ($props['type'] === 'Boolean')) {
71af887d 245 $this->addRadio($setting, $props['title'], [1 => ts('Yes'), 0 => ts('No')], CRM_Utils_Array::value('html_attributes', $props), '&nbsp;&nbsp;');
c7cd4e2c 246 }
8a52ae34 247 elseif ($add === 'add') {
12c73866 248 $this->add($props['html_type'], $setting, $props['title'], $options, FALSE, $props['html_extra'] ?? NULL);
8a52ae34 249 }
e894ae15 250 else {
6dabf459 251 $this->$add($setting, $props['title'], $options);
e894ae15 252 }
253 // Migrate to using an array as easier in smart...
9c1bc317 254 $description = $props['description'] ?? NULL;
f8857611 255 $descriptions[$setting] = $description;
256 $this->assign("{$setting}_description", $description);
76c8a771 257 if ($setting === 'max_attachments') {
e894ae15 258 //temp hack @todo fix to get from metadata
259 $this->addRule('max_attachments', ts('Value should be a positive number'), 'positiveInteger');
260 }
76c8a771 261 if ($setting === 'max_attachments_backend') {
8913e915
D
262 //temp hack @todo fix to get from metadata
263 $this->addRule('max_attachments_backend', ts('Value should be a positive number'), 'positiveInteger');
264 }
76c8a771 265 if ($setting === 'maxFileSize') {
e894ae15 266 //temp hack
267 $this->addRule('maxFileSize', ts('Value should be a positive number'), 'positiveInteger');
268 }
269
270 }
271 }
272 // setting_description should be deprecated - see Mail.tpl for metadata based tpl.
273 $this->assign('setting_descriptions', $descriptions);
274 $this->assign('settings_fields', $settingMetaData);
51f35276 275 $this->assign('fields', $this->getSettingsOrderedByWeight());
6be2178c 276 // @todo look at sharing the code below in the settings trait.
277 if ($this->hasReadOnlyFields()) {
278 $this->freeze($this->readOnlyFields);
279 CRM_Core_Session::setStatus(ts("Some fields are loaded as 'readonly' as they have been set (overridden) in civicrm.settings.php."), '', 'info', ['expires' => 0]);
280 }
e894ae15 281 }
282
c5af8245 283 /**
284 * Get the quickform type for the given html type.
285 *
286 * @param array $spec
287 *
288 * @return string
289 */
290 protected function getQuickFormType($spec) {
0e700ee7 291 if (isset($spec['quick_form_type']) &&
292 !($spec['quick_form_type'] === 'Element' && !empty($spec['html_type']))) {
5c33bd6b 293 // This is kinda transitional
c5af8245 294 return $spec['quick_form_type'];
295 }
5c33bd6b 296
297 // The spec for settings has been updated for consistency - we provide deprecation notices for sites that have
298 // not made this change.
299 $htmlType = $spec['html_type'];
300 if ($htmlType !== strtolower($htmlType)) {
6dabf459
ML
301 // Avoiding 'ts' for obscure strings.
302 CRM_Core_Error::deprecatedFunctionWarning('Settings fields html_type should be lower case - see https://docs.civicrm.org/dev/en/latest/framework/setting/ - this needs to be fixed for ' . $spec['name']);
5c33bd6b 303 $htmlType = strtolower($spec['html_type']);
304 }
c5af8245 305 $mapping = [
306 'checkboxes' => 'CheckBoxes',
c89a43b3 307 'checkbox' => 'CheckBox',
c5af8245 308 'radio' => 'Radio',
b70c6629 309 'select' => 'Select',
7399a0a6 310 'textarea' => 'Element',
a7e15692 311 'text' => 'Element',
eba92929 312 'entity_reference' => 'EntityRef',
5c33bd6b 313 'advmultiselect' => 'Element',
0512a83f 314 'chainselect' => 'ChainSelect',
c5af8245 315 ];
8a52ae34 316 $mapping += array_fill_keys(CRM_Core_Form::$html5Types, '');
12c73866 317 return $mapping[$htmlType] ?? '';
c5af8245 318 }
62d3ee27 319
601361a3 320 /**
321 * Get the defaults for all fields defined in the metadata.
322 *
323 * All others are pending conversion.
76c8a771 324 *
325 * @throws \CiviCRM_API3_Exception
326 * @throws \CRM_Core_Exception
601361a3 327 */
328 protected function setDefaultsForMetadataDefinedFields() {
329 CRM_Core_BAO_ConfigSetting::retrieve($this->_defaults);
6821aa1d 330 foreach (array_keys($this->_settings) as $setting) {
601361a3 331 $this->_defaults[$setting] = civicrm_api3('setting', 'getvalue', ['name' => $setting]);
76c8a771 332 $spec = $this->getSettingsMetaData()[$setting];
c11c73cd 333 if (!empty($spec['serialize']) && !is_array($this->_defaults[$setting])) {
334 $this->_defaults[$setting] = CRM_Core_DAO::unSerializeField((string) $this->_defaults[$setting], $spec['serialize']);
6821aa1d 335 }
c5af8245 336 if ($this->getQuickFormType($spec) === 'CheckBoxes') {
6821aa1d 337 $this->_defaults[$setting] = array_fill_keys($this->_defaults[$setting], 1);
338 }
74f89a9f 339 if ($this->getQuickFormType($spec) === 'CheckBox') {
340 $this->_defaults[$setting] = [$setting => $this->_defaults[$setting]];
341 }
6821aa1d 342 }
343 }
344
345 /**
281db812 346 * Save any fields which have been defined via metadata.
347 *
348 * (Other fields are hack-handled... sadly.
349 *
350 * @param array $params
351 * Form input.
6821aa1d 352 *
281db812 353 * @throws \CiviCRM_API3_Exception
6821aa1d 354 */
355 protected function saveMetadataDefinedSettings($params) {
356 $settings = $this->getSettingsToSetByMetadata($params);
357 foreach ($settings as $setting => $settingValue) {
4e086328
CW
358 $settingMetaData = $this->getSettingMetadata($setting);
359 if (!empty($settingMetaData['sortable'])) {
360 $settings[$setting] = $this->getReorderedSettingData($setting, $settingValue);
361 }
362 elseif ($this->getQuickFormType($settingMetaData) === 'CheckBoxes') {
6821aa1d 363 $settings[$setting] = array_keys($settingValue);
364 }
4e086328 365 elseif ($this->getQuickFormType($settingMetaData) === 'CheckBox') {
74f89a9f 366 // This will be an array with one value.
1da4481d 367 $settings[$setting] = (bool) reset($settings[$setting]);
74f89a9f 368 }
601361a3 369 }
6821aa1d 370 civicrm_api3('setting', 'create', $settings);
601361a3 371 }
372
4e086328
CW
373 /**
374 * Display options in correct order on the form
375 *
376 * @param $setting
377 * @param $options
378 * @return array
379 */
380 public static function reorderSortableOptions($setting, $options) {
381 return array_merge(array_flip(Civi::settings()->get($setting)), $options);
382 }
383
384 /**
385 * @param string $setting
386 * @param array $settingValue
387 *
388 * @return array
76c8a771 389 *
390 * @throws \CRM_Core_Exception
4e086328
CW
391 */
392 private function getReorderedSettingData($setting, $settingValue) {
393 // Get order from $_POST as $_POST maintains the order the sorted setting
394 // options were sent. You can simply assign data from $_POST directly to
395 // $settings[] but preference has to be given to data from Quickform.
396 $order = array_keys(\CRM_Utils_Request::retrieve($setting, 'String'));
397 $settingValueKeys = array_keys($settingValue);
398 return array_intersect($order, $settingValueKeys);
399 }
400
71bbdae1 401 /**
402 * Add settings to form if the metadata designates they should be on the page.
403 *
404 * @throws \CiviCRM_API3_Exception
405 */
406 protected function addSettingsToFormFromMetadata() {
407 $filter = $this->getSettingPageFilter();
408 $settings = civicrm_api3('Setting', 'getfields', [])['values'];
409 foreach ($settings as $key => $setting) {
410 if (isset($setting['settings_pages'][$filter])) {
411 $this->_settings[$key] = $setting;
412 }
413 }
414 }
415
946389fb 416}