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