diff --git a/CRM/Twingle/Form/Profile.php b/CRM/Twingle/Form/Profile.php index 5c4bdc7..ca94dee 100644 --- a/CRM/Twingle/Form/Profile.php +++ b/CRM/Twingle/Form/Profile.php @@ -462,92 +462,27 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form { /** * Validates the profile form. - * - * @param array $values - * The submitted form values, keyed by form element name. - * - * @return bool | array - * TRUE when the form was successfully validated, or an array of error - * messages, keyed by form element name. + * @return bool + * TRUE when the form was successfully validated. */ public function validate() { - $values = $this->exportValues(); - // Validate new profile names. - if ( - isset($values['name']) - && ($values['name'] != $this->profile->getName() || $this->_op != 'edit') - && !empty(CRM_Twingle_Profile::getProfile($values['name'])) - ) { - $this->_errors['name'] = E::ts('A profile with this name already exists.'); - } + if (in_array($this->_op, ['create', 'edit', 'copy'])) { + // Create profile with new values + $profile_values = $this->exportValues(); + $profile = new CRM_Twingle_Profile( + $profile_values['name'], + $profile_values, + $this->profile_id + ); - // Restrict profile names to alphanumeric characters and the underscore. - if (isset($values['name']) && preg_match("/[^A-Za-z0-9\_]/", $values['name'])) { - $this->_errors['name'] = E::ts('Only alphanumeric characters and the underscore (_) are allowed for profile names.'); - } - - // Validate custom field mapping. - try { - if (isset($values['custom_field_mapping'])) { - $custom_field_mapping = preg_split('/\r\n|\r|\n/', $values['custom_field_mapping'], -1, PREG_SPLIT_NO_EMPTY); - if (!is_array($custom_field_mapping)) { - throw new Exception( - E::ts('Could not parse custom field mapping.') - ); - } - foreach ($custom_field_mapping as $custom_field_map) { - $custom_field_map = explode("=", $custom_field_map); - if (count($custom_field_map) !== 2) { - throw new Exception( - E::ts('Could not parse custom field mapping.') - ); - } - list($twingle_field_name, $custom_field_name) = $custom_field_map; - $custom_field_id = substr($custom_field_name, strlen('custom_')); - - // Check for custom field existence - try { - $custom_field = civicrm_api3('CustomField', 'getsingle', array( - 'id' => $custom_field_id, - )); - } - catch (CiviCRM_API3_Exception $exception) { - throw new Exception( - E::ts( - 'Custom field custom_%1 does not exist.', - array(1 => $custom_field_id) - ) - ); - } - - // Only allow custom fields on relevant entities. - try { - $custom_group = civicrm_api3('CustomGroup', 'getsingle', array( - 'id' => $custom_field['custom_group_id'], - 'extends' => array( - 'IN' => array( - 'Contact', - 'Individual', - 'Organization', - 'Contribution', - 'ContributionRecur', - ), - ), - )); - } catch (CiviCRM_API3_Exception $exception) { - throw new Exception( - E::ts( - 'Custom field custom_%1 is not in a CustomGroup that extends one of the supported CiviCRM entities.', - array(1 => $custom_field['id']) - ) - ); - } - } + // Validate profile data + try { + $profile->validate(); + } + catch (ProfileValidationError $e) { + $this->setElementError($e->getAffectedFieldName(), $e->getMessage()); } - } - catch (Exception $exception) { - $this->_errors['custom_field_mapping'] = $exception->getMessage(); } return parent::validate(); diff --git a/CRM/Twingle/Profile.php b/CRM/Twingle/Profile.php index 806e60c..7f784bd 100644 --- a/CRM/Twingle/Profile.php +++ b/CRM/Twingle/Profile.php @@ -94,14 +94,7 @@ class CRM_Twingle_Profile { * @return bool */ public function matches($project_id) { - $selector = $this->getAttribute('selector'); - $project_ids = array_map( - function($project_id) { - return trim($project_id); - }, - explode(',', $selector) - ); - return in_array($project_id, $project_ids); + return in_array($project_id, $this->getProjectIds()); } /** @@ -173,6 +166,20 @@ class CRM_Twingle_Profile { return $this->name == 'default'; } + /** + * Retrieves the profile's project IDs. + * + * @return array + */ + public function getProjectIds(): array { + return array_map( + function($project_id) { + return trim($project_id); + }, + explode(',', $this->getAttribute("selector")) + ); + } + /** * Retrieves an attribute of the profile. * @@ -229,21 +236,138 @@ class CRM_Twingle_Profile { * Verifies whether the profile is valid (i.e. consistent and not colliding * with other profiles). * - * @throws Exception + * @throws \CRM_Twingle_Exceptions_ProfileValidationError + * @throws \Civi\Core\Exception\DBQueryException * When the profile could not be successfully validated. */ - public function verifyProfile() { - // TODO: check - // data of this profile consistent? - // conflicts with other profiles? + public function validate() { + + // Name cannot be empty + if (empty($this->getName())) { + throw new ProfileValidationError( + 'name', + E::ts('Profile name cannot be empty.'), + ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED + ); + } + + // Restrict profile names to alphanumeric characters, space and the underscore. + $contains_illegal_characters = preg_match("/[^A-Za-z0-9_\s]/", $this->getName()); + if ($contains_illegal_characters) { + throw new ProfileValidationError( + 'name', + E::ts('Only alphanumeric characters, space and the underscore (_) are allowed for profile names.'), + ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED + ); + } + + // Check if profile name is already used for other profile + $profile_name_duplicates = array_filter( + CRM_Twingle_Profile::getProfiles(), function($profile) { + return $profile->getName() == $this->getName() && $this->getId() != $profile->getId(); + }); + if (!empty($profile_name_duplicates)) { + throw new ProfileValidationError( + 'name', + E::ts("A profile with the name '%1' already exists.", [1 => $this->getName()]), + ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED + ); + } + + // Check if project_id is already used in other profile + // FIXME: Check is not working + $profiles = $this::getProfiles(); + foreach ($profiles as $profile) { + if ($profile->getId() == $this->getId() || $profile->is_default()) continue; + $project_ids = $this->getProjectIds(); + $id_duplicates = array_intersect($profile->getProjectIds(), $project_ids); + if (!empty($id_duplicates)) { + throw new ProfileValidationError( + 'selector', + E::ts( + "Project ID(s) [%1] already used in profile '%2'.", + [ + 1 => implode(", ", $id_duplicates), + 2 => $profile->getName() + ] + ), + ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED + ); + } + } + + // Validate custom field mapping. + $custom_field_mapping = $this->getAttribute('custom_field_mapping', FALSE); + if ($custom_field_mapping) { + $custom_field_mapping = preg_split('/\r\n|\r|\n/', $custom_field_mapping, -1, PREG_SPLIT_NO_EMPTY); + $parsing_error = new ProfileValidationError( + 'custom_field_mapping', + E::ts('Could not parse custom field mapping.'), + ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED + ); + if (!is_array($custom_field_mapping)) { + throw $parsing_error; + } + foreach ($custom_field_mapping as $custom_field_map) { + $custom_field_map = explode("=", $custom_field_map); + if (count($custom_field_map) !== 2) { + throw $parsing_error; + } + [$twingle_field_name, $custom_field_name] = $custom_field_map; + $custom_field_id = substr($custom_field_name, strlen('custom_')); + + // Check for custom field existence + try { + $custom_field = civicrm_api3( + 'CustomField', 'getsingle', ['id' => $custom_field_id] + ); + } + catch (CiviCRM_API3_Exception $exception) { + throw new ProfileValidationError( + 'custom_field_mapping', + E::ts( + 'Custom field custom_%1 does not exist.', + [1 => $custom_field_id] + ), + ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED + ); + } + + // Only allow custom fields on relevant entities. + try { + civicrm_api3('CustomGroup', 'getsingle', + [ + 'id' => $custom_field['custom_group_id'], + 'extends' => [ + 'IN' => [ + 'Contact', + 'Individual', + 'Organization', + 'Contribution', + 'ContributionRecur', + ], + ], + ]); + } catch (CiviCRM_API3_Exception $exception) { + throw new ProfileValidationError( + 'custom_field_mapping', + E::ts( + 'Custom field custom_%1 is not in a CustomGroup that extends one of the supported CiviCRM entities.', + [1 => $custom_field['id']] + ), + ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED + ); + } + } + } } /** - * Persists the profile within the CiviCRM settings. + * Persists the profile within the database. + * + * @throws \CRM_Twingle_Exceptions_ProfileException */ public function saveProfile() { - // make sure it's valid - $this->verifyProfile(); try { if ($this->id !== NULL) {