Commit e7b4303d authored by Rich's avatar Rich

Add limit for running form processor

parent 360aaead
......@@ -3,6 +3,12 @@ use CRM_Actionlinks_ExtensionUtil as E;
class CRM_Actionlinks_BAO_ActionLink extends CRM_Actionlinks_DAO_ActionLink {
/** @var callback For testing only. */
public static $api3callback = 'civicrm_api3';
/** @var null|int cache */
protected $cacheCountUseByContact;
/**
* Create a new ActionLink based on array-data
*
......@@ -114,48 +120,137 @@ class CRM_Actionlinks_BAO_ActionLink extends CRM_Actionlinks_DAO_ActionLink {
}
}
if ($linkBAO->use_per_contact) {
// Limit times a particular contact can use this link.
// Limit times a particular contact can use this link.
if ($linkBAO->use_per_contact
&& $linkBAO->countUseByContact($contact_id) >= $linkBAO->use_per_contact) {
return $denied_response;
}
// Ok! Let's do this!
// Record that this contact has accessed it.
$linkBAO->logUseByContact($contact_id);
$linkBAO->runFormProcessor($contact_id, $query);
return ['status' => 200, 'redirect' => $linkBAO->allowed_url];
}
/**
* Run the Form Processor if configured to.
*
* @var int $contacID
* @var array $query (normally _GET data)
*
* @return NULL|TRUE
* TRUE if processor ran (used by phpunit tests)
*/
public function runFormProcessor($contactID, $query) {
if (!$this->form_processor_name) {
// Not configured.
return;
}
// If we're running live and not in a phpunit test, check the form processor is installed.
// If api3callback is not as expected, we're being called as a test; assume the extension
// is enabled.
if (static::$api3callback === 'civicrm_api3' && CRM_Extension_System::singleton()->getManager()->getStatus('form-processor') !== CRM_Extension_Manager::STATUS_INSTALLED) {
// Form processor not installed.
Civi::log()->warning("Action Link #$this->id is configured to run a Form Processor but Form Processor is not installed/enabled.");
return;
}
// Form Processor is configured for this link, and is installed.
// Is it configured to run every time, or once per contact?
if ($this->form_processor_run === 'once' && $this->countUseByContact($contactID) > 1) {
// Configured to run once, and that's been done.
// Note; because logUseByContact() is called before this method, the count will be 1 for the first time of running.
return;
}
$api3callback = static::$api3callback;
// Get input params for form processor.
$fp = $api3callback('FormProcessorInstance', 'get', [
'name' => $this->form_processor_name,
'is_active' => 1,
'sequential' => 1
])['values'][0] ?? FALSE;
if (!$fp) {
Civi::log()->warning("Action Link #$this->id is configured to run Form Processor '$this->form_processor_name' but that does not exist or is not active.");
return;
}
$configuredParams = json_decode($this->form_processor_params, TRUE);
if (!$configuredParams) {
$configuredParams = [];
}
// Only allow using params specified in the form processor.
$fpParams = [];
foreach ($fp['inputs'] as $_) {
$inputName = $_['name'];
if (!empty($configuredParams[$inputName])) {
// First priority is the configured values.
$fpParams[$inputName] = $configuredParams[$inputName];
}
elseif (!empty($query[$inputName])) {
// Second priority is the query data.
$fpParams[$inputName] = $query[$inputName];
}
}
// Finally, call the FormProcessor.
$api3callback('FormProcessor', $this->form_processor_name, $fpParams);
// Return TRUE to tell phpunit tests we ran.
return TRUE;
}
/**
* Look up how many times this link has been used by this contact.
*
* @return int
*/
public function countUseByContact($contactID, $reset = FALSE) {
if (!$this->id) {
throw new \LogicException('countUseByContact called before record has an ID. This is a coding error since this should not be called on new objects.');
}
if ($reset || $this->cacheCountUseByContact === NULL) {
$stats = CRM_Core_DAO::executeQuery(
'SELECT COUNT(1) used_times
FROM civicrm_action_link_contact
WHERE contact_id = %1 AND action_link_id = %2',
[
1 => [$contact_id, 'Integer'],
2 => [$linkBAO->id, 'Integer'],
1 => [$contactID, 'Integer'],
2 => [$this->id, 'Integer'],
]
);
$stats->fetch();
if ($stats->used_times >= $linkBAO->use_per_contact) {
return $denied_response;
}
$this->cacheCountUseByContact = (int) $stats->used_times;
}
return $this->cacheCountUseByContact;
}
/**
* Log use in civicrm_action_link_contact table
*
* @param int $contactID
*/
public function logUseByContact($contactID) {
// Ok! Let's do this!
$linkBAO->use_count += 1;
$linkBAO->save();
$this->use_count += 1;
$this->save();
// Record that this contact has accessed it.
$sqlParams = [
1 => [$contact_id, 'Integer'],
2 => [$linkBAO->id, 'Integer'],
1 => [$contactID, 'Integer'],
2 => [$this->id, 'Integer'],
];
CRM_Core_DAO::executeQuery(
'INSERT INTO civicrm_action_link_contact (contact_id, action_link_id, use_date) VALUES (%1, %2, NOW());',
$sqlParams);
if ($linkBAO->form_processor_name) {
// @todo
if (CRM_Extension_System::singleton()->getManager()->getStatus('form-processor') === CRM_Extension_Manager::STATUS_INSTALLED) {
// Pass over to the form processor.
$params = $query;
$result = civicrm_api3('FormProcessor', $linkBAO->form_processor_name, $params);
}
}
return ['status' => 200, 'redirect' => $linkBAO->allowed_url];
// Invalidate cache.
$this->cacheCountUseByContact = NULL;
}
public function redirectToDeny() {
if ($this->denied_url) {
......
......@@ -6,7 +6,7 @@
*
* Generated from /buildkit/build/dmaster/sites/default/files/civicrm/ext/actionlinks/xml/schema/CRM/Actionlinks/ActionLink.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
* (GenCodeChecksum:f1ec7c33cf845da13a36cf66ac2f42c0)
* (GenCodeChecksum:fada413e7d6b6fa66b960e0cebcfd323)
*/
/**
......@@ -84,6 +84,13 @@ class CRM_Actionlinks_DAO_ActionLink extends CRM_Core_DAO {
*/
public $form_processor_name;
/**
* every or once - how often to run the form prcessor per contact
*
* @var string
*/
public $form_processor_run;
/**
* JSON parameters (object of simple key:value pairs) for the form processor
*
......@@ -246,6 +253,21 @@ class CRM_Actionlinks_DAO_ActionLink extends CRM_Core_DAO {
'bao' => 'CRM_Actionlinks_DAO_ActionLink',
'localizable' => 0,
],
'form_processor_run' => [
'name' => 'form_processor_run',
'type' => CRM_Utils_Type::T_STRING,
'title' => CRM_Actionlinks_ExtensionUtil::ts('Form Processor Run'),
'description' => CRM_Actionlinks_ExtensionUtil::ts('every or once - how often to run the form prcessor per contact'),
'required' => TRUE,
'maxlength' => 5,
'size' => CRM_Utils_Type::SIX,
'where' => 'civicrm_action_link.form_processor_run',
'default' => 'every',
'table_name' => 'civicrm_action_link',
'entity' => 'ActionLink',
'bao' => 'CRM_Actionlinks_DAO_ActionLink',
'localizable' => 0,
],
'form_processor_params' => [
'name' => 'form_processor_params',
'type' => CRM_Utils_Type::T_TEXT,
......
......@@ -60,13 +60,12 @@ class CRM_Actionlinks_Upgrader extends CRM_Actionlinks_Upgrader_Base {
*
* @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)');
*/
public function upgrade_0001() {
$this->ctx->log->info('Applying update 0001: add form_processor_run field');
CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_action_link ADD form_processor_run VARCHAR(5) NOT NULL DEFAULT 'every' COMMENT 'every or once - how often to run the form prcessor per contact';");
return TRUE;
} // */
}
/**
......
......@@ -46,23 +46,32 @@ Links**
Here you can add your first action link. The edit screen is shown in the
screnshot above.
- Name: used for the administrative interface only.
- Description: same
- Active: if un-checked users of the link will get the denied/fallback URL
- Link URL: the web address to the page/resource you want to redirect
- *Name*: used for the administrative interface only.
- *Description*: same
- *Active*: if un-checked users of the link will get the denied/fallback URL
- *Link URL*: the web address to the page/resource you want to redirect
people to.
- Fallback URL (if denied): the web address to redirect people to if
- *Fallback URL (if denied)*: the web address to redirect people to if
they're not alowed the link for any reason.
- Form Processor: if you have the Form Processor extension, you can pick
- *Form Processor*: if you have the Form Processor extension, you can pick
a form processor to call. This gives you all the logic that extension
provides, e.g. creating activities, adding to groups. If you choose one,
that form processor's inputs will show up too.
- Max uses: the max times the link can be used by anyone. (probably not
provides, e.g. creating activities, adding to groups. If you choose one
then the following configuration is also available:
- Whether to run the Form Processor every time the link is allowed, or
only once per contact. e.g. you might want to record that a contact
has downloaded a resource, but you don't need to know every time they
downloaded it.
- Values for the Form Processor's inputs. Note that you can
(programmatically) add Form Processor input params to the links
generated. You can't overwrite a configured input, and you can't
provide a parameter that is not an allowed/expected input.
- *Max uses*: the max times the link can be used by anyone. (probably not
that useful)
- Max number of unique contacts who can use this. e.g. "First 10 people to
- *Max number of unique contacts who can use this.* e.g. "First 10 people to
click this get a voucher for a free pie.".
- Number of times each contact is allowed to use this link.
- When to stop allowing access.
- *Number of times each contact is allowed to use this link*.
- *When to stop allowing access*.
### Create links for contacts
......
......@@ -108,17 +108,30 @@
</select>
</div>
<div crm-ui-field="{name: 'editLinkForm.form_processor_run', title: ts('Run Form Processor')}" ng-show="editData.form_processor_name">
<select
crm-ui-id="editLinkForm.form_processor_run"
ng-model="editData.form_processor_run"
name="form_processor_run"
class="crm-form-select"
>
<option value="every" >{{ts('Every time')}}</option>
<option value="once" >{{ts('Once per contact')}}</option>
</select>
if the link is allowed.
</div>
<div ng-show="editData.fpParams.length > 0" class="crm-section">
<div class="label">
Form processor parameters
<label>Form processor parameters</label>
</div>
<div class="content">
<div ng-repeat="fpParam in editData.fpParams">
<label for="{{'fpParam_' + fpParam.name}}" >{{fpParam.name}}</label><br/>
<label for="{{'fpParam_' + fpParam.name}}" ><strong>{{fpParam.title}}</strong> (<code>{{fpParam.name}}</code>)</label><br/>
<input ng-model="fpParam.value" id="{{'fpParam_' + fpParam.name}}" /><br/>
<span><strong>{{fpParam.title}}</strong> {{fpParam.description}}</span>
<span ng-show="!fpParam.value" >This value will need to be added to the link query string, unless it has a default.</span>
<span>{{fpParam.description}}</span>
<p ng-show="!fpParam.value" >This value will need to be added to the link query string, unless it has a default.</p>
</div>
</div>
</div>
......
......@@ -42,6 +42,7 @@
use_by_contacts: '',
denied_url: '',
form_processor_name: '',
form_processor_run: 'every',
form_processor_params: '',
fpParams: []
};
......
......@@ -14,8 +14,8 @@
<url desc="Support">https://lab.civicrm.org/artfulrobot/actionlinks</url>
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
</urls>
<releaseDate>2020-02-17</releaseDate>
<version>1.0</version>
<releaseDate>2020-02-20</releaseDate>
<version>1.0.1</version>
<develStage>alpha</develStage>
<compatibility>
<ver>5.0</ver>
......
......@@ -57,6 +57,7 @@ CREATE TABLE `civicrm_action_link` (
`denied_url` varchar(255) COMMENT 'The URL to redirect to if not allowed',
`contact_required` tinyint DEFAULT 1 COMMENT 'Must we be able to identify a valid contact',
`form_processor_name` varchar(255) NULL COMMENT 'The name of the form processor to trigger.',
`form_processor_run` varchar(5) NOT NULL DEFAULT "every" COMMENT 'every or once - how often to run the form prcessor per contact',
`form_processor_params` text NULL COMMENT 'JSON parameters (object of simple key:value pairs) for the form processor',
`use_count` int unsigned DEFAULT 0 ,
`use_limit` int unsigned NULL COMMENT 'Access denied after this many uses',
......
This diff is collapsed.
......@@ -68,6 +68,15 @@
<required>false</required>
</field>
<field>
<name>form_processor_run</name>
<type>varchar</type>
<length>5</length>
<default>"every"</default>
<required>true</required>
<comment>every or once - how often to run the form prcessor per contact</comment>
</field>
<field>
<name>form_processor_params</name>
<type>text</type>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment