diff --git a/CRM/Twingle/Page/Profiles.php b/CRM/Twingle/Page/Profiles.php index f9fad8a..eb5bf0c 100644 --- a/CRM/Twingle/Page/Profiles.php +++ b/CRM/Twingle/Page/Profiles.php @@ -18,7 +18,8 @@ use CRM_Twingle_ExtensionUtil as E; class CRM_Twingle_Page_Profiles extends CRM_Core_Page { public function run() { - $profiles = array(); + CRM_Utils_System::setTitle(E::ts("Twingle API Profiles")); + $profiles = []; foreach (CRM_Twingle_Profile::getProfiles() as $profile_name => $profile) { $profiles[$profile_name]['name'] = $profile_name; foreach (CRM_Twingle_Profile::allowedAttributes() as $attribute) { @@ -26,6 +27,7 @@ class CRM_Twingle_Page_Profiles extends CRM_Core_Page { } } $this->assign('profiles', $profiles); + $this->assign('profile_stats', CRM_Twingle_Profile::getProfileStats()); parent::run(); } diff --git a/CRM/Twingle/Profile.php b/CRM/Twingle/Profile.php index e6a6642..58a8352 100644 --- a/CRM/Twingle/Profile.php +++ b/CRM/Twingle/Profile.php @@ -21,12 +21,6 @@ use CRM_Twingle_ExtensionUtil as E; */ class CRM_Twingle_Profile { - /** - * @var CRM_Twingle_Profile[] $_profiles - * Caches the profile objects. - */ - protected static $_profiles = NULL; - /** * @var string $name * The name of the profile. @@ -56,6 +50,20 @@ class CRM_Twingle_Profile { ); } + /** + * Logs (production) access to this profile + * + * @return bool + */ + public function logAccess() { + CRM_Core_DAO::executeQuery(" + UPDATE civicrm_twingle_profile + SET + last_access = NOW(), + access_counter = access_counter + 1 + WHERE name = %1", [1 => [$this->name, 'String']]); + } + /** * Checks whether the profile's selector matches the given project ID. * @@ -79,7 +87,7 @@ class CRM_Twingle_Profile { * The profile's configured custom field mapping */ public function getCustomFieldMapping() { - $custom_field_mapping = array(); + $custom_field_mapping = []; if (!empty($custom_field_definition = $this->getAttribute('custom_field_mapping'))) { foreach (preg_split('/\r\n|\r|\n/', $custom_field_definition, -1, PREG_SPLIT_NO_EMPTY) as $custom_field_map) { list($twingle_field_name, $custom_field_name) = explode("=", $custom_field_map); @@ -144,7 +152,7 @@ class CRM_Twingle_Profile { */ public function setAttribute($attribute_name, $value) { if (!in_array($attribute_name, self::allowedAttributes())) { - throw new Exception(E::ts('Unknown attribute %1.', array(1 => $attribute_name))); + throw new Exception(E::ts('Unknown attribute %1.', [1 => $attribute_name])); } // TODO: Check if value is acceptable. $this->data[$attribute_name] = $value; @@ -182,17 +190,36 @@ class CRM_Twingle_Profile { * Persists the profile within the CiviCRM settings. */ public function saveProfile() { - self::$_profiles[$this->getName()] = $this; + // make sure it's valid $this->verifyProfile(); - self::storeProfiles(); + + // check if the profile exists + $profile_id = CRM_Core_DAO::singleValueQuery( + "SELECT id FROM civicrm_twingle_profile WHERE name = %1", [1 => [$this->name, 'String']]); + if ($profile_id) { + // existing profile -> just update the config + CRM_Core_DAO::executeQuery( + "UPDATE civicrm_twingle_profile SET config = %2 WHERE name = %1", + [ + 1 => [$this->name, 'String'], + 2 => [json_encode($this->data), 'String'] + ]); + } else { + // new profile -> add new entry to the DB + CRM_Core_DAO::executeQuery( + "INSERT IGNORE INTO civicrm_twingle_profile(name,config,last_access,access_counter) VALUES (%1, %2, null, 0)", + [ + 1 => [$this->name, 'String'], + 2 => [json_encode($this->data), 'String'] + ]); + } } /** - * Deletes the profile from the CiviCRM settings. + * Deletes the profile from the database */ public function deleteProfile() { - unset(self::$_profiles[$this->getName()]); - self::storeProfiles(); + CRM_Core_DAO::executeQuery("DELETE FROM civicrm_twingle_profile WHERE name = %1", [1 => [$this->name, 'String']]); } /** @@ -202,7 +229,7 @@ class CRM_Twingle_Profile { */ public static function allowedAttributes() { return array_merge( - array( + [ 'selector', 'xcm_profile', 'location_type_id', @@ -228,7 +255,7 @@ class CRM_Twingle_Profile { 'membership_postprocess_call', 'newsletter_double_opt_in', 'required_address_components', - ), + ], // Add payment methods. array_keys(static::paymentInstruments()), @@ -245,7 +272,7 @@ class CRM_Twingle_Profile { * @return array */ public static function paymentInstruments() { - return array( + return [ 'pi_banktransfer' => E::ts('Bank transfer'), 'pi_debit_manual' => E::ts('Debit manual'), 'pi_debit_automatic' => E::ts('Debit automatic'), @@ -257,7 +284,7 @@ class CRM_Twingle_Profile { 'pi_paydirekt' => E::ts('paydirekt'), 'pi_applepay' => E::ts('Apple Pay'), 'pi_googlepay' => E::ts('Google Pay'), - ); + ]; } /** @@ -269,7 +296,7 @@ class CRM_Twingle_Profile { * @return CRM_Twingle_Profile */ public static function createDefaultProfile($name = 'default') { - return new CRM_Twingle_Profile($name, array( + return new CRM_Twingle_Profile($name, [ 'selector' => '', 'xcm_profile' => '', 'location_type_id' => CRM_Twingle_Submission::LOCATION_TYPE_ID_WORK, @@ -307,7 +334,7 @@ class CRM_Twingle_Profile { 'city', 'country', ], - ) + ] // Add contribution status for all payment methods. + array_fill_keys(array_map(function($attribute) { return $attribute . '_status'; @@ -339,54 +366,56 @@ class CRM_Twingle_Profile { /** * Retrieves the profile with the given name. * - * @param $name + * @param string $name * * @return CRM_Twingle_Profile | NULL */ public static function getProfile($name) { - $profiles = self::getProfiles(); - if (isset($profiles[$name])) { - return $profiles[$name]; - } - else { - return NULL; + if (!empty($name)) { + $profile_data = CRM_Core_DAO::singleValueQuery("SELECT config FROM civicrm_twingle_profile WHERE name = %1", [ + 1 => [$name, 'String']]); + if ($profile_data) { + return new CRM_Twingle_Profile($name, json_decode($profile_data, 1)); + } } + return NULL; } /** * Retrieves the list of all profiles persisted within the current CiviCRM * settings, including the default profile. * - * @return CRM_Twingle_Profile[] + * @return array + * profile_name => CRM_Twingle_Profile */ public static function getProfiles() { - if (self::$_profiles === NULL) { - self::$_profiles = array(); - if ($profiles_data = Civi::settings()->get('twingle_profiles')) { - foreach ($profiles_data as $profile_name => $profile_data) { - self::$_profiles[$profile_name] = new CRM_Twingle_Profile($profile_name, $profile_data); - } - } + // todo: cache? + $profiles = []; + $profile_data = CRM_Core_DAO::executeQuery("SELECT name, config FROM civicrm_twingle_profile"); + while ($profile_data->fetch()) { + $profiles[$profile_data->name] = new CRM_Twingle_Profile($profile_data->name, json_decode($profile_data->config, 1)); } - - // Include the default profile if it was not overridden within the settings. - if (!isset(self::$_profiles['default'])) { - self::$_profiles['default'] = self::createDefaultProfile(); - self::storeProfiles(); - } - - return self::$_profiles; + return $profiles; } - /** - * Persists the list of profiles into the CiviCRM settings. + * Get the stats (access_count, last_access) for all twingle profiles + * + * @return CRM_Twingle_Profile[] */ - public static function storeProfiles() { - $profile_data = array(); - foreach (self::$_profiles as $profile_name => $profile) { - $profile_data[$profile_name] = $profile->data; + public static function getProfileStats() { + $stats = []; + $profile_data = CRM_Core_DAO::executeQuery("SELECT name, last_access, access_counter FROM civicrm_twingle_profile"); + while ($profile_data->fetch()) { + $stats[$profile_data->name] = [ + 'name' => $profile_data->name, + 'last_access' => $profile_data->last_access, + 'last_access_txt' => $profile_data->last_access ? date('Y-m-d H:i:s', strtotime($profile_data->last_access)) : E::ts("never"), + 'access_counter' => $profile_data->access_counter, + 'access_counter_txt' => $profile_data->access_counter ? ((int) $profile_data->access_counter) . 'x' : E::ts("never"), + ]; } - Civi::settings()->set('twingle_profiles', $profile_data); + return $stats; } + } diff --git a/CRM/Twingle/Upgrader.php b/CRM/Twingle/Upgrader.php index 35c1283..9b3af0c 100644 --- a/CRM/Twingle/Upgrader.php +++ b/CRM/Twingle/Upgrader.php @@ -6,32 +6,15 @@ use CRM_Twingle_ExtensionUtil as E; */ class CRM_Twingle_Upgrader extends CRM_Twingle_Upgrader_Base { - // By convention, functions that look like "function upgrade_NNNN()" are - // upgrade tasks. They are executed in order (like Drupal's hook_update_N). - /** - * Example: Run an external SQL script when the module is installed. - * + * Installer script + */ public function install() { - $this->executeSqlFile('sql/myinstall.sql'); - } + // create a DB table for the twingle profiles + $this->executeSqlFile('sql/civicrm_twingle_profile.sql'); - /** - * Example: Work with entities usually not available during the install step. - * - * This method can be used for any post-install tasks. For example, if a step - * of your installation depends on accessing an entity that is itself - * created during the installation (e.g., a setting or a managed entity), do - * so here to avoid order of operation problems. - * - public function postInstall() { - $customFieldId = civicrm_api3('CustomField', 'getvalue', array( - 'return' => array("id"), - 'name' => "customFieldCreatedViaManagedHook", - )); - civicrm_api3('Setting', 'create', array( - 'myWeirdFieldSetting' => array('id' => $customFieldId, 'weirdness' => 1), - )); + // add a default profile + CRM_Twingle_Profile::createDefaultProfile()->saveProfile(); } /** @@ -41,96 +24,6 @@ class CRM_Twingle_Upgrader extends CRM_Twingle_Upgrader_Base { $this->executeSqlFile('sql/myuninstall.sql'); } - /** - * Example: Run a simple query when a module is enabled. - * - public function enable() { - CRM_Core_DAO::executeQuery('UPDATE foo SET is_active = 1 WHERE bar = "whiz"'); - } - - /** - * Example: Run a simple query when a module is disabled. - * - public function disable() { - CRM_Core_DAO::executeQuery('UPDATE foo SET is_active = 0 WHERE bar = "whiz"'); - } - - /** - * Example: Run a couple simple queries. - * - * @return TRUE on success - * @throws Exception - * - public function upgrade_4200() { - $this->ctx->log->info('Applying update 4200'); - CRM_Core_DAO::executeQuery('UPDATE foo SET bar = "whiz"'); - CRM_Core_DAO::executeQuery('DELETE FROM bang WHERE willy = wonka(2)'); - return TRUE; - } // */ - - - /** - * Example: Run an external SQL script. - * - * @return TRUE on success - * @throws Exception - public function upgrade_4201() { - $this->ctx->log->info('Applying update 4201'); - // this path is relative to the extension base dir - $this->executeSqlFile('sql/upgrade_4201.sql'); - return TRUE; - } // */ - - - /** - * Example: Run a slow upgrade process by breaking it up into smaller chunk. - * - * @return TRUE on success - * @throws Exception - public function upgrade_4202() { - $this->ctx->log->info('Planning update 4202'); // PEAR Log interface - - $this->addTask(E::ts('Process first step'), 'processPart1', $arg1, $arg2); - $this->addTask(E::ts('Process second step'), 'processPart2', $arg3, $arg4); - $this->addTask(E::ts('Process second step'), 'processPart3', $arg5); - return TRUE; - } - public function processPart1($arg1, $arg2) { sleep(10); return TRUE; } - public function processPart2($arg3, $arg4) { sleep(10); return TRUE; } - public function processPart3($arg5) { sleep(10); return TRUE; } - // */ - - - /** - * Example: Run an upgrade with a query that touches many (potentially - * millions) of records by breaking it up into smaller chunks. - * - * @return TRUE on success - * @throws Exception - public function upgrade_4203() { - $this->ctx->log->info('Planning update 4203'); // PEAR Log interface - - $minId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(min(id),0) FROM civicrm_contribution'); - $maxId = CRM_Core_DAO::singleValueQuery('SELECT coalesce(max(id),0) FROM civicrm_contribution'); - for ($startId = $minId; $startId <= $maxId; $startId += self::BATCH_SIZE) { - $endId = $startId + self::BATCH_SIZE - 1; - $title = E::ts('Upgrade Batch (%1 => %2)', array( - 1 => $startId, - 2 => $endId, - )); - $sql = ' - UPDATE civicrm_contribution SET foobar = whiz(wonky()+wanker) - WHERE id BETWEEN %1 and %2 - '; - $params = array( - 1 => array($startId, 'Integer'), - 2 => array($endId, 'Integer'), - ); - $this->addTask($title, 'executeSql', $sql, $params); - } - return TRUE; - } // */ - /** * Copy financial_type_id setting to new setting financial_type_id_recur. */ @@ -163,4 +56,32 @@ class CRM_Twingle_Upgrader extends CRM_Twingle_Upgrader_Base { return TRUE; } + /** + * Upgrading to 1.4.0 needs to convert the profiles into the new infrastructure + * + * @return TRUE on success + * @throws Exception + */ + public function upgrade_5140() { + $this->ctx->log->info('Converting twingle profiles.'); + + // create a DB table for the twingle profiles + $this->executeSqlFile('sql/civicrm_twingle_profile.sql'); + + // migrate the current profiles + if ($profiles_data = Civi::settings()->get('twingle_profiles')) { + foreach ($profiles_data as $profile_name => $profile_data) { + $profile = new CRM_Twingle_Profile($profile_name, $profile_data); + $data = json_encode($profile->getData()); + CRM_Core_DAO::executeQuery( + "INSERT IGNORE INTO civicrm_twingle_profile(name,config,last_access,access_counter) VALUES (%1, %2, NOW(), 0)", + [ + 1 => [$profile_name, 'String'], + 2 => [$data, 'String'] + ]); + } + } + + return TRUE; + } } diff --git a/api/v3/TwingleDonation/Submit.php b/api/v3/TwingleDonation/Submit.php index abea337..22d2355 100644 --- a/api/v3/TwingleDonation/Submit.php +++ b/api/v3/TwingleDonation/Submit.php @@ -278,6 +278,7 @@ function civicrm_api3_twingle_donation_Submit($params) { // Get the profile defined for the given form ID, or the default profile // if none matches. $profile = CRM_Twingle_Profile::getProfileForProject($params['project_id']); + $profile->logAccess(); // Validate submitted parameters CRM_Twingle_Submission::validateSubmission($params, $profile); diff --git a/sql/civicrm_twingle_profile.sql b/sql/civicrm_twingle_profile.sql new file mode 100644 index 0000000..479e231 --- /dev/null +++ b/sql/civicrm_twingle_profile.sql @@ -0,0 +1,16 @@ +-- /******************************************************* +-- ** civicrm_twingle_profile +-- ** +-- ** stores twingle profile data v1.4+ +-- ********************************************************/ + +CREATE TABLE IF NOT EXISTS `civicrm_twingle_profile`( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `name` varchar(255) COMMENT 'configuration name, i.e. internal ID', + `config` text COMMENT 'JSON encoded configuration', + `last_access` datetime COMMENT 'timestamp of the last access (through the api)', + `access_counter` int unsigned COMMENT 'number of accesses (through the api)', + PRIMARY KEY (`id`), + UNIQUE INDEX (`name`) +) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; + diff --git a/templates/CRM/Twingle/Page/Profiles.tpl b/templates/CRM/Twingle/Page/Profiles.tpl index f97b9ed..b5b869e 100644 --- a/templates/CRM/Twingle/Page/Profiles.tpl +++ b/templates/CRM/Twingle/Page/Profiles.tpl @@ -26,6 +26,8 @@ {ts domain="de.systopia.twingle"}Profile name{/ts} {ts domain="de.systopia.twingle"}Properties{/ts} + {ts domain="de.systopia.twingle"}Used{/ts} + {ts domain="de.systopia.twingle"}Last Used{/ts} {ts domain="de.systopia.twingle"}Operations{/ts} @@ -37,6 +39,8 @@
{ts domain="de.systopia.twingle"}Selector{/ts}: {$profile.selector}
+ {ts domain="de.systopia.twingle"}{$profile_stats.$profile_name.access_counter_txt}{/ts} + {ts domain="de.systopia.twingle"}{$profile_stats.$profile_name.last_access_txt}{/ts} {ts domain="de.systopia.twingle"}Edit{/ts} {ts domain="de.systopia.twingle"}Copy{/ts} diff --git a/twingle.php b/twingle.php index 214d041..8551f2b 100644 --- a/twingle.php +++ b/twingle.php @@ -153,6 +153,19 @@ function twingle_civicrm_alterAPIPermissions($entity, $action, &$params, &$permi $permissions['twingle_donation']['endrecurring'] = array('access Twingle API'); } +/** + * Make sure, that the last_access and access_counter column is not logged + * + * @param array $logTableSpec + */ +function twingle_civicrm_alterLogTables(&$logTableSpec) +{ + if (isset($logTableSpec['civicrm_twingle_profile'])) { + $logTableSpec['civicrm_twingle_profile']['exceptions'] = ['last_access', 'access_counter']; + } +} + + // --- Functions below this ship commented out. Uncomment as required. --- /**