readOnlyFields);
}
/**
* Get the metadata relating to the settings on the form, ordered by the keys in $this->_settings.
*
* @return array
*/
protected function getSettingsMetaData(): array {
if (empty($this->settingsMetadata)) {
$this->settingsMetadata = \Civi\Core\SettingsMetadata::getMetadata(['name' => array_keys($this->_settings)], NULL, TRUE);
// This array_merge re-orders to the key order of $this->_settings.
$this->settingsMetadata = array_merge($this->_settings, $this->settingsMetadata);
}
return $this->settingsMetadata;
}
/**
* Get the settings which can be stored based on metadata.
*
* @param array $params
* @return array
*/
protected function getSettingsToSetByMetadata($params) {
$setValues = array_intersect_key($params, $this->_settings);
// Checkboxes will be unset rather than empty so we need to add them back in.
// Handle quickform hateability just once, right here right now.
$unsetValues = array_diff_key($this->_settings, $params);
foreach ($unsetValues as $key => $unsetValue) {
$quickFormType = $this->getQuickFormType($this->getSettingMetadata($key));
if ($quickFormType === 'CheckBox') {
$setValues[$key] = [$key => 0];
}
elseif ($quickFormType === 'CheckBoxes') {
$setValues[$key] = [];
}
}
return $setValues;
}
/**
* @param $params
*/
protected function filterParamsSetByMetadata(&$params) {
foreach ($this->getSettingsToSetByMetadata($params) as $setting => $settingGroup) {
//@todo array_diff this
unset($params[$setting]);
}
}
/**
* Get the metadata for a particular field.
*
* @param $setting
* @return mixed
*/
protected function getSettingMetadata($setting) {
return $this->getSettingsMetaData()[$setting];
}
/**
* Get the metadata for a particular field for a particular item.
*
* e.g get 'serialize' key, if exists, for a field.
*
* @param $setting
* @param $item
* @return mixed
*/
protected function getSettingMetadataItem($setting, $item) {
return CRM_Utils_Array::value($item, $this->getSettingsMetaData()[$setting]);
}
/**
* This is public so we can retrieve the filter name via hooks etc. and apply conditional logic (eg. loading javascript conditionals).
*
* @return string
*/
public function getSettingPageFilter() {
if (!isset($this->_filter)) {
// Get the last URL component without modifying the urlPath property.
$urlPath = array_values($this->urlPath);
$this->_filter = end($urlPath);
}
return $this->_filter;
}
/**
* Returns a re-keyed copy of the settings, ordered by weight.
*
* @return array
*/
protected function getSettingsOrderedByWeight() {
$settingMetaData = $this->getSettingsMetaData();
$filter = $this->getSettingPageFilter();
usort($settingMetaData, function ($a, $b) use ($filter) {
// Handle cases in which a comparison is impossible. Such will be considered ties.
if (
// A comparison can't be made unless both setting weights are declared.
!isset($a['settings_pages'][$filter]['weight'], $b['settings_pages'][$filter]['weight'])
// A pair of settings might actually have the same weight.
|| $a['settings_pages'][$filter]['weight'] === $b['settings_pages'][$filter]['weight']
) {
return 0;
}
return $a['settings_pages'][$filter]['weight'] > $b['settings_pages'][$filter]['weight'] ? 1 : -1;
});
return $settingMetaData;
}
/**
* Add fields in the metadata to the template.
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
protected function addFieldsDefinedInSettingsMetadata() {
$this->addSettingsToFormFromMetadata();
$settingMetaData = $this->getSettingsMetaData();
$descriptions = [];
foreach ($settingMetaData as $setting => $props) {
$quickFormType = $this->getQuickFormType($props);
if (isset($quickFormType)) {
$options = $props['options'] ?? NULL;
if ($options) {
if ($quickFormType === 'Select' && isset($props['is_required']) && $props['is_required'] === FALSE && !isset($options[''])) {
// If the spec specifies the field is not required add a null option.
// Why not if empty($props['is_required']) - basically this has been added to the spec & might not be set to TRUE
// when it is true.
$options = ['' => ts('None')] + $options;
}
}
if ($props['type'] === 'Boolean') {
$options = [$props['title'] => $props['name']];
}
//Load input as readonly whose values are overridden in civicrm.settings.php.
if (Civi::settings()->getMandatory($setting) !== NULL) {
$props['html_attributes']['readonly'] = TRUE;
$this->readOnlyFields[] = $setting;
}
$add = 'add' . $quickFormType;
if ($add === 'addElement') {
$this->$add(
$props['html_type'],
$setting,
$props['title'],
($options !== NULL) ? $options : CRM_Utils_Array::value('html_attributes', $props, []),
($options !== NULL) ? CRM_Utils_Array::value('html_attributes', $props, []) : NULL
);
}
elseif ($add === 'addSelect') {
$this->addElement('select', $setting, $props['title'], $options, CRM_Utils_Array::value('html_attributes', $props));
}
elseif ($add === 'addCheckBox') {
$this->addCheckBox($setting, '', $options, NULL, CRM_Utils_Array::value('html_attributes', $props), NULL, NULL, [' ']);
}
elseif ($add === 'addCheckBoxes') {
$newOptions = array_flip($options);
$classes = 'crm-checkbox-list';
if (!empty($props['sortable'])) {
$classes .= ' crm-sortable-list';
$newOptions = array_flip(self::reorderSortableOptions($setting, $options));
}
$settingMetaData[$setting]['wrapper_element'] = ['
'];
$this->addCheckBox($setting,
$props['title'],
$newOptions,
NULL, NULL, NULL, NULL,
''
);
}
elseif ($add === 'addChainSelect') {
$this->addChainSelect($setting, ['label' => $props['title']] + $props['chain_select_settings']);
}
elseif ($add === 'addMonthDay') {
$this->add('date', $setting, $props['title'], CRM_Core_SelectValues::date(NULL, 'M d'));
}
elseif ($add === 'addEntityRef') {
$this->$add($setting, $props['title'], $props['entity_reference_options']);
}
elseif ($add === 'addYesNo' && ($props['type'] === 'Boolean')) {
$this->addRadio($setting, $props['title'], [1 => ts('Yes'), 0 => ts('No')], CRM_Utils_Array::value('html_attributes', $props), ' ');
}
elseif ($add === 'add') {
$this->add($props['html_type'], $setting, $props['title'], $options, FALSE, $props['html_extra'] ?? NULL);
}
else {
$this->$add($setting, $props['title'], $options);
}
// Migrate to using an array as easier in smart...
$description = $props['description'] ?? NULL;
$descriptions[$setting] = $description;
$this->assign("{$setting}_description", $description);
if ($setting === 'max_attachments') {
//temp hack @todo fix to get from metadata
$this->addRule('max_attachments', ts('Value should be a positive number'), 'positiveInteger');
}
if ($setting === 'max_attachments_backend') {
//temp hack @todo fix to get from metadata
$this->addRule('max_attachments_backend', ts('Value should be a positive number'), 'positiveInteger');
}
if ($setting === 'maxFileSize') {
//temp hack
$this->addRule('maxFileSize', ts('Value should be a positive number'), 'positiveInteger');
}
}
}
// setting_description should be deprecated - see Mail.tpl for metadata based tpl.
$this->assign('setting_descriptions', $descriptions);
$this->assign('settings_fields', $settingMetaData);
$this->assign('fields', $this->getSettingsOrderedByWeight());
// @todo look at sharing the code below in the settings trait.
if ($this->hasReadOnlyFields()) {
$this->freeze($this->readOnlyFields);
CRM_Core_Session::setStatus(ts("Some fields are loaded as 'readonly' as they have been set (overridden) in civicrm.settings.php."), '', 'info', ['expires' => 0]);
}
}
/**
* Get the quickform type for the given html type.
*
* @param array $spec
*
* @return string
*/
protected function getQuickFormType($spec) {
if (isset($spec['quick_form_type']) &&
!($spec['quick_form_type'] === 'Element' && !empty($spec['html_type']))) {
// This is kinda transitional
return $spec['quick_form_type'];
}
// The spec for settings has been updated for consistency - we provide deprecation notices for sites that have
// not made this change.
$htmlType = $spec['html_type'];
if ($htmlType !== strtolower($htmlType)) {
// Avoiding 'ts' for obscure strings.
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']);
$htmlType = strtolower($spec['html_type']);
}
$mapping = [
'checkboxes' => 'CheckBoxes',
'checkbox' => 'CheckBox',
'radio' => 'Radio',
'select' => 'Select',
'textarea' => 'Element',
'text' => 'Element',
'entity_reference' => 'EntityRef',
'advmultiselect' => 'Element',
'chainselect' => 'ChainSelect',
];
$mapping += array_fill_keys(CRM_Core_Form::$html5Types, '');
return $mapping[$htmlType] ?? '';
}
/**
* Get the defaults for all fields defined in the metadata.
*
* All others are pending conversion.
*
* @throws \CiviCRM_API3_Exception
* @throws \CRM_Core_Exception
*/
protected function setDefaultsForMetadataDefinedFields() {
CRM_Core_BAO_ConfigSetting::retrieve($this->_defaults);
foreach (array_keys($this->_settings) as $setting) {
$this->_defaults[$setting] = civicrm_api3('setting', 'getvalue', ['name' => $setting]);
$spec = $this->getSettingsMetaData()[$setting];
if (!empty($spec['serialize']) && !is_array($this->_defaults[$setting])) {
$this->_defaults[$setting] = CRM_Core_DAO::unSerializeField((string) $this->_defaults[$setting], $spec['serialize']);
}
if ($this->getQuickFormType($spec) === 'CheckBoxes') {
$this->_defaults[$setting] = array_fill_keys($this->_defaults[$setting], 1);
}
if ($this->getQuickFormType($spec) === 'CheckBox') {
$this->_defaults[$setting] = [$setting => $this->_defaults[$setting]];
}
}
}
/**
* Save any fields which have been defined via metadata.
*
* (Other fields are hack-handled... sadly.
*
* @param array $params
* Form input.
*
* @throws \CiviCRM_API3_Exception
*/
protected function saveMetadataDefinedSettings($params) {
$settings = $this->getSettingsToSetByMetadata($params);
foreach ($settings as $setting => $settingValue) {
$settingMetaData = $this->getSettingMetadata($setting);
if (!empty($settingMetaData['sortable'])) {
$settings[$setting] = $this->getReorderedSettingData($setting, $settingValue);
}
elseif ($this->getQuickFormType($settingMetaData) === 'CheckBoxes') {
$settings[$setting] = array_keys($settingValue);
}
elseif ($this->getQuickFormType($settingMetaData) === 'CheckBox') {
// This will be an array with one value.
$settings[$setting] = (bool) reset($settings[$setting]);
}
}
civicrm_api3('setting', 'create', $settings);
}
/**
* Display options in correct order on the form
*
* @param $setting
* @param $options
* @return array
*/
public static function reorderSortableOptions($setting, $options) {
return array_merge(array_flip(Civi::settings()->get($setting)), $options);
}
/**
* @param string $setting
* @param array $settingValue
*
* @return array
*
* @throws \CRM_Core_Exception
*/
private function getReorderedSettingData($setting, $settingValue) {
// Get order from $_POST as $_POST maintains the order the sorted setting
// options were sent. You can simply assign data from $_POST directly to
// $settings[] but preference has to be given to data from Quickform.
$order = array_keys(\CRM_Utils_Request::retrieve($setting, 'String'));
$settingValueKeys = array_keys($settingValue);
return array_intersect($order, $settingValueKeys);
}
/**
* Add settings to form if the metadata designates they should be on the page.
*
* @throws \CiviCRM_API3_Exception
*/
protected function addSettingsToFormFromMetadata() {
$filter = $this->getSettingPageFilter();
$settings = civicrm_api3('Setting', 'getfields', [])['values'];
foreach ($settings as $key => $setting) {
if (isset($setting['settings_pages'][$filter])) {
$this->_settings[$key] = $setting;
}
}
}
}