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