diff --git a/CRM/TwingleCampaign/BAO/Campaign.php b/CRM/TwingleCampaign/BAO/Campaign.php index 593a358..316aa3f 100644 --- a/CRM/TwingleCampaign/BAO/Campaign.php +++ b/CRM/TwingleCampaign/BAO/Campaign.php @@ -33,7 +33,7 @@ abstract class CRM_TwingleCampaign_BAO_Campaign { * @param array $values * @param int|null $id */ - protected function __construct(array $values, int $id = NULL) { + protected function __construct(array $values = [], int $id = NULL) { $this->id = $id; $tmpClassName = explode('_', get_class($this)); diff --git a/CRM/TwingleCampaign/BAO/TwingleApiCall.php b/CRM/TwingleCampaign/BAO/TwingleApiCall.php index 20d8c68..4ae2536 100644 --- a/CRM/TwingleCampaign/BAO/TwingleApiCall.php +++ b/CRM/TwingleCampaign/BAO/TwingleApiCall.php @@ -129,10 +129,14 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { */ public function pushProject(array $project): array { - $projectOptions = $project['project_options']; - unset($project['project_options']); - $paymentMethods = $project['payment_methods']; - unset($project['payment_methods']); + if (isset($project['project_options'])) { + $projectOptions = $project['project_options']; + unset($project['project_options']); + } + if (isset($project['payment_methods'])) { + $paymentMethods = $project['payment_methods']; + unset($project['payment_methods']); + } try { if (!isset($project['id'])) { @@ -153,32 +157,40 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { } // Post project_options - $updatedProject['project_options'] = - $this->curlPost($url . '/options', $projectOptions); + if (isset($projectOptions)) { + $updatedProject['project_options'] = + $this->curlPost($url . '/options', $projectOptions); + } + else { + $updatedProject['project_options'] = $this->getProjectOptions($updatedProject['id']); + } // Post payment_methods - $this->curlPost($url . '/payment-methods', $paymentMethods); - $updatedProject['payment_methods'] = - $this->getProjectPaymentMethods($updatedProject['id']); - - // Set last update time - $updatedProject['last_update'] = max( - $updatedProject['last_update'], - $updatedProject['project_options']['last_update'], - $updatedProject['payment_methods']['updated_at'] - ); - unset($updatedProject['project_options']['last_update']); - unset($updatedProject['payment_methods']['updated_at']); - - return $updatedProject; - } catch (Exception $e) { - throw new Exception( - E::SHORT_NAME . 'Call to Twingle API failed: ' . - $e->getMessage() - ); - } + if (isset ($paymentMethods)) { + $this->curlPost($url . '/payment-methods', $paymentMethods); + } + $updatedProject['payment_methods'] = + $this->getProjectPaymentMethods($updatedProject['id']); + // Set last update time + $updatedProject['last_update'] = max( + $updatedProject['last_update'], + $updatedProject['project_options']['last_update'], + $updatedProject['payment_methods']['updated_at'] + ); + unset($updatedProject['project_options']['last_update']); + unset($updatedProject['payment_methods']['updated_at']); + + return $updatedProject; + } + catch + (Exception $e) { + throw new Exception( + E::SHORT_NAME . 'Call to Twingle API failed: ' . + $e->getMessage() + ); + } } @@ -194,48 +206,48 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { * @throws Exception */ public function getEvent(int $projectId, int $eventId = NULL): array { - $result = []; + $result = []; - // Construct url for curl - $url = empty($eventId) - ? $this->protocol . 'project' . $this->baseUrl . $projectId . '/event' - : $this->protocol . 'project' . $this->baseUrl . $projectId . '/event/' - . $eventId; + // Construct url for curl + $url = empty($eventId) + ? $this->protocol . 'project' . $this->baseUrl . $projectId . '/event' + : $this->protocol . 'project' . $this->baseUrl . $projectId . '/event/' + . $eventId; - $offset = 0; - $finished = FALSE; + $offset = 0; + $finished = FALSE; - // Get only as much results per call as configured in $this->limit - while (!$finished) { - $params = [ - 'orderby' => 'id', - 'direction' => 'desc', - 'limit' => $this->limit, - 'offset' => $offset, - 'image' => 'as-boolean', - 'public' => 0, - ]; - $response = $this->curlGet($url, $params); + // Get only as much results per call as configured in $this->limit + while (!$finished) { + $params = [ + 'orderby' => 'id', + 'direction' => 'desc', + 'limit' => $this->limit, + 'offset' => $offset, + 'image' => 'as-boolean', + 'public' => 0, + ]; + $response = $this->curlGet($url, $params); - // If no $eventId was given, expect one or more events. - // Store the events, increase the offset and ask again until there - // are no more events incoming. - if (!$eventId) { - $result = array_merge($result, $response['data']); - $offset = $offset + $this->limit; - $finished = count($response['data']) < $this->limit; - } - // If $eventId was given, expect only one event - else { - // If the response array contains 'message', the $eventId does not exist - if (!$response['message']) { - $result = $response; + // If no $eventId was given, expect one or more events. + // Store the events, increase the offset and ask again until there + // are no more events incoming. + if (!$eventId) { + $result = array_merge($result, $response['data']); + $offset = $offset + $this->limit; + $finished = count($response['data']) < $this->limit; + } + // If $eventId was given, expect only one event + else { + // If the response array contains 'message', the $eventId does not exist + if (!$response['message']) { + $result = $response; + } + $finished = TRUE; } - $finished = TRUE; } + return $result; } - return $result; - } /** * ## Get project embed data @@ -248,47 +260,49 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { */ public function getProjectEmbedData($projectId): array { - $result = $this->getProject($projectId); + $result = $this->getProject($projectId); - if ($result['embed']) { - // Include counter url into embed data - $result['embed']['counter'] = $result['counter-url']['url']; + if ($result['embed']) { + // Include counter url into embed data + $result['embed']['counter'] = $result['counter-url']['url']; - return $result['embed']; + return $result['embed']; + } + else { + throw new Exception( + "Could not get embed data for project $projectId." + ); + } } - else { - throw new Exception( - "Could not get embed data for project $projectId." - ); - } - } /** * ## Get project options * Gets all project options from the Twingle API + * * @param $projectId * * @return array * @throws \Exception */ public function getProjectOptions($projectId): array { - $url = $this->protocol . 'project' . $this->baseUrl . $projectId . '/options'; - return $this->curlGet($url); - } + $url = $this->protocol . 'project' . $this->baseUrl . $projectId . '/options'; + return $this->curlGet($url); + } /** * ## Get project payment methods * Gets all project payment methods from the Twingle API + * * @param $projectId * * @return array * @throws \Exception */ public function getProjectPaymentMethods($projectId): array { - $url = $this->protocol . 'project' . $this->baseUrl . $projectId - . '/payment-methods'; - return $this->curlGet($url); - } + $url = $this->protocol . 'project' . $this->baseUrl . $projectId + . '/payment-methods'; + return $this->curlGet($url); + } /** @@ -301,9 +315,9 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { * @throws Exception */ public function deleteProject(int $projectId): bool { - $url = $this->protocol . 'project' . $this->baseUrl . $projectId; - return $this->curlDelete($url); - } + $url = $this->protocol . 'project' . $this->baseUrl . $projectId; + return $this->curlDelete($url); + } /** @@ -319,10 +333,10 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { * @throws Exception */ public function deleteEvent(int $projectId, int $eventId): bool { - $url = $this->protocol . 'project' . $this->baseUrl . $projectId . - '/event/' . $eventId; - return $this->curlDelete($url); - } + $url = $this->protocol . 'project' . $this->baseUrl . $projectId . + '/event/' . $eventId; + return $this->curlDelete($url); + } /** @@ -360,7 +374,8 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { } if ($curl_status_code == 404) { throw new Exception('http status code 404 (not found)'); - } elseif ($curl_status_code == 500) { + } + elseif ($curl_status_code == 500) { throw new Exception('https status code 500 (internal error)'); } return $response; @@ -402,10 +417,11 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { } if ($curl_status_code == 404) { throw new Exception('http status code 404 (not found)'); - } elseif ($curl_status_code == 500) { + } + elseif ($curl_status_code == 500) { throw new Exception('https status code 500 (internal error)'); } - if (sizeof($response) == 1 && isset($response['message'])){ + if (sizeof($response) == 1 && isset($response['message'])) { throw new Exception($response['message']); } return $response; @@ -448,7 +464,8 @@ class CRM_TwingleCampaign_BAO_TwingleApiCall { } if ($curl_status_code == 404) { throw new Exception('http status code 404 (not found)'); - } elseif ($curl_status_code == 500) { + } + elseif ($curl_status_code == 500) { throw new Exception('https status code 500 (internal error)'); } return ($curl_status_code == 200); diff --git a/CRM/TwingleCampaign/BAO/TwingleCampaign.php b/CRM/TwingleCampaign/BAO/TwingleCampaign.php index f7e8521..58433ed 100644 --- a/CRM/TwingleCampaign/BAO/TwingleCampaign.php +++ b/CRM/TwingleCampaign/BAO/TwingleCampaign.php @@ -26,7 +26,7 @@ class CRM_TwingleCampaign_BAO_TwingleCampaign { * * @throws \CiviCRM_API3_Exception */ - public function __construct(array $values, int $id = NULL) { + public function __construct(array $values = [], int $id = NULL) { $this->prefix = 'twingle_campaign_'; $this->id = $id ?? NULL; diff --git a/CRM/TwingleCampaign/BAO/TwingleEvent.php b/CRM/TwingleCampaign/BAO/TwingleEvent.php index f1c4473..19de97b 100644 --- a/CRM/TwingleCampaign/BAO/TwingleEvent.php +++ b/CRM/TwingleCampaign/BAO/TwingleEvent.php @@ -19,7 +19,7 @@ class CRM_TwingleCampaign_BAO_TwingleEvent extends Campaign { * * @throws \Exception */ - public function __construct(array $event, int $id = NULL) { + public function __construct(array $event = [], int $id = NULL) { parent::__construct($event, $id); $this->prefix = 'twingle_event_'; diff --git a/CRM/TwingleCampaign/BAO/TwingleProject.php b/CRM/TwingleCampaign/BAO/TwingleProject.php index 4035ecc..7e24868 100644 --- a/CRM/TwingleCampaign/BAO/TwingleProject.php +++ b/CRM/TwingleCampaign/BAO/TwingleProject.php @@ -40,7 +40,7 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * @param int|null $id * CiviCRM Campaign id */ - public function __construct(array $values, int $id = NULL) { + public function __construct(array $values = [], int $id = NULL) { parent::__construct($values, $id); $this->prefix = 'twingle_project_'; @@ -85,7 +85,9 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * ## Int to bool * Changes all project values that are defined as CiviCRM 'Boolean' types * from strings to booleans. + * * @param array $values + * * @throws \Exception */ private function intToBool(array &$values) { @@ -104,7 +106,10 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { } elseif (in_array($key, $boolArrays)) { foreach ($values[$key] as $_key => $_value) { - if (is_numeric($_value) && $_value < 2 || empty($_value)) { + if (is_bool($_value)) { + // nothing to do here + } + elseif (is_numeric($_value) && $_value < 2 || empty($_value)) { $values[$key][$_key] = (bool) $_value; } else { @@ -122,7 +127,9 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * ## Int to bool * Changes all project values that are strings but originally came as integers * back to integers. + * * @param array $values + * * @throws \Exception */ private function strToInt(array &$values) { @@ -214,8 +221,33 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { } /** - * ## Clone this TwingleProject + * ## Complement campaign values + * Complement existing campaign values with new ones * + * @param array $arrayToComplement + * + * @overrides CRM_TwingleCampaign_BAO_Campaign + */ + public function complement(array $arrayToComplement) { + + // Set all contact fields to false + if ( + isset($arrayToComplement['values']['project_options']['contact_fields']) + ) { + foreach ( + $arrayToComplement['values']['project_options']['contact_fields'] + as $key => $value + ) { + $arrayToComplement['values']['project_options']['contact_fields'][$key] + = FALSE; + } + } + + parent::complement($arrayToComplement); + } + + /** + * ## Clone this TwingleProject * This method removes the id and the identifier from this instance and in * the next step it pushes the clone as a new project with the same values to * Twingle. @@ -223,11 +255,163 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * @throws \Exception */ public function clone() { - $this->values['id'] = 0; - $this->values['identifier'] = 0; + $this->values['id'] = ''; + $this->values['identifier'] = ''; $this->create(); // this will also trigger the postSave hook } + /** + * ## Validate + * Validates project values and returns an array containing the result and + * another array with eventual error messages. + * + * @return array ['valid' => bool, 'messages' => array] + */ + public function validate(): array { + $valid = TRUE; + $messages = []; + + // Validate email address + if ( + !filter_var( + $this->values['project_options']['bcc_email_address'], + FILTER_VALIDATE_EMAIL + ) + && !empty($this->values['project_options']['bcc_email_address']) + ) { + $valid = FALSE; + $messages[] = E::ts("BCC email invalid"); + } + + // Validate hexadecimal color fields + $colorFields = + [ + 'design_background_color', + 'design_primary_color', + 'design_font_color', + ]; + foreach ($colorFields as $colorField) { + if ( + !empty($this->values['project_options'][$colorField]) && + ( + !( + ctype_xdigit($this->values['project_options'][$colorField]) || + is_integer($this->values['project_options'][$colorField]) + ) || + strlen((string) $this->values['project_options'][$colorField]) > 6 + ) + ) { + $valid = FALSE; + $messages[] = + E::ts("Invalid hexadecimal value in color field: %1", + [1 => $colorField]); + } + } + + // Check if donation values are integers and if proposed donation value + // lies between max and min values + if ( + // Is integer and >= 0 or empty + ( + empty($this->values['project_options']['donation_value_default']) || + ( + is_integer($this->values['project_options']['donation_value_default']) || + ctype_digit($this->values['project_options']['donation_value_default']) + ) && ( + $this->values['project_options']['donation_value_default'] >= 0 + ) + ) && ( + empty($this->values['project_options']['donation_value_min']) || + ( + is_integer($this->values['project_options']['donation_value_min']) || + ctype_digit($this->values['project_options']['donation_value_min']) + ) && ( + $this->values['project_options']['donation_value_max'] >= 0 + ) + ) && ( + empty($this->values['project_options']['donation_value_max']) || + ( + is_integer($this->values['project_options']['donation_value_max']) || + ctype_digit($this->values['project_options']['donation_value_max']) + ) && ( + $this->values['project_options']['donation_value_max'] >= 0 + ) + ) + ) { + if ( + // all empty + empty($this->values['project_options']['donation_value_default']) && + empty($this->values['project_options']['donation_value_min']) && + empty($this->values['project_options']['donation_value_max']) + ) { + // nothing to validate + } + elseif ( + // Max empty, min not empty + (!empty($this->values['project_options']['donation_value_min']) && + empty($this->values['project_options']['donation_value_max'])) || + // Max empty, default not empty + (!empty($this->values['project_options']['donation_value_default']) && + empty($this->values['project_options']['donation_value_max'])) + ) { + $valid = FALSE; + $messages[] = + E::ts("Missing maximum donation value"); + } + else { + if ( + // Min >= Max + $this->values['project_options']['donation_value_min'] >= + $this->values['project_options']['donation_value_max'] + ) { + $valid = FALSE; + $messages[] = + E::ts("Maximum donation value must be higher than the minimum"); + } + elseif ( + // Default < min or default > max + $this->values['project_options']['donation_value_default'] < + $this->values['project_options']['donation_value_min'] || + $this->values['project_options']['donation_value_default'] > + $this->values['project_options']['donation_value_max'] + ) { + $valid = FALSE; + $messages[] = + E::ts("Default donation value must lie in between maximum and minimum values"); + } + } + } + else { + $valid = FALSE; + $messages[] = + E::ts("Donation values (Min, Max, Default) must be positive integers"); + } + + // Validate sharing url + $urlFields = + [ + 'custom_css', + 'share_url', + ]; + + foreach ($urlFields as $urlField) { + if (!empty($this->values['project_options'][$urlField])) { + if ( + !filter_var( + $this->values['project_options'][$urlField], + FILTER_VALIDATE_URL + ) || empty($this->values['project_options'][$urlField]) + ) { + $valid = FALSE; + $messages[] = + E::ts("Invalid URL: %1", [1 => $urlField]); + } + } + } + + return ['valid' => $valid, 'messages' => $messages]; + } + /** * ## Translate values between CiviCRM Campaigns and Twingle formats @@ -246,7 +430,8 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * * @throws Exception */ - public static function formatValues(array &$values, string $direction) { + public + static function formatValues(array &$values, string $direction) { if ($direction == self::IN) { @@ -467,7 +652,8 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * @return array * */ - public function getResponse(string $status = NULL): array { + public + function getResponse(string $status = NULL): array { $project_type = empty($this->values['type']) ? 'default' : $this->values['type']; $response = [ @@ -489,7 +675,8 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * * @return int|string|null */ - public function lastUpdate() { + public + function lastUpdate(): ?int { return self::getTimestamp($this->values['last_update']); } @@ -500,8 +687,72 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * * @return int */ - public function getProjectId(): int { + public + function getProjectId(): int { return (int) $this->values['id']; } + /** + * ## Get the payment methods array of this project + * + * @return array + */ + public function getValues(): array { + if (isset($this->values)) { + return $this->values; + } + else { + return []; + } + } + + /** + * ## Get the project options array of this project + * + * @return array + */ + public function getOptions(): array { + if (isset($this->values['project_options'])) { + return $this->values['project_options']; + } + else { + return []; + } + } + + /** + * ## Get the payment methods array of this project + * + * @return array + */ + public function getPaymentMethods(): array { + if (isset($this->values['payment_methods'])) { + return $this->values['payment_methods']; + } + else { + return []; + } + } + + /** + * ## Get the payment methods array of this project + */ + public function deleteValues(): void { + unset ($this->values); + } + + /** + * ## Get the project options array of this project + */ + public function deleteOptions(): void { + unset($this->values['project_options']); + } + + /** + * ## Get the payment methods array of this project + */ + public function deletePaymentMethods(): void { + unset($this->values['payment_methods']); + } + } diff --git a/api/v3/TwingleProject/Sync.php b/api/v3/TwingleProject/Sync.php index aae2b47..5792a5b 100644 --- a/api/v3/TwingleProject/Sync.php +++ b/api/v3/TwingleProject/Sync.php @@ -131,6 +131,20 @@ function civicrm_api3_twingle_project_Sync(array $params): array { // instantiate project $project = new TwingleProject($result, $id); + // Send project information to Tingle and update project with the + // answer + $projectOptions = $project->getOptions(); + $project->deleteOptions(); + $paymentMethods = $project->getPaymentMethods(); + $project->deletePaymentMethods(); + $projectFromTwingle = $twingleApi->pushProject($project->export()); + $project = new TwingleProject($projectFromTwingle, $project->getId()); + $projectValues = $project->getValues(); + $projectValues['project_options'] = $projectOptions; + $projectValues['payment_methods'] = $paymentMethods; + $project->update($projectValues); + $project->complement($projectFromTwingle); + // Push project to Twingle return _pushProjectToTwingle($project, $twingleApi, $params); } @@ -342,17 +356,19 @@ function _updateProjectLocally(array $project_from_twingle, * @param \CRM_TwingleCampaign_BAO_TwingleProject $project * @param \CRM_TwingleCampaign_BAO_TwingleApiCall $twingleApi * @param array $params + * @param bool $update Update project after push? * * @return array * @throws \CiviCRM_API3_Exception */ function _pushProjectToTwingle(TwingleProject $project, TwingleApiCall $twingleApi, - array $params): array { + array $params = [], + bool $update = TRUE): array { // If this is a test, do not make db changes - if ($params['is_test']) { - $response[$project->getId] = + if (isset($params['is_test']) && $params['is_test']) { + $response[$project->getId()] = $project->getResponse('TwingleProject ready to push to Twingle'); return civicrm_api3_create_success( $response, @@ -379,43 +395,55 @@ function _pushProjectToTwingle(TwingleProject $project, } // Update local campaign with data returning from Twingle - if ($result) { - $project->update($result); - try { - // Create updated campaign - $project->create(TRUE); - $response[$project->getId()] = - $project->getResponse('TwingleProject pushed to Twingle'); - return civicrm_api3_create_success( - $response, - $params, - 'TwingleProject', - 'Sync' - ); - } catch (Exception $e) { + if ($update) { + if ($result) { + $project->update($result); + try { + // Create updated campaign + $project->create(TRUE); + $response[$project->getId()] = + $project->getResponse('TwingleProject pushed to Twingle'); + return civicrm_api3_create_success( + $response, + $params, + 'TwingleProject', + 'Sync' + ); + } catch (Exception $e) { + Civi::log()->error( + E::LONG_NAME . + ' pushed TwingleProject to Twingle but local update failed: ' . + $e->getMessage(), + $project->getResponse() + ); + return civicrm_api3_create_error( + 'TwingleProject was pushed to Twingle but local update failed: ' . + $e->getMessage(), + $project->getResponse() + ); + } + } + // If the curl fails, the $result may be empty + else { Civi::log()->error( E::LONG_NAME . - ' pushed TwingleProject to Twingle but local update failed: ' . - $e->getMessage(), + ' could not push TwingleProject campaign', $project->getResponse() ); return civicrm_api3_create_error( - 'TwingleProject was pushed to Twingle but local update failed: ' . - $e->getMessage(), + "Could not push TwingleProject campaign", $project->getResponse() ); } } - // If the curl fails, the $result may be empty else { - Civi::log()->error( - E::LONG_NAME . - ' could not push TwingleProject campaign', - $project->getResponse() - ); - return civicrm_api3_create_error( - "Could not push TwingleProject campaign", - $project->getResponse() + $response[$project->getId()] = + $project->getResponse('TwingleProject pushed to Twingle'); + return civicrm_api3_create_success( + $response, + $params, + 'TwingleProject', + 'Sync' ); } } diff --git a/twinglecampaign.php b/twinglecampaign.php index ef73167..e210c34 100644 --- a/twinglecampaign.php +++ b/twinglecampaign.php @@ -1,6 +1,8 @@ id, $dao->campaign_type_id] - ); + + if (_validateAndSendInput($dao->id, $dao->campaign_type_id)) { + + CRM_Core_Transaction::addCallback( + CRM_Core_Transaction::PHASE_POST_COMMIT, + 'twinglecampaign_postSave_campaign_update_callback', + [$dao->id, $dao->campaign_type_id] + ); + } } + // If the transaction is already finished, call the function directly else { - twinglecampaign_postSave_campaign_callback($dao->id, $dao->campaign_type_id); + twinglecampaign_postSave_campaign_update_callback($dao->id, $dao->campaign_type_id); } } @@ -89,21 +97,13 @@ function twinglecampaign_civicrm_postSave_civicrm_campaign($dao) { * * @throws \CiviCRM_API3_Exception */ -function twinglecampaign_postSave_campaign_callback ( +function twinglecampaign_postSave_campaign_update_callback( int $campaign_id, int $campaign_type_id ) { - // Get campaign type id for TwingleProject - $twingle_project_campaign_type_id = - ExtensionCache::getInstance() - ->getCampaigns()['campaign_types']['twingle_project']['id']; - - // Get campaign type id for TwingleCampaign - $twingle_campaign_campaign_type_id = - ExtensionCache::getInstance() - ->getCampaigns()['campaign_types']['twingle_campaign']['id']; - + $twingle_project_campaign_type_id = _get_campaign_type_id_twingle_project(); + $twingle_campaign_campaign_type_id = _get_campaign_type_id_twingle_campaign(); // If $campaign_type_id is a TwingleProject or TwingleCampaign campaign, // synchronize it @@ -121,15 +121,14 @@ function twinglecampaign_postSave_campaign_callback ( } if (isset($_POST['action'])) { - if ($_POST['action'] == 'clone') { + if ($_POST['action'] == 'clone' && $entity == 'TwingleProject') { unset($_POST['action']); - $result = civicrm_api3($entity, 'getsingle', + $result = civicrm_api3('TwingleProject', 'getsingle', ['id' => $campaign_id] - )['values'][$campaign_id]; - $className = 'CRM_TwingleCampaign_BAO_' . $entity; + ); $id = $result['id']; unset($result['id']); - $project = new $className($result, $id); + $project = new TwingleProject($result, $id); try { $project->clone(); } catch (Exception $e) { @@ -137,58 +136,232 @@ function twinglecampaign_postSave_campaign_callback ( E::LONG_NAME . ' could not clone ' . $entity . ': ' . $e->getMessage() ); - CRM_Utils_System::setUFMessage($entity . ' could not get cloned.'); - } - } - elseif ($entity == 'TwingleProject') { - try { - civicrm_api3('TwingleProject', 'sync', ['id' => $campaign_id]); - CRM_Utils_System::setUFMessage('TwingleProject was saved.'); - } catch (CiviCRM_API3_Exception $e) { - Civi::log()->error( - 'twinglecampaign_postSave_callback ' . $e->getMessage() - ); - } - } - else { - try { - civicrm_api3('TwingleCampaign', 'create', ['id' => $campaign_id]); - CRM_Utils_System::setUFMessage('TwingleCampaign was saved.'); - } catch (CiviCRM_API3_Exception $e) { - Civi::log()->error( - 'twinglecampaign_postSave_callback ' . $e->getMessage() + CRM_Core_Session::setStatus( + $e->getMessage(), + E::ts("Campaign cloning failed"), + error, + [unique => TRUE] ); } } } + + // If a TwingleProject is getting saved elseif ($entity == 'TwingleProject') { - // Also synchronize all child TwingleCampaign campaigns + + // Synchronize all child TwingleCampaign campaigns try { - civicrm_api3('TwingleCampaign', 'sync', ['project_id' => $campaign_id]); + civicrm_api3( + 'TwingleCampaign', + 'sync', + ['parent_id' => $campaign_id]); + } catch (CiviCRM_API3_Exception $e) { + CRM_Core_Session::setStatus( + $e->getMessage(), + E::ts("TwingleCampaign update failed"), + error, [unique => TRUE] + ); + Civi::log()->error( + E::SHORT_NAME . + ' Update of TwingleCampaigns failed: ' . $e->getMessage() + ); + } + } + else { + try { + civicrm_api3('TwingleCampaign', 'create', + ['id' => $campaign_id, 'parent_id' => $_POST['parent_id']]); + CRM_Utils_System::setUFMessage(E::ts('TwingleCampaign was saved.')); } catch (CiviCRM_API3_Exception $e) { Civi::log()->error( 'twinglecampaign_postSave_callback ' . $e->getMessage() ); } - try { - civicrm_api3('TwingleProject', 'sync', ['id' => $campaign_id]); - CRM_Utils_System::setUFMessage('TwingleProject was saved.'); + } + } +} function _get_campaign_type_id_twingle_project() { return ExtensionCache::getInstance() - ->getCampaignIds()['campaign_types']['twingle_project']['id']; + ->getCampaignIds()['campaign_types']['twingle_project']['id']; } function _get_campaign_type_id_twingle_campaign() { return ExtensionCache::getInstance() - ->getCampaignIds()['campaign_types']['twingle_campaign']['id']; + ->getCampaignIds()['campaign_types']['twingle_campaign']['id']; } - 'twinglecampaign_postSave_callback ' . $e->getMessage() - ); +/** + * Callback to sync a project after its creation. + * @param int $campaign_id + */ +function twinglecampaign_postSave_project_create_callback( + int $campaign_id +) { + try { + civicrm_api3( + 'TwingleProject', + 'sync', + ['id' => $campaign_id]); + } catch (Exception $e) { + CRM_Core_Session::setStatus( + $e->getMessage(), + E::ts("TwingleProject creation failed"), + error, [unique => TRUE] + ); + Civi::log()->error( + E::SHORT_NAME . + ' Update of TwingleProject creation failed: ' . $e->getMessage() + ); + } +} + +/** + * First validate and then sends the input of this transaction to Twinge. + * If the call to the Twingle API succeeded, this function returns TRUE; + * + * @param $id + * @param $campaign_type_id + * + * @return bool + * @throws \CiviCRM_API3_Exception + */ +function _validateAndSendInput($id, $campaign_type_id): bool { + + // Set callback for cloning + if (isset($_POST['action'])) { + CRM_Core_Transaction::addCallback( + CRM_Core_Transaction::PHASE_POST_COMMIT, + 'twinglecampaign_postSave_campaign_update_callback', + [$id, $campaign_type_id] + ); + return FALSE; + } + + if ($campaign_type_id == _get_campaign_type_id_twingle_project()) { + + // Instantiate project + $project = new TwingleProject(); + + // Translate custom fields from $_POST + $customFields = []; + $customFieldsKeys = preg_grep('/^custom_/', array_keys($_POST)); + foreach ($customFieldsKeys as $key) { + $customFields[preg_replace('/_-?\d*$/', '', $key)] = + $_POST[$key]; + } + $project->translateCustomFields( + $customFields, + TwingleProject::OUT + ); + TwingleProject::formatValues($customFields, TwingleProject::OUT); + + // Update project + $project->update($customFields); + + // Validate project values + $validation = $project->validate(); + + // If the input is valid, send it to Twingle + if ($validation['valid']) { + + // Try to retrieve twingleApi from cache or create a new + $twingleApi = Civi::cache()->get('twinglecampaign_twingle_api'); + if (NULL === $twingleApi) { + try { + $twingleApi = + new TwingleApiCall(Civi::settings()->get('twingle_api_key')); + } catch (Exception $e) { + + // Roll back transaction if input validation failed + CRM_Core_Transaction::rollbackIfFalse(FALSE); + + CRM_Core_Session::setStatus( + $e->getMessage(), + E::ts("Could not retrieve Twingle API key"), + error, + [unique => TRUE] + ); + Civi::log()->error( + E::SHORT_NAME . + ' Could not retrieve Twingle API key: ' . $e->getMessage() + ); + } + Civi::cache('long')->set('twinglecampaign_twingle_api', $twingleApi); } + + try { + // Complement project values with values from Twingle if it has a + // project_id + if ($project->getProjectId()) { + $project_from_twingle = $twingleApi->getProject($project->getProjectId()); + $project->complement($project_from_twingle); + } + // If this campaign is just about to become created, add a callback to + // sync it after the transaction has finished + else { + CRM_Core_Transaction::addCallback( + CRM_Core_Transaction::PHASE_POST_COMMIT, + 'twinglecampaign_postSave_project_create_callback', [$id] + ); + return FALSE; + } + + // Push project + require E::path() . '/api/v3/TwingleProject/Sync.php'; + $result = _pushProjectToTwingle($project, $twingleApi, [], FALSE); + if ($result['is_error'] != 0) { + throw new \CiviCRM_API3_Exception($result['error_message']); + } + } catch (Exception $e) { + + // Roll back transaction if input validation failed + CRM_Core_Transaction::rollbackIfFalse(FALSE); + + // Display and log error message + CRM_Core_Session::setStatus( + $e->getMessage(), + E::ts("TwingleProject synchronization failed: %1", + [1 => $e->getMessage()]), + error, + [unique => TRUE] + ); + Civi::log()->error( + E::SHORT_NAME . + ' TwingleProject synchronization failed: ' . $e->getMessage() + ); + // Push failed + return FALSE; + } + // Push succeeded + return TRUE; + } + // Display error message if validation failed + else { + + // Roll back transaction if input validation failed + CRM_Core_Transaction::rollbackIfFalse(FALSE); + + // Build error message + $errorMessage = ''; + + CRM_Core_Session::setStatus( + $errorMessage, + E::ts("Input validation failed"), + error, + [unique => TRUE] + ); + // Validation failed + return FALSE; } } + + // TwingleCampaigns always return TRUE; + return TRUE; }