├── images ├── screenshot.png ├── debug-console.png ├── drupal-full-html.png ├── profile-enable.png ├── check-console-log.png ├── cors-configuration.png ├── drupal-change-roles.png ├── right-click-inspect.png ├── drupal-change-full-html.png ├── profile-on-remote-site.png ├── wordpress-add-custom-html.png └── contribution-standard-enable.png ├── gendoc ├── docs ├── wordpress_website.md ├── drupal_website.md ├── extending.md ├── drupal_civicrm.md ├── wordpress_civicrm.md ├── index.md └── api.md ├── api └── v3 │ ├── RemoteForm │ ├── Countries.php │ ├── Stateprovincesforcountry.php │ └── Countiesforstateprovince.php │ └── RemoteFormContributionPage │ └── Submit.php ├── templates ├── CRM │ └── Remoteform │ │ ├── Page │ │ └── RemoteForm.tpl │ │ └── Form │ │ └── RemoteformSettings.tpl ├── profile.tpl └── contribution_page.tpl ├── Civi └── Api4 │ ├── Remoteform.php │ └── Action │ └── Remoteform │ └── GenerateCorsServices.php ├── xml └── Menu │ └── remoteform.xml ├── phpunit.xml.dist ├── remoteform.css ├── spin.css ├── tests └── phpunit │ ├── api │ └── v3 │ │ ├── ContributionPage │ │ └── SubmitTest.php │ │ └── RemoteFormContributionPage │ │ └── SubmitTest.php │ └── bootstrap.php ├── info.xml ├── settings └── Remoteform.setting.php ├── remoteform.stripe.php ├── README.md ├── remoteform.stripe.js ├── CRM ├── Contribute │ └── Form │ │ └── Contribution │ │ └── RemoteformConfirm.php └── Remoteform │ ├── Form │ └── RemoteformSettings.php │ └── Page │ └── RemoteForm.php ├── remoteform.civix.php ├── remoteform.php ├── LICENSE.txt └── remoteform.js /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /images/debug-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/debug-console.png -------------------------------------------------------------------------------- /images/drupal-full-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/drupal-full-html.png -------------------------------------------------------------------------------- /images/profile-enable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/profile-enable.png -------------------------------------------------------------------------------- /images/check-console-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/check-console-log.png -------------------------------------------------------------------------------- /images/cors-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/cors-configuration.png -------------------------------------------------------------------------------- /images/drupal-change-roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/drupal-change-roles.png -------------------------------------------------------------------------------- /images/right-click-inspect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/right-click-inspect.png -------------------------------------------------------------------------------- /images/drupal-change-full-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/drupal-change-full-html.png -------------------------------------------------------------------------------- /images/profile-on-remote-site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/profile-on-remote-site.png -------------------------------------------------------------------------------- /images/wordpress-add-custom-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/wordpress-add-custom-html.png -------------------------------------------------------------------------------- /images/contribution-standard-enable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressivetech/net.ourpowerbase.remoteform/HEAD/images/contribution-standard-enable.png -------------------------------------------------------------------------------- /gendoc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | awk '#/**#{flag=1;next}#*/#{flag=0}flag' remoteform.js 4 | 5 | cat remoteform.js | awk '/[/][*][*]/{flag=1;next}/[*][/]/{flag=0}flag' | sed -E 's/^([ *]*)//g' > docs/api.md 6 | -------------------------------------------------------------------------------- /docs/wordpress_website.md: -------------------------------------------------------------------------------- 1 | # WordPress 2 | 3 | If you are adding or editing a post in Wordpress on your client website, 4 | be sure to click the link to add custom HTML: 5 | 6 | ![Custom HTML](../images/wordpress-add-custom-html.png) 7 | 8 | -------------------------------------------------------------------------------- /api/v3/RemoteForm/Countries.php: -------------------------------------------------------------------------------- 1 | This new page is generated by CRM/Remoteform/Page/RemoteForm.php 2 | 3 | {* Example: Display a variable directly *} 4 |

The current time is {$currentTime}

5 | 6 | {* Example: Display a translated string -- which happens to include a variable *} 7 |

{ts 1=$currentTime}(In your native language) The current time is %1.{/ts}

8 | -------------------------------------------------------------------------------- /Civi/Api4/Remoteform.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | civicrm/remoteform 5 | CRM_Remoteform_Page_RemoteForm 6 | RemoteForm 7 | profile create 8 | 9 | 10 | civicrm/admin/remoteform 11 | CRM_Remoteform_Form_RemoteformSettings 12 | RemoteformSettings 13 | access CiviCRM 14 | 15 | 16 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/phpunit 6 | 7 | 8 | 9 | 10 | ./ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /templates/CRM/Remoteform/Form/RemoteformSettings.tpl: -------------------------------------------------------------------------------- 1 |
2 |

Remote Forms is an extension that allows you to submit data directly from remote sites.

3 | 4 |

To use Remote Forms, you must list the addresses of all sites that will be submitting data. Only data submitted from sites listed below will be accepted.

5 | 6 |

Once you have specified the remote sites below, you can configure the Profile or Contribution page you want to display.

7 |
8 | 9 |
10 | {foreach from=$elementNames item=elementName} 11 |
12 |
{$form.$elementName.label}
13 |
{$form.$elementName.html}
14 |
15 |
16 | {/foreach} 17 | 18 | 19 |
20 | {include file="CRM/common/formButtons.tpl" location="bottom"} 21 |
22 |
23 | -------------------------------------------------------------------------------- /docs/drupal_website.md: -------------------------------------------------------------------------------- 1 | # Adding your code to a Drupal site 2 | 3 | If you are running your client web site on Drupal, adding the remoteform 4 | javascript is a breeze, *provided you have the proper permissions.* 5 | 6 | If you do have the proper permissions, you can add the code to any page or 7 | block. Simply paste the code into the available text box and then be sure to 8 | change the text format to: Full HTML. 9 | 10 | ![Select Full HTML](../images/drupal-full-html.png) 11 | 12 | If you do not see the option to select Full HTML, then you will need to ensure 13 | you have the right permissions. 14 | 15 | ## Setting permissions 16 | 17 | To change permissions for the Full HTML option, click `Administration -> 18 | Configuration -> Content Authoring -> Text Formats`. 19 | 20 | Then, find the Full HTML option and click the link to configure it. 21 | 22 | ![Edit Full HTML](../images/drupal-change-full-html.png) 23 | 24 | Lastly, select a role you are in or that you want to provide access. 25 | 26 | ![Change Role](../images/drupal-change-roles.png) 27 | -------------------------------------------------------------------------------- /api/v3/RemoteForm/Stateprovincesforcountry.php: -------------------------------------------------------------------------------- 1 | get('defaultContactCountry'); 25 | } 26 | $values = CRM_Core_PseudoConstant::stateProvinceForCountry($country_id); 27 | return civicrm_api3_create_success($values); 28 | } 29 | 30 | -------------------------------------------------------------------------------- /remoteform.css: -------------------------------------------------------------------------------- 1 | .alert { 2 | padding: 15px; 3 | margin-bottom: 20px; 4 | border: 1px solid transparent; 5 | border-radius: 8px; 6 | } 7 | 8 | .alert-success { 9 | color: #3c763d; 10 | background-color: #dff0d8; 11 | border-color: #d6e9c6; 12 | } 13 | 14 | .alert-warning { 15 | color: #8a6d3b; 16 | background-color: #fcf8e3; 17 | border-color: #faebcc; 18 | } 19 | 20 | .rf-form { 21 | background-color: #f9f9f9; 22 | padding-left: 10px; 23 | border: 1px solid #dddd; 24 | border-radius: 8px; 25 | margin-left: 20px; 26 | margin-right: 10px; 27 | } 28 | 29 | .form-group { 30 | padding-top: 10px; 31 | padding-bottom: 10px; 32 | } 33 | 34 | .rf-label { 35 | display: block; 36 | color: #545454; 37 | font-family: sans-serif; 38 | } 39 | 40 | .form-control { 41 | width: 90%; 42 | height: 34px; 43 | } 44 | 45 | small.form-text { 46 | display: block; 47 | } 48 | 49 | .text-muted { 50 | color: #777; 51 | } 52 | 53 | .sr-only { 54 | position: absolute; 55 | width: 1px; 56 | height: 1px; 57 | padding: 0; 58 | margin: -1px; 59 | overflow: hidden; 60 | clip: rect(0,0,0,0); 61 | border: 0; 62 | } 63 | -------------------------------------------------------------------------------- /api/v3/RemoteForm/Countiesforstateprovince.php: -------------------------------------------------------------------------------- 1 | get('defaultContactStateProvince'); 24 | if (empty($state_province_id)) { 25 | // Rather then return all counties in the world, return nothing if no defaults. 26 | return civicrm_api3_create_success([]); 27 | } 28 | } 29 | $values = CRM_Core_PseudoConstant::countyForState($state_province_id); 30 | return civicrm_api3_create_success($values); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /templates/profile.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 |
5 | {$form.remoteform_profile_enable.html} 6 | {$form.remoteform_profile_enable.label} 7 |
{ts}If enabled, you will be able to allow people to fill out this profile from your on web site by including a few lines of javascript code.{/ts}
8 |
{$remoteform_code}
9 |
12 | 13 | {literal} 14 | 15 | 37 | {/literal} 38 | 39 | -------------------------------------------------------------------------------- /templates/contribution_page.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 |
5 | {$form.remoteform_contribution_page_enable.html} 6 | {$form.remoteform_contribution_page_enable.label} 7 |
{ts}If enabled, you will be able to allow people to make contributions via this page from your own web site by including a few lines of javascript code.{/ts}
8 |
{$remoteform_code}
9 |
12 | 13 | {literal} 14 | 15 | 37 | {/literal} 38 | 39 | -------------------------------------------------------------------------------- /spin.css: -------------------------------------------------------------------------------- 1 | .remoteForm-spinner-frame { 2 | position: fixed; /* Sit on top of the page content */ 3 | display: none; /* Hidden by default */ 4 | width: 100%; /* Full width (cover the whole page) */ 5 | height: 100%; /* Full height (cover the whole page) */ 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | background-color: rgba(0,0,0,0.2); /* Black background with opacity */ 11 | z-index: 2; /* Specify a stack order in case you're using a different order for other elements */ 12 | height: 100%; 13 | width: 100%; 14 | } 15 | .remoteForm-spinner { 16 | position: fixed; /* Sit on top of the page content */ 17 | display: none; /* Hidden by default */ 18 | width: 100%; /* Full width (cover the whole page) */ 19 | height: 100%; /* Full height (cover the whole page) */ 20 | top: 20%; 21 | left: 20%; 22 | right: 20%; 23 | bottom: 20%; 24 | z-index: 2; /* Specify a stack order in case you're using a different order for other elements */ 25 | cursor: pointer; /* Add a pointer on hover */ 26 | border: 16px solid #f3f3f3; 27 | border-radius: 50%; 28 | border-top: 16px solid #d81ab5; 29 | border-bottom: 16px solid #d81ab5; 30 | height: 100px; 31 | width: 100px; 32 | -webkit-animation: spin 2s linear infinite; 33 | animation: spin 2s linear infinite; 34 | } 35 | 36 | @-webkit-keyframes spin { 37 | 0% { -webkit-transform: rotate(0deg); } 38 | 100% { -webkit-transform: rotate(360deg); } 39 | } 40 | 41 | @keyframes spin { 42 | 0% { transform: rotate(0deg); } 43 | 100% { transform: rotate(360deg); } 44 | } 45 | -------------------------------------------------------------------------------- /tests/phpunit/api/v3/ContributionPage/SubmitTest.php: -------------------------------------------------------------------------------- 1 | installMe(__DIR__) 21 | ->apply(); 22 | } 23 | 24 | /** 25 | * The setup() method is executed before the test is executed (optional). 26 | */ 27 | public function setUp() { 28 | parent::setUp(); 29 | } 30 | 31 | /** 32 | * The tearDown() method is executed after the test was executed (optional) 33 | * This can be used for cleanup. 34 | */ 35 | public function tearDown() { 36 | parent::tearDown(); 37 | } 38 | 39 | /** 40 | * Simple example test case. 41 | * 42 | * Note how the function name begins with the word "test". 43 | */ 44 | public function testApiExample() { 45 | $result = civicrm_api3('ContributionPage', 'Submit', array('magicword' => 'sesame')); 46 | $this->assertEquals('Twelve', $result['values'][12]['name']); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/phpunit/api/v3/RemoteFormContributionPage/SubmitTest.php: -------------------------------------------------------------------------------- 1 | installMe(__DIR__) 21 | ->apply(); 22 | } 23 | 24 | /** 25 | * The setup() method is executed before the test is executed (optional). 26 | */ 27 | public function setUp() { 28 | parent::setUp(); 29 | } 30 | 31 | /** 32 | * The tearDown() method is executed after the test was executed (optional) 33 | * This can be used for cleanup. 34 | */ 35 | public function tearDown() { 36 | parent::tearDown(); 37 | } 38 | 39 | /** 40 | * Simple example test case. 41 | * 42 | * Note how the function name begins with the word "test". 43 | */ 44 | public function testApiExample() { 45 | $result = civicrm_api3('RemoteFormContributionPage', 'Submit', array('magicword' => 'sesame')); 46 | $this->assertEquals('Twelve', $result['values'][12]['name']); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /docs/extending.md: -------------------------------------------------------------------------------- 1 | # Remoteform: Extending 2 | ## Hooks 3 | 4 | Remoteform makes the following hooks available, to allow other extensions to alter 5 | some of its native behaviors. 6 | 7 | ### hook_civicrm_remoteform_extraJsParams 8 | Alter the "extra JavaScript parameters" that Remoteform includes in the copy/paste 9 | embedded HTML code for each form. 10 | 11 | #### Definition 12 | ``` 13 | hook_civicrm_remoteform_extraJsParams($id, &$params); 14 | ``` 15 | #### Parameters 16 | * Int $id - system ID of the Contribution Page or Event entity 17 | * String $params - reference to the string of "extra JavaScript parameters" that will be embedded in the code. 18 | 19 | 20 | #### Returns 21 | Void. 22 | 23 | #### Example 24 | ``` 25 | function example_civicrm_remoteform_extraJsParams($id, &$params) { 26 | $params .= htmlentities("createFieldDivFunc: exampleCreateFieldDiv,") . '
'; 27 | } 28 | ``` 29 | 30 | 31 | # hook_civicrm_remoteform_extraJsUrls 32 | Alter the array of "extra JavaScript files URLs" that Remoteform includes in the copy/paste 33 | embedded HTML code for each form. 34 | 35 | #### Definition 36 | ``` 37 | hook_civicrm_remoteform_extraJsUrls($id, &$urls); 38 | ``` 39 | #### Parameters 40 | * Int $id - system ID of the Contribution Page or Event entity 41 | * Array $urls - reference to the array of "extra JavaScript files URLs" that Remoteform will include. 42 | 43 | 44 | #### Returns 45 | Void. 46 | 47 | #### Example 48 | ``` 49 | function example_civicrm_remoteform_extraJsUrls($id, &$urls) { 50 | $urls[] = CRM_Core_Resources::singleton()->getUrl('example', 'exampleRemoteformExtra.js'); 51 | } 52 | ``` -------------------------------------------------------------------------------- /info.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | remoteform 4 | Remoteform 5 | Remoteform allows you to easily create CiviCRM forms on a remote web site using a few lines of javascript code. 6 | AGPL-3.0 7 | 8 | Jamie McClelland 9 | jamie@progressivetech.org 10 | 11 | 12 | https://github.com/progressivetech/net.ourpowerbase.remoteform 13 | https://github.com/progressivetech/net.ourpowerbase.remoteform/docs 14 | https://github.com/progressivetech/net.ourpowerbase.remoteform/issues 15 | http://www.gnu.org/licenses/agpl-3.0.html 16 | 17 | 2018-06-07 18 | 0.1 19 | alpha 20 | 21 | 5.69 22 | 23 | This module is not fully tested and not ready for production. If using with Stripe, please ensure you are running Stripe greater or equal to 6.3 24 | 25 | CRM/Remoteform 26 | 23.02.1 27 | 28 | 29 | menu-xml@1.0.0 30 | setting-php@1.0.0 31 | smarty-v2@1.0.1 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /settings/Remoteform.setting.php: -------------------------------------------------------------------------------- 1 | array( 9 | 'group_name' => 'Remote Form', 10 | 'group' => 'remoteform', 11 | 'name' => 'remoteform_cors_urls', 12 | 'type' => 'String', 13 | 'quick_form_type' => 'Element', 14 | 'html_type' => 'textarea', 15 | 'html_attributes' => array('rows' => 5, 'cols' => 50), 16 | 'default' => '', 17 | 'add' => '5.3', 18 | 'is_domain' => 1, 19 | 'is_contact' => 0, 20 | 'title' => "Allow forms to be submitted from the following locations. Please list full URL (including https://), one per line", 21 | 'description' => 'Remote URLs that are allowed to submit data.', 22 | 'help_text' => 'List the URLs of web sites that are allowed to submit data to CiviCRM via the Remote Form extension', 23 | ), 24 | 'remoteform_enabled_profile' => array( 25 | 'group_name' => 'Remote Form Enabled Entities', 26 | 'group' => 'remoteform_enabled_entities', 27 | 'name' => 'remoteform_enabled_profile', 28 | 'type' => 'Array', 29 | 'default' => array(), 30 | 'add' => '5.3', 31 | 'is_domain' => 1, 32 | 'is_contact' => 0, 33 | 'title' => "An array of profile ids that are allowed to accept remote form submissions", 34 | ), 35 | 'remoteform_enabled_contribution_page' => array( 36 | 'group_name' => 'Remote Form Enabled Contribution Pages', 37 | 'group' => 'remoteform_enabled_entities', 38 | 'name' => 'remoteform_enabled_contribution_page', 39 | 'type' => 'Array', 40 | 'default' => array(), 41 | 'add' => '5.3', 42 | 'is_domain' => 1, 43 | 'is_contact' => 0, 44 | 'title' => "An array of contribution page ids that are allowed to accept remote form submissions", 45 | ) 46 | ); 47 | -------------------------------------------------------------------------------- /remoteform.stripe.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'title' => 'Credit Card', 18 | 'entity' => 'contribution', 19 | 'html' => [ 20 | 'type' => 'text' 21 | ] 22 | ] 23 | ]; 24 | 25 | } 26 | 27 | function remoteformstripe_extra_js_urls() { 28 | $extraJsUrls = []; 29 | $extraJsUrls[] = Civi::resources()->getUrl('net.ourpowerbase.remoteform', 'remoteform.stripe.js'); 30 | $extraJsUrls[] = "https://js.stripe.com/v3/"; 31 | return $extraJsUrls; 32 | } 33 | 34 | function remoteformstripe_extra_js_params($id) { 35 | $details = remoteform_get_contribution_page_details($id); 36 | $live_id = $details['payment_processor']; 37 | $test_id = $live_id + 1; // Is this right?? 38 | 39 | $live_key = remoteformstripe_get_public_key($live_id); 40 | $test_key = remoteformstripe_get_public_key($test_id); 41 | 42 | return htmlentities(' customInitFunc: initStripe,') . '
' . 43 | htmlentities(' customSubmitDataFunc: submitStripe,') . '
' . 44 | htmlentities(' customSubmitDataParams: {') . '
' . 45 | htmlentities(' apiKey: "' . $live_key . '",') . '
' . 46 | htmlentities(' // uncomment for testing: apiKey: "' . $test_key . '",') . '
' . 47 | htmlentities(' },') . '
'; 48 | } 49 | 50 | function remoteformstripe_get_public_key($ppid) { 51 | return CRM_Core_Payment_Stripe::getPublicKeyById($ppid); 52 | } 53 | -------------------------------------------------------------------------------- /tests/phpunit/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('CRM_', __DIR__); 10 | $loader->add('Civi\\', __DIR__); 11 | $loader->add('api_', __DIR__); 12 | $loader->add('api\\', __DIR__); 13 | $loader->register(); 14 | 15 | /** 16 | * Call the "cv" command. 17 | * 18 | * @param string $cmd 19 | * The rest of the command to send. 20 | * @param string $decode 21 | * Ex: 'json' or 'phpcode'. 22 | * @return string 23 | * Response output (if the command executed normally). 24 | * @throws \RuntimeException 25 | * If the command terminates abnormally. 26 | */ 27 | function cv($cmd, $decode = 'json') { 28 | $cmd = 'cv ' . $cmd; 29 | $descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR); 30 | $oldOutput = getenv('CV_OUTPUT'); 31 | putenv("CV_OUTPUT=json"); 32 | 33 | // Execute `cv` in the original folder. This is a work-around for 34 | // phpunit/codeception, which seem to manipulate PWD. 35 | $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd); 36 | 37 | $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); 38 | putenv("CV_OUTPUT=$oldOutput"); 39 | fclose($pipes[0]); 40 | $result = stream_get_contents($pipes[1]); 41 | fclose($pipes[1]); 42 | if (proc_close($process) !== 0) { 43 | throw new RuntimeException("Command failed ($cmd):\n$result"); 44 | } 45 | switch ($decode) { 46 | case 'raw': 47 | return $result; 48 | 49 | case 'phpcode': 50 | // If the last output is /*PHPCODE*/, then we managed to complete execution. 51 | if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") { 52 | throw new \RuntimeException("Command failed ($cmd):\n$result"); 53 | } 54 | return $result; 55 | 56 | case 'json': 57 | return json_decode($result, 1); 58 | 59 | default: 60 | throw new RuntimeException("Bad decoder format ($decode)"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/drupal_civicrm.md: -------------------------------------------------------------------------------- 1 | 2 | # Hosting your CiviCRM RemoteForms on a Drupal site 3 | 4 | The CiviCRM installation where you install the RemoteForms extension, 5 | and in which you collect the data collected by the forms exposed by it 6 | must also be configured to allow connections from the client website. 7 | 8 | ## configuring your (server) CiviCRM installation to support CORS 9 | 10 | The browser user interface provided by the RemoteForms extension 11 | (at the /civicrm/admin/remoteform path) is known to work for 12 | drupal7 installations. 13 | 14 | If you have installed the extension on a drupal 8/9 platform, 15 | to enable access from your client website(s) will require instead 16 | that you configure the cors.config stanza in the configuration file 17 | at: web/sites/default/services.yml . 18 | 19 | You can automatically add the required settings by using the Api4 20 | call Remoteform.GenerateCorsServices via the command: 21 | 22 | ``` 23 | cv --user=admin api4 Remoteform.GenerateCorsServices path=/path/to/sites/default/services.yml 24 | ``` 25 | 26 | If you omit the path argument, the settings will be saved in a temp file which 27 | will be provided as output to the cv command. 28 | 29 | This API call respects existing settings in your `services.yml` file so you can 30 | safely run it even if your services.yml file is populated. 31 | 32 | Alternatively, you can modify the file by hand, ensuring it has the following 33 | values: 34 | 35 | ``` 36 | parameters: 37 | cors.config: 38 | enabled: true 39 | allowedOrigins: 40 | - 'https://www.YOUR_WEBSITE_DOMAIN.org' 41 | - 'https://www.ANOTHER_WEBSITE_DOMAIN.org' 42 | allowedMethods: ['HEAD','GET','POST','PUT'] 43 | allowedHeaders: ["content-type"] 44 | ``` 45 | 46 | ## enable encryted communication between the host and client servers 47 | 48 | Note, that both the client site and the server site must be run with encryption 49 | on (e.g. https). 50 | 51 | ## learn more about CORS 52 | 53 | To learn more about CORS, and the other options in your services.yml file, 54 | try these links: 55 | 56 | [CORS documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 57 | [Opt-in CORS support](https://www.drupal.org/node/2715637) 58 | 59 | -------------------------------------------------------------------------------- /docs/wordpress_civicrm.md: -------------------------------------------------------------------------------- 1 | 2 | # Hosting your CiviCRM RemoteForms on a Wordpress site 3 | 4 | The CiviCRM installation where you install the RemoteForms extension, 5 | and in which you collect the data collected by the forms exposed by it 6 | must also be configured to allow connections from the client website. 7 | 8 | ## enabling CORS on a Wordpress site 9 | 10 | Developers with more experience with Wordpress are encouraged to 11 | form this project at: 12 | 13 | * [Fork the project and edit this page](https://github.com/progressivetech/net.ourpowerbase.remoteform) 14 | 15 | In your intial tests, try setting: 16 | 17 | enabled: true 18 | allowedOrigins: ['*'] 19 | allowedMethods: ['*'] 20 | allowedHeaders: ['*'] 21 | 22 | ## tightening security 23 | 24 | Once you have a working interaction between your (server) civicrm installation and 25 | your (client) website, you can begin to dial back the Headers and Methods allowed 26 | settings to determine what the minimum privileges required to make your form work 27 | might be. 28 | 29 | Start by limiting interactions to ONLY your intended (client) website(s): 30 | 31 | allowedOrigins: ['https://www.YOUR_CLIENT_WEBSITE_DOMAIN.org'] 32 | 33 | Next try limiting the allowed Methods, deleting or restoring one at a time, 34 | to determine which are required for successful interactions by your form: 35 | 36 | allowedMethods: ['HEAD','GET','POST','PUT'] 37 | 38 | A custom profile has been found to work between two drupal sites with 39 | ['GET','POST'] enabled. As users successfully test contribution forms 40 | and other civicrm entities as they may be enabled by future versions 41 | of this extension, pull requests are welcome to hone this documentation 42 | to reflect that experience. 43 | 44 | Similar experimentation may reveal the minimum set of headers required 45 | for a working form. Again, please consider offering a pull request 46 | to enhance this documentation to reflect your experience successfully 47 | configuring this extension to work in your environment. 48 | 49 | ## enable encryted communication between the host and client servers 50 | 51 | Note, that both the client site and the server site must be run with encryption on. 52 | Otherwise the connection will be rejected. 53 | 54 | ## learn more about CORS 55 | 56 | To learn more about CORS, and the other options in your services.yml file, 57 | try these links: 58 | 59 | [CORS documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 60 | 61 | -------------------------------------------------------------------------------- /Civi/Api4/Action/Remoteform/GenerateCorsServices.php: -------------------------------------------------------------------------------- 1 | getPath()) { 27 | $this->setPath(tempnam('/tmp', 'remoteform-services')); 28 | } 29 | 30 | $wantCorsConfig = [ 31 | 'enabled' => TRUE, 32 | 'allowedOrigins' => explode("\r\n", \Civi::Settings()->get('remoteform_cors_urls', [])), 33 | 'allowMethods' => ['HEAD', 'GET', 'POST', 'PUT'], 34 | 'allowedHeaders' => ['content-type'], 35 | ]; 36 | 37 | $servicesPath = "sites/default/services.yml"; 38 | if (file_exists($servicesPath)) { 39 | $services = Yaml::parseFile($servicesPath); 40 | } 41 | else { 42 | $services = []; 43 | } 44 | 45 | $parameters = $services['parameters'] || NULL; 46 | if (!$parameters) { 47 | // If we have no parameters key, fill it all and we are done. 48 | $services['parameters'] = [ 49 | 'cors.config' => $corsConfig, 50 | ]; 51 | } 52 | else { 53 | $corsConfig = $services['parameters']['cors.config'] || NULL; 54 | if (!$corsConfig) { 55 | // If we have no cors.config key, fill it all and we are done. 56 | $services['parameters']['cors.config'] = $$corsConfig; 57 | } 58 | else { 59 | // Otherwise, we have to carefully integrate our values into the 60 | // existing values. 61 | foreach ($wantCorsConfig as $key => $wantCorsConfigValues) { 62 | echo "Key: $key\n"; 63 | if ($key == 'enabled') { 64 | // This is always going to be TRUE, overwrite if necessary. 65 | $services['parameters']['cors.config']['enable'] = TRUE; 66 | continue; 67 | } 68 | // Otherwise, iterate over our desired values and append if necessary. 69 | foreach ($wantCorsConfigValues as $wantValue) { 70 | $existingValue = []; 71 | if (array_key_exists($key, $services['parameters']['cors.config'])) { 72 | $existingValue = $services['parameters']['cors.config'][$key]; 73 | } 74 | if (!in_array($wantValue, $existingValue)) { 75 | $services['parameters']['cors.config'][$key][] = $wantValue; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | $yaml = Yaml::dump($services, 4); 82 | file_put_contents($this->getPath(), $yaml); 83 | $result[] = [ 'file' => $this->getPath() ]; 84 | 85 | } 86 | } 87 | 88 | 89 | 90 | 91 | 92 | 93 | ?> 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remoteform 2 | 3 | ----- 4 | 5 | **NOTE**: This extension's days are numbered. See [work on oembed 6 | standard](https://github.com/totten/civicrm-core/blob/master-oembed/ext/oembed/README.md) 7 | in CiviCRM core. Once oembed is fully functional in CiviCRM, this extension 8 | will be deprecated and no longer maintained. 9 | 10 | Remoteform allows you to add a CiviCRM form to a remote web site via a few 11 | lines of javascript code. 12 | 13 | Currently, only profiles and contribution pages are supported (events and 14 | petitions are in the works). 15 | 16 | ## How does it work? 17 | 18 | Full [documentation is available](docs/index.md). See below for an overview. 19 | 20 | First, click `Administer -> Customize data and screens -> Remote Forms.` 21 | 22 | Enter your web site's address. Only the addresses listed here will be able to 23 | submit forms to your CiviCRM instance. 24 | 25 | ![Choose URLs to allow](/images/cors-configuration.png) 26 | 27 | Note: Drupal 8+ users must also [update your services.yml file](docs/drupal_civicrm.md). 28 | 29 | Second, edit the profile or contribution page to enable remoteform. Here's an 30 | example of a profile page (look in `Profile Settings -> Advanced Settings`): 31 | 32 | ![Enable remoteform for a contribution](/images/profile-enable.png) 33 | 34 | Third, copy and paste the provided javascript code to your remote web site and 35 | you are done. 36 | 37 | ![Profile shown on remote site](/images/profile-on-remote-site.png) 38 | 39 | ## Can I configure how the fields are displayed. 40 | 41 | Yes, the javascript api is [fully documented](docs/api.md). You can change just 42 | about everything. 43 | 44 | ## Is this secure? 45 | 46 | This extension does open a tiny hole in your CiviCRM armour. Specifically, it 47 | allows the sites you specify to by-pass the normal 48 | [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) 49 | restrictions. 50 | 51 | CORS prevents one web site from getting your web browser to post data to 52 | another web site, unless the website you are posting to specifically allows it. 53 | 54 | There is a good reason for CORS! The main reason is to prevent one malicious 55 | web site from taking over your browser and posting information to another web 56 | site without your knowledge (for example, a web site could secretly get your 57 | browser to change your password in your CiviCRM installation and then take over 58 | your account). 59 | 60 | Remoteform mitigates against this danger in two ways: 61 | 62 | * You specify the sites to allow. If you specify your organization's web site, 63 | then a malicious user would have to take over your web site first 64 | 65 | * Remoteform refuses to operate if your browser is logged into your CiviCRM 66 | installation. Even if a malicious user could take over your site, they would 67 | not be able to do any damage to your site because all operations are 68 | performed as an anonymous user. 69 | 70 | ## License 71 | 72 | The extension is licensed under [AGPL-3.0](LICENSE.txt). 73 | 74 | ## Requirements 75 | 76 | * PHP v7.0+ 77 | * CiviCRM (5.69) This extension overrides the Contribution Page submit.php file, so you must be 78 | sure to run the exact version of CiviCRM specified. 79 | 80 | ## Known Problems 81 | 82 | If you or any one who wants to fill out a form generated by Remoteform has 83 | [Privacy Badger](https://www.eff.org/privacybadger) or similar software that 84 | restricts javascript from passing data about your session to remote servers, 85 | then Remoteform won't work. It will, however, display a friendly warning 86 | suggesting that the user disable privacy badger or any other security 87 | restrictions that may be in place. 88 | -------------------------------------------------------------------------------- /remoteform.stripe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stripe related functions. Use this file as a model if you are extending 3 | * remoteform to work with another token-based payment processor. 4 | */ 5 | 6 | /** 7 | * 8 | * initStripe 9 | * 10 | * This function is called after the form is created. It allows you to add 11 | * additional elements to it. 12 | * 13 | * submitStripe 14 | * 15 | * This function is called after the user chooses the amount to pay and has 16 | * filled out the profile. 17 | * 18 | * @params is the data they are submitting 19 | * @post is the function we should call after we do our business 20 | * @cfg is the customSubmitDataParams which includes 21 | * configuraiton items specific to this payment processor. 22 | */ 23 | 24 | 25 | // These variables are needed globally. 26 | var stripe_card; 27 | var stripe; 28 | 29 | function initStripe(cfg) { 30 | // Create a div to hold the credit card fields. 31 | ccDiv = document.createElement('div'); 32 | ccDiv.id = 'card-element'; 33 | 34 | // Now ask Stripe to insert their janky iframe. 35 | stripe = Stripe(cfg.customSubmitDataParams.apiKey); 36 | var elements = stripe.elements(); 37 | stripe_card = elements.create('card'); 38 | target = document.getElementById("placeholder_stripe_cc_field").parentElement; 39 | stripe_card.mount(target); 40 | } 41 | 42 | 43 | function submitStripe(params, finalSubmitDataFunc, cfg, remoteformPostFunc) { 44 | console.log("cfg start", cfg); 45 | stripe.createPaymentMethod('card', stripe_card).then(function (result) { 46 | function handleServerResponse(result) { 47 | console.log('handleServerResponse', result); 48 | if (result.is_error) { 49 | // Show error from server on payment form 50 | console.log("Error: ", result); 51 | } else if (result.values.requires_action) { 52 | // Use Stripe.js to handle required card action 53 | handleAction(result.values); 54 | } else { 55 | // All good, we can submit the form 56 | successHandler('paymentIntentID', result.values.paymentIntent); 57 | } 58 | } 59 | 60 | function handleAction(response) { 61 | stripe.handleCardAction(response.payment_intent_client_secret) 62 | .then(function(result) { 63 | if (result.error) { 64 | // Show error in payment form 65 | console.log(result); 66 | } else { 67 | // The card action has been handled 68 | // The PaymentIntent can be confirmed again on the server 69 | successHandler('paymentIntentID', result.paymentIntent); 70 | } 71 | }); 72 | } 73 | 74 | function successHandler(type, object ) { 75 | params['params'][type] = object.id; 76 | console.log("Final post", params); 77 | finalSubmitDataFunc(params); 78 | } 79 | 80 | if (result.error) { 81 | // Show error in payment form 82 | console.log("Problems!", result); 83 | } 84 | else { 85 | var post_params = { 86 | payment_method_id: result.paymentMethod.id, 87 | amount: params['params']['amount'], 88 | currency: 'USD', 89 | payment_processor_id: params['params']['payment_processor_id'], 90 | csrfToken: params['params']['csrfToken'], 91 | session_id: params['params']['session_id'], 92 | description: document.title, 93 | }; 94 | var args = { 95 | entity: 'StripePaymentintent', 96 | action: 'processPublic', 97 | params: post_params, 98 | } 99 | // Send paymentMethod.id to powerbase server 100 | remoteformPostFunc(args, handleServerResponse); 101 | } 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /CRM/Contribute/Form/Contribution/RemoteformConfirm.php: -------------------------------------------------------------------------------- 1 | _id = $params['id']; 39 | 40 | // Added by remoteform: 41 | if (isset($params['test_mode']) && $params['test_mode'] == 1) { 42 | $form->_mode = 'test'; 43 | } 44 | 45 | CRM_Contribute_BAO_ContributionPage::setValues($form->_id, $form->_values); 46 | //this way the mocked up controller ignores the session stuff 47 | $_SERVER['REQUEST_METHOD'] = 'GET'; 48 | $form->controller = new CRM_Contribute_Controller_Contribution(); 49 | $params['invoiceID'] = md5(uniqid(rand(), TRUE)); 50 | 51 | $paramsProcessedForForm = $form->_params = self::getFormParams($params['id'], $params); 52 | 53 | $form->order = new CRM_Financial_BAO_Order(); 54 | $form->order->setPriceSetIDByContributionPageID($params['id']); 55 | $form->order->setPriceSelectionFromUnfilteredInput($params); 56 | if (isset($params['amount']) && !$form->isSeparateMembershipPayment()) { 57 | // @todo deprecate receiving amount, calculate on the form. 58 | $form->order->setOverrideTotalAmount((float) $params['amount']); 59 | } 60 | // hack these in for test support. 61 | $form->_fields['billing_first_name'] = 1; 62 | $form->_fields['billing_last_name'] = 1; 63 | // CRM-18854 - Set form values to allow pledge to be created for api test. 64 | if (!empty($params['pledge_block_id'])) { 65 | $form->_values['pledge_id'] = $params['pledge_id'] ?? NULL; 66 | $form->_values['pledge_block_id'] = $params['pledge_block_id']; 67 | $pledgeBlock = CRM_Pledge_BAO_PledgeBlock::getPledgeBlock($params['id']); 68 | $form->_values['max_reminders'] = $pledgeBlock['max_reminders']; 69 | $form->_values['initial_reminder_day'] = $pledgeBlock['initial_reminder_day']; 70 | $form->_values['additional_reminder_day'] = $pledgeBlock['additional_reminder_day']; 71 | $form->_values['is_email_receipt'] = FALSE; 72 | } 73 | $priceSetID = $form->_params['priceSetId'] = $paramsProcessedForForm['price_set_id']; 74 | $priceFields = CRM_Price_BAO_PriceSet::getSetDetail($priceSetID); 75 | $priceSetFields = reset($priceFields); 76 | $form->_values['fee'] = $priceSetFields['fields']; 77 | $form->_priceSetId = $priceSetID; 78 | $form->setFormAmountFields($priceSetID); 79 | $capabilities = []; 80 | if ($form->_mode) { 81 | $capabilities[] = (ucfirst($form->_mode) . 'Mode'); 82 | } 83 | $form->_paymentProcessors = CRM_Financial_BAO_PaymentProcessor::getPaymentProcessors($capabilities); 84 | $form->_params['payment_processor_id'] = $params['payment_processor_id'] ?? 0; 85 | if ($form->_params['payment_processor_id'] !== '') { 86 | // It can be blank with a $0 transaction - then no processor needs to be selected 87 | $form->_paymentProcessor = $form->_paymentProcessors[$form->_params['payment_processor_id']]; 88 | } 89 | 90 | if (!empty($params['useForMember'])) { 91 | $form->set('useForMember', 1); 92 | $form->_useForMember = 1; 93 | } 94 | $priceFields = $priceFields[$priceSetID]['fields']; 95 | $membershipPriceFieldIDs = []; 96 | foreach ($form->order->getLineItems() as $lineItem) { 97 | if (!empty($lineItem['membership_type_id'])) { 98 | $form->set('useForMember', 1); 99 | $form->_useForMember = 1; 100 | $membershipPriceFieldIDs['id'] = $priceSetID; 101 | $membershipPriceFieldIDs[] = $lineItem['price_field_value_id']; 102 | } 103 | } 104 | $form->set('memberPriceFieldIDS', $membershipPriceFieldIDs); 105 | $form->setRecurringMembershipParams(); 106 | // Modified by remoteform: 107 | // $form->processFormSubmission($params['contact_id'] ?? NULL); 108 | return $form->processFormSubmission($params['contact_id'] ?? NULL); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /CRM/Remoteform/Form/RemoteformSettings.php: -------------------------------------------------------------------------------- 1 | 'remoteform'); 12 | private $_settings = NULL; 13 | private $_submittedValues = array(); 14 | 15 | function buildQuickForm() { 16 | CRM_Utils_System::setTitle(E::ts("Remote Form Settings")); 17 | 18 | $settings = $this->getFormSettings(); 19 | foreach ($settings as $name => $setting) { 20 | if (isset($setting['quick_form_type'])) { 21 | $add = 'add' . $setting['quick_form_type']; 22 | if ($add == 'addElement') { 23 | $this->$add($setting['html_type'], $name, ts($setting['title']), CRM_Utils_Array::value('html_attributes', $setting, array ())); 24 | } 25 | elseif ($setting['html_type'] == 'Select') { 26 | $optionValues = array(); 27 | if (!empty($setting['pseudoconstant'])) { 28 | if(!empty($setting['pseudoconstant']['optionGroupName'])) { 29 | $optionValues = CRM_Core_OptionGroup::values($setting['pseudoconstant']['optionGroupName'], FALSE, FALSE, FALSE, NULL, 'name'); 30 | } 31 | elseif (!empty($setting['pseudoconstant']['callback'])) { 32 | $cb = Civi\Core\Resolver::singleton()->get($setting['pseudoconstant']['callback']); 33 | $optionValues = call_user_func_array($cb, $optionValues); 34 | } 35 | } 36 | $this->add('select', $setting['name'], $setting['title'], $optionValues, FALSE, $setting['html_attributes']); 37 | } 38 | else { 39 | $this->$add($name, ts($setting['title'])); 40 | } 41 | $this->assign("{$setting['description']}_description", ts('description')); 42 | } 43 | } 44 | $this->addButtons(array( 45 | array ( 46 | 'type' => 'submit', 47 | 'name' => ts('Submit'), 48 | 'isDefault' => TRUE, 49 | ) 50 | )); 51 | // export form elements 52 | $this->assign('elementNames', $this->getRenderableElementNames()); 53 | parent::buildQuickForm(); 54 | } 55 | function postProcess() { 56 | $this->_submittedValues = $this->exportValues(); 57 | $this->saveSettings(); 58 | parent::postProcess(); 59 | } 60 | 61 | /** 62 | * Get the fields/elements defined in this form. 63 | * 64 | * @return array (string) 65 | */ 66 | function getRenderableElementNames() { 67 | // The _elements list includes some items which should not be 68 | // auto-rendered in the loop -- such as "qfKey" and "buttons". These 69 | // items don't have labels. We'll identify renderable by filtering on 70 | // the 'label'. 71 | $elementNames = array(); 72 | foreach ($this->_elements as $element) { 73 | $label = $element->getLabel(); 74 | if (!empty($label)) { 75 | $elementNames[] = $element->getName(); 76 | } 77 | } 78 | return $elementNames; 79 | } 80 | /** 81 | * Get the settings we are going to allow to be set on this form. 82 | * 83 | * @return array 84 | */ 85 | function getFormSettings() { 86 | if (is_null($this->_settings)) { 87 | $this->_settings = array(); 88 | $result = civicrm_api3('setting', 'getfields', array('filters' => $this->_settingFilter)); 89 | $this->_settings = $result['values']; 90 | } 91 | return $this->_settings; 92 | } 93 | 94 | /** 95 | * Get the settings we are going to allow to be set on this form. 96 | * 97 | * @return array 98 | */ 99 | function saveSettings() { 100 | $settings = $this->getFormSettings(); 101 | $values = array_intersect_key($this->_submittedValues, $settings); 102 | civicrm_api3('setting', 'create', $values); 103 | $session = CRM_Core_Session::singleton(); 104 | $session->setStatus(E::ts("Settings were saved."), E::ts("Remote Form"), "success"); 105 | 106 | } 107 | /** 108 | * Set defaults for form. 109 | * 110 | * @see CRM_Core_Form::setDefaultValues() 111 | */ 112 | function setDefaultValues() { 113 | $existing = civicrm_api3('setting', 'get', array('return' => array_keys($this->getFormSettings()))); 114 | $defaults = array(); 115 | $domainID = CRM_Core_Config::domainID(); 116 | foreach ($existing['values'][$domainID] as $name => $value) { 117 | $defaults[$name] = $value; 118 | } 119 | return $defaults; 120 | } 121 | 122 | /** 123 | * Rules callback. 124 | */ 125 | public function addRules() { 126 | $this->addFormRule(array('CRM_Remoteform_Form_RemoteformSettings', 'myRules')); 127 | } 128 | 129 | static function myRules($values) { 130 | $errors = array(); 131 | 132 | $urls = explode("\n", $values['remoteform_cors_urls']); 133 | foreach($urls as $url) { 134 | if (substr($url, 0, 8) != 'https://') { 135 | $errors['remoteform_cors_urls'] = E::ts('Please add one URL per line and ensure they all start with https://'); 136 | } 137 | if (substr($url, -1, 1) == '/') { 138 | $errors['remoteform_cors_urls'] = E::ts('URLs should not end in a slash.'); 139 | } 140 | } 141 | 142 | return empty($errors) ? TRUE : $errors; 143 | } 144 | } 145 | 146 | 147 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Remoteform 2 | 3 | Remoteform is intended for CiviCRM users who maintain their web site on a 4 | separate server from their CiviCRM installation (e.g. for 5 | [Powerbase](https://ourpowerbase.net/) users). 6 | 7 | With remoteform you can add a fully functional CiviCRM form to a remote web 8 | site via a few lines of javascript code. Your site visitors will no longer need 9 | to be re-directed to your CiviCRM installation to fill out a profile or make a 10 | contribution. There is no need to match your CiviCRM theme with your web site 11 | look and feel. *The entire interaction takes place on your own web site.* 12 | 13 | Furthermore, with the help of a fully documented [api](api.md) users with 14 | advanced javascript and CSS skills can make the form look exactly how you want 15 | it, so it completely blends in with your web site's look and feel. 16 | 17 | To use Remoteform, your web site must run via https. 18 | 19 | Using Remoteform is a two step process. 20 | 21 | 1. Set things up in CiviCRM. 22 | 2. Set things up on your web site 23 | 24 | Before you begin, be sure you are logged into both CiviCRM and your web site. 25 | 26 | ## In CiviCRM 27 | 28 | First, install and enable the Remoteform extension. 29 | 30 | Second, click `Adminstration -> Customize data and screens -> Remote Forms.` 31 | 32 | Enter your web site's address. Only the addresses listed here will be able to 33 | submit forms to your CiviCRM instance. 34 | 35 | ![Choose URLs to allow](/images/cors-configuration.png) 36 | 37 | If this configuration is not sufficient to enable access to the CiviCRM server 38 | from your intended client site, try the guidance provided at this link: 39 | 40 | * [Enable CORS access on a Drupal site hosting CiviCRM RemoteForms as a service](drupal_civicrm.md) 41 | * [Enable CORS access on a Wordpress site hosting CiviCRM RemoteForms as a service](wordpress_civicrm.md) 42 | 43 | Third, edit the profile or contribution page to enable remoteform. Here's an 44 | example of a profile page (look in `Profile Settings -> Advanced Settings`): 45 | 46 | ![Enable remoteform for a contribution](/images/profile-enable.png) 47 | 48 | If you want to place a contribution page on your web site, you will see the 49 | same field option on the main Title configuration tab of all your contribution 50 | pages. 51 | 52 | **Important**: Be sure to click save after you click the checkbox! If you don't 53 | save the profile or contribution page, then Remoteform will not work. 54 | 55 | Once saved, then go back in and copy the javascript code. 56 | 57 | ## In your web site 58 | 59 | Everyone's web site is different. Here are examples for how to paste in 60 | javascript code on Drupal and Wordpress client sites, two of the most 61 | popular tools for building web sites. 62 | 63 | * [Add remoteform to a (client) website hosted on Drupal](drupal_website.md) 64 | * [Add remoteform to a (client) website hosted on Wordpress](wordpress_website.md) 65 | 66 | Most web site editing tools do not let you paste in javascript code without 67 | making some kind of adjustment. 68 | 69 | ## Debugging 70 | 71 | When you add your javascript code, you may get an error telling you to check the console log. 72 | 73 | ![Check console log](/images/check-console-log.png) 74 | 75 | You can do that in either Firefox or Chrome by right clicking on the page and choosing the Inspector. Below is what it looks like in Firefox. 76 | 77 | ![Right click to inspect](/images/right-click-inspect.png) 78 | 79 | Click the Console tab, and check for messages. 80 | 81 | ![Check console log](/images/debug-console.png) 82 | 83 | In this case, the error message is telling us that CORS is not set correctly. 84 | That means that the web site you're pasting the javascript code into has not been 85 | properly configured in CiviCRM on the Remote Forms page. 86 | 87 | ## Debugging Undisplayed Fields and Unstored Data 88 | 89 | When building your profile (at /civicrm/admin/uf/group?reset=1), do not use 'Primary' 90 | for any of your Contact fields. For whatever reason, although the fields are exhibited 91 | on the (client) website, the data does not get stored on the (server) CiviCRM site. 92 | 93 | It appears that if you omit the country selector field on your form, that the state 94 | selector will be built on a default country matching that of the 'Default Organization 95 | Address' configured at: /civicrm/admin/domain. 96 | 97 | If this guidance is not enough to ensure that your profile fields configured 98 | on the (server) CiviCRM installation collect data on the (client) website, 99 | and store that data in the database hosted on the (server) CiviCRM installation, 100 | try running this API call via the command line or the API explorer on the 101 | (server) CiviCRM installation: 102 | 103 | vendor/bin/cv api Profile.getfields api_action=submit profile_id=YOURPROFILEID 104 | 105 | If your missing field shows up in response to this query, 106 | then this is probably a remoteform bug. 107 | 108 | * [Report bugs in the RemoteForm CiviCRM extension](https://github.com/progressivetech/net.ourpowerbase.remoteform/issues) 109 | 110 | If it does not show up, then it's a core api v3 bug. 111 | 112 | * [Report bugs in the CiviCRM API for v3](https://lab.civicrm.org/dev/core/-/issues) 113 | 114 | This extension is built using the CiviCRM API version 3. Now that 115 | [API version 4](https://docs.civicrm.org/dev/en/latest/api/v4/usage/) is on the scene, 116 | not all of the core API version 3 bugs will get fixed so please open an issue here if 117 | you do not get any movement on an API version 3 issue in core. In the long run, this 118 | extension will either need to switch to API version 4, and/or intergrate with 119 | [afform](https://lab.civicrm.org/dev/core/-/tree/master/ext/afform). 120 | 121 | -------------------------------------------------------------------------------- /remoteform.civix.php: -------------------------------------------------------------------------------- 1 | getUrl(self::LONG_NAME), '/'); 47 | } 48 | return CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME, $file); 49 | } 50 | 51 | /** 52 | * Get the path of a resource file (in this extension). 53 | * 54 | * @param string|NULL $file 55 | * Ex: NULL. 56 | * Ex: 'css/foo.css'. 57 | * @return string 58 | * Ex: '/var/www/example.org/sites/default/ext/org.example.foo'. 59 | * Ex: '/var/www/example.org/sites/default/ext/org.example.foo/css/foo.css'. 60 | */ 61 | public static function path($file = NULL) { 62 | // return CRM_Core_Resources::singleton()->getPath(self::LONG_NAME, $file); 63 | return __DIR__ . ($file === NULL ? '' : (DIRECTORY_SEPARATOR . $file)); 64 | } 65 | 66 | /** 67 | * Get the name of a class within this extension. 68 | * 69 | * @param string $suffix 70 | * Ex: 'Page_HelloWorld' or 'Page\\HelloWorld'. 71 | * @return string 72 | * Ex: 'CRM_Foo_Page_HelloWorld'. 73 | */ 74 | public static function findClass($suffix) { 75 | return self::CLASS_PREFIX . '_' . str_replace('\\', '_', $suffix); 76 | } 77 | 78 | } 79 | 80 | use CRM_Remoteform_ExtensionUtil as E; 81 | 82 | /** 83 | * (Delegated) Implements hook_civicrm_config(). 84 | * 85 | * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config 86 | */ 87 | function _remoteform_civix_civicrm_config($config = NULL) { 88 | static $configured = FALSE; 89 | if ($configured) { 90 | return; 91 | } 92 | $configured = TRUE; 93 | 94 | $extRoot = __DIR__ . DIRECTORY_SEPARATOR; 95 | $include_path = $extRoot . PATH_SEPARATOR . get_include_path(); 96 | set_include_path($include_path); 97 | // Based on , this does not currently require mixin/polyfill.php. 98 | } 99 | 100 | /** 101 | * Implements hook_civicrm_install(). 102 | * 103 | * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install 104 | */ 105 | function _remoteform_civix_civicrm_install() { 106 | _remoteform_civix_civicrm_config(); 107 | // Based on , this does not currently require mixin/polyfill.php. 108 | } 109 | 110 | /** 111 | * (Delegated) Implements hook_civicrm_enable(). 112 | * 113 | * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable 114 | */ 115 | function _remoteform_civix_civicrm_enable(): void { 116 | _remoteform_civix_civicrm_config(); 117 | // Based on , this does not currently require mixin/polyfill.php. 118 | } 119 | 120 | /** 121 | * Inserts a navigation menu item at a given place in the hierarchy. 122 | * 123 | * @param array $menu - menu hierarchy 124 | * @param string $path - path to parent of this item, e.g. 'my_extension/submenu' 125 | * 'Mailing', or 'Administer/System Settings' 126 | * @param array $item - the item to insert (parent/child attributes will be 127 | * filled for you) 128 | * 129 | * @return bool 130 | */ 131 | function _remoteform_civix_insert_navigation_menu(&$menu, $path, $item) { 132 | // If we are done going down the path, insert menu 133 | if (empty($path)) { 134 | $menu[] = [ 135 | 'attributes' => array_merge([ 136 | 'label' => $item['name'] ?? NULL, 137 | 'active' => 1, 138 | ], $item), 139 | ]; 140 | return TRUE; 141 | } 142 | else { 143 | // Find an recurse into the next level down 144 | $found = FALSE; 145 | $path = explode('/', $path); 146 | $first = array_shift($path); 147 | foreach ($menu as $key => &$entry) { 148 | if ($entry['attributes']['name'] == $first) { 149 | if (!isset($entry['child'])) { 150 | $entry['child'] = []; 151 | } 152 | $found = _remoteform_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item); 153 | } 154 | } 155 | return $found; 156 | } 157 | } 158 | 159 | /** 160 | * (Delegated) Implements hook_civicrm_navigationMenu(). 161 | */ 162 | function _remoteform_civix_navigationMenu(&$nodes) { 163 | if (!is_callable(['CRM_Core_BAO_Navigation', 'fixNavigationMenu'])) { 164 | _remoteform_civix_fixNavigationMenu($nodes); 165 | } 166 | } 167 | 168 | /** 169 | * Given a navigation menu, generate navIDs for any items which are 170 | * missing them. 171 | */ 172 | function _remoteform_civix_fixNavigationMenu(&$nodes) { 173 | $maxNavID = 1; 174 | array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) { 175 | if ($key === 'navID') { 176 | $maxNavID = max($maxNavID, $item); 177 | } 178 | }); 179 | _remoteform_civix_fixNavigationMenuItems($nodes, $maxNavID, NULL); 180 | } 181 | 182 | function _remoteform_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) { 183 | $origKeys = array_keys($nodes); 184 | foreach ($origKeys as $origKey) { 185 | if (!isset($nodes[$origKey]['attributes']['parentID']) && $parentID !== NULL) { 186 | $nodes[$origKey]['attributes']['parentID'] = $parentID; 187 | } 188 | // If no navID, then assign navID and fix key. 189 | if (!isset($nodes[$origKey]['attributes']['navID'])) { 190 | $newKey = ++$maxNavID; 191 | $nodes[$origKey]['attributes']['navID'] = $newKey; 192 | $nodes[$newKey] = $nodes[$origKey]; 193 | unset($nodes[$origKey]); 194 | $origKey = $newKey; 195 | } 196 | if (isset($nodes[$origKey]['child']) && is_array($nodes[$origKey]['child'])) { 197 | _remoteform_civix_fixNavigationMenuItems($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /api/v3/RemoteFormContributionPage/Submit.php: -------------------------------------------------------------------------------- 1 | $values['title'], 77 | 'intro_text' => $values['intro_text'], 78 | 'thankyou_text' => $values['thankyou_text'], 79 | ); 80 | $params['control'] = array( 81 | 'is_active' => $values['is_active'], 82 | 'start_date' => $values['start_date'], 83 | 'currency' => $values['currency'], 84 | 'min_amount' => $values['min_amount'], 85 | 'payment_processor' => $ppid, 86 | ); 87 | } 88 | 89 | function _rf_add_profile_fields($id, &$params) { 90 | // Now get profile fields. 91 | $result = civicrm_api3('UFJoin', 'get', array( 92 | 'module' => 'CiviContribute', 93 | 'entity_table' => 'civicrm_contribution_page', 94 | 'entity_id' => $id, 95 | 'return' => array('uf_group_id') 96 | )); 97 | require_once('api/v3/Profile.php'); 98 | foreach($result['values'] as $value) { 99 | $uf_group_id = $value['uf_group_id']; 100 | $uf_result = civicrm_api3('Profile', 'getfields', array( 101 | 'api_action' => 'submit', 102 | 'profile_id' => $uf_group_id, 103 | 'get_options' => 'all' 104 | )); 105 | foreach($uf_result['values'] as $field_name => $field) { 106 | $params[$field_name] = $field; 107 | } 108 | } 109 | } 110 | 111 | function _rf_add_price_fields($id, &$params, $currency = 'USD') { 112 | $sql = "SELECT fv.id, fv.name, fv.label, fv.help_pre, fv.help_post, fv.amount, 113 | fv.is_default, pse.price_set_id, pf.id AS price_field_id, ps.name as price_set_name 114 | FROM civicrm_price_field_value fv 115 | JOIN civicrm_price_field pf ON fv.price_field_id = pf.id 116 | JOIN civicrm_price_set ps ON ps.id = pf.price_set_id 117 | JOIN civicrm_price_set_entity pse ON pse.price_set_id = pf.price_set_id 118 | WHERE pse.entity_table = 'civicrm_contribution_page' AND pse.entity_id = %0 119 | AND fv.is_active = 1 AND pf.is_active = 1 AND ps.is_active = 1"; 120 | $dao = CRM_Core_DAO::executeQuery($sql, array(0 => array($id, 'Integer'))); 121 | $options = array(); 122 | $default_value = NULL; 123 | $i = 0; 124 | $key = NULL; 125 | while($dao->fetch()) { 126 | $options[$dao->id] = array( 127 | 'amount' => $dao->amount, 128 | 'label' => $dao->label, 129 | 'name' => $dao->name, 130 | 'currency' => $currency, 131 | // This is unused in most cases, but is required for any "other_amount" options 132 | // so we know which price field it belongs to. 133 | 'price_field_id' => $dao->price_field_id, 134 | ); 135 | if ($dao->is_default) { 136 | $default_value = $dao->id; 137 | } 138 | if (is_null($key) && $dao->price_set_name != 'other_amount') { 139 | $key = 'price_' . $dao->price_field_id; 140 | } 141 | } 142 | $params[$key] = array( 143 | 'title' => 'Choose Amount', 144 | 'default_value' => $default_value, 145 | 'entity' => 'contribution', 146 | 'options' => $options, 147 | 'html' => array( 148 | 'type' => 'Radio' 149 | ), 150 | ); 151 | $params['price_set_id'] = array( 152 | 'title' => ts("Price Set ID"), 153 | 'default_value' => $dao->price_set_id, 154 | 'entity' => 'contribution', 155 | 'html' => array('type' => 'hidden'), 156 | ); 157 | $params['payment_instrument_id'] = array( 158 | 'title' => ts("Payment Instrument ID"), 159 | 'default_value' => CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_FinancialTrxn', 'payment_instrument_id', 'Credit Card'), 160 | 'entity' => 'contribution', 161 | 'html' => array('type' => 'hidden'), 162 | ); 163 | } 164 | 165 | function _rf_add_credit_card_fields(&$params) { 166 | $params['credit_card_number'] = array( 167 | 'title' => 'Credit Card', 168 | 'default_value' => '', 169 | 'entity' => 'contribution', 170 | 'api.required' => 1, 171 | 'html' => array( 172 | 'type' => 'Text' 173 | ), 174 | ); 175 | 176 | $params['cvv2'] = array( 177 | 'title' => 'CVV', 178 | 'default_value' => '', 179 | 'entity' => 'contribution', 180 | 'api.required' => 1, 181 | 'html' => array( 182 | 'type' => 'Text' 183 | ), 184 | ); 185 | 186 | $params['credit_card_exp_date_M'] = array( 187 | 'title' => 'Exp Month', 188 | 'default_value' => '', 189 | 'entity' => 'contribution', 190 | 'api.required' => 1, 191 | 'html' => array( 192 | 'type' => 'select' 193 | ), 194 | 'options' => array( 195 | '1' => '01 - ' . ts('Jan'), 196 | '2' => '02 - ' . ts('Feb'), 197 | '3' => '03 - ' . ts('Mar'), 198 | '4' => '04 - ' . ts('Apr'), 199 | '5' => '05 - ' . ts('May'), 200 | '6' => '06 - ' . ts('Jun'), 201 | '7' => '07 - ' . ts('Jul'), 202 | '8' => '08 - ' . ts('Aug'), 203 | '9' => '09 - ' . ts('Sep'), 204 | '10' => '10 - ' . ts('Oct'), 205 | '11' => '11 - ' . ts('Nov'), 206 | '12' => '12 - ' . ts('Dec'), 207 | ), 208 | ); 209 | $params['credit_card_exp_date_Y'] = array( 210 | 'title' => 'Exp Year', 211 | 'default_value' => '', 212 | 'entity' => 'contribution', 213 | 'api.required' => 1, 214 | 'html' => array( 215 | 'type' => 'select' 216 | ), 217 | 'options' => array(), 218 | ); 219 | 220 | $start_year = date('Y'); 221 | $end_year = $start_year + 30; 222 | $year = $start_year; 223 | while($year < $end_year) { 224 | $params['credit_card_exp_date_Y']['options'][$year] = $year; 225 | $year++; 226 | } 227 | } 228 | 229 | /** 230 | * 231 | * Add CSRF token 232 | * 233 | * If you have the firewall extension enabled, it might 234 | * reject this submission when it is submitted unless 235 | * it has a valid CSRF token. 236 | * 237 | */ 238 | function _rf_add_csrf_token_fields(&$params) { 239 | if (class_exists('\Civi\Firewall\Firewall')) { 240 | $params['csrfToken'] = array( 241 | 'title' => 'CSRF token', 242 | 'default_value' => \Civi\Firewall\Firewall::getCSRFToken(), 243 | 'entity' => 'contribution', 244 | 'html' => array( 245 | 'type' => 'hidden' 246 | ), 247 | ); 248 | $params['session_id'] = array( 249 | 'title' => 'Session Id', 250 | 'default_value' => \CRM_Core_Config::singleton()->userSystem->getSessionId(), 251 | 'entity' => 'contribution', 252 | 'html' => array( 253 | 'type' => 'hidden' 254 | ), 255 | ); 256 | } 257 | } 258 | 259 | 260 | /** 261 | * RemoteFormContributionPage.Submit API 262 | * 263 | * @param array $params 264 | * @return array API result descriptor 265 | * @see civicrm_api3_create_success 266 | * @see civicrm_api3_create_error 267 | * @throws API_Exception 268 | */ 269 | function civicrm_api3_remote_form_contribution_page_Submit($params) { 270 | // We translate a few fields to ensure compatability with all payment 271 | // processors. 272 | $params['id'] = $params['contribution_page_id']; 273 | $params['month'] = CRM_Core_Payment_Form::getCreditCardExpirationMonth($params); 274 | $params['year'] = CRM_Core_Payment_Form::getCreditCardExpirationYear($params); 275 | 276 | // Now, submit. 277 | $result = CRM_Contribute_Form_Contribution_RemoteformConfirm::submit($params); 278 | 279 | // First check for payment failure. 280 | if ($result['is_payment_failure']) { 281 | $msg = $result['error']->getMessage(); 282 | return civicrm_api3_create_error($msg); 283 | } 284 | 285 | // $result gives us a contribution object, which will cause 286 | // civicrm_api3_create_success to fail. We have to convert it to 287 | // an array and remove some of the db object garbage that we don't need. 288 | $contribution = (Array)$result['contribution']; 289 | $result['contribution'] = array(); 290 | foreach($contribution as $key => $value) { 291 | $first_character = substr($key, 0, 1); 292 | if ($first_character == '_' || $key == 'N' ) { 293 | continue; 294 | } 295 | $result['contribution'][$key] = $value; 296 | } 297 | return civicrm_api3_create_success(array($result), $params, 'RemoteFormContributionPage', 'submit'); 298 | } 299 | 300 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # Remoteform 2 | ## Introduction 3 | 4 | All example code uses YOURSITE.ORG in place of the domain name of your 5 | actual CiviCRM installation. 6 | 7 | In addition, the examples are written from Drupal paths, but WordPress and 8 | Joomla should work just as well by substituting the paths for ones 9 | appropriate to your CMS. 10 | 11 | ## Overview 12 | 13 | When including a remoteform on a web site, there are three kinds of 14 | resources to provide. 15 | 16 | ### CSS, Javascript and HTML 17 | 18 | Typically, you will start with: 19 | 20 | ``` 21 | 22 | ``` 23 | 24 | The CSS line is purely optional. The code generates HTML with Bootstrap 25 | based classes, so if your web site uses the Bootstrap CSS framework you 26 | should omit the CSS line entirely and it should integrate perfectly well. 27 | 28 | Alternatively, you can adjust your web site CSS based on the example css to 29 | fully control the display. 30 | 31 | Or, if you use a different CSS framework, see below for how you can change 32 | the CSS classes that are printed. 33 | 34 | Next up: 35 | 36 | ``` 37 | 38 | ``` 39 | 40 | This line is required, it pulls in the javascript that makes everything 41 | work. The javascript has no dependencies and should not conflict with any 42 | existing libraries in use by your site. 43 | 44 | And lastly: 45 | 46 | ``` 47 |
48 | ``` 49 | 50 | You have to provide a div that will enclose the form created. See below if 51 | you want to use a div already present on your page but with a different id. 52 | 53 | ### The parameters 54 | 55 | You must pass a config option to the remoteForm function: 56 | 57 | ``` 58 | var remoteFormConfig = { 59 | url: "https://YOURSITE.ORG/civicrm/remoteform", 60 | id: 1, 61 | entity: "ContributionPage" 62 | }; 63 | ``` 64 | 65 | The minimum parameters are url, id and entity. See below for details on all 66 | the available parameters. 67 | 68 | ### The function 69 | 70 | Finally, you have to call the function: 71 | 72 | ``` 73 | remoteForm(remoteFormConfig); 74 | ``` 75 | ## Properties 76 | 77 | ### cfg 78 | 79 | cfg is a sanitized global configuration object based on config, which 80 | is the object passed in by the user. All the parameters below can be 81 | changed by adding or editing your ```var config``` line. For example, 82 | if you read about cfg.parentElementId and decide you want to change the 83 | parentElementId, you would pass the following to the remoteForm function: 84 | 85 | ``` 86 | var config = { 87 | url: "https://YOURSITE.ORG/civicrm/remoteform", 88 | id: 1, 89 | entity: "ContributionPage", 90 | parentElementId: "my-parent-id" 91 | }; 92 | ``` 93 | ### cfg.url 94 | 95 | The url of the CiviCRM installation we are posting to. Required. 96 | ### cfg.id 97 | 98 | The id of the entity (profile id, contribution page id, etc.). Required. 99 | ### cfg.parentElementId 100 | 101 | The id of the element to which the form will be appended. Default: remoteform. 102 | ### cfg.entity 103 | 104 | The CiviCRM entity we are creating (currently only Profile and ContributionPage are supported). 105 | Default: Profile. 106 | ### cfg.paymentTestMode 107 | 108 | For ContributionPage entities only, indicates whether you should submit to the 109 | test payment processor or the live payment processor. Default: false 110 | ### cfg.autoInit 111 | 112 | How the form will be initialized - either true if the form will be 113 | initialized on page load or false if a button will need to be clicked in 114 | order to display the form. Default: true. 115 | ### cfg.initTxt 116 | 117 | If cfg.autoInit is false, the text displayed on the button to click 118 | for the user to display the form. Default: Fill in the form. 119 | ### cfg.submitTxt 120 | 121 | The text to display on the form's submit button. Default: Submit. 122 | ### cfg.cancelTxt 123 | 124 | The text to display on the form's cancel button. Default: Cancel. 125 | ### cfg.successMsg 126 | 127 | The message displayed to the user upon successful submission of the 128 | form. Default: Thank you! Your submission was received. 129 | ### cfg.displayLabels 130 | Whether or not the form labels should be displayed when placeholder 131 | text could be used instead to save space. Default: false. 132 | ### cfg.createFieldDivFunc 133 | 134 | Custom function to override the function used to create html fields 135 | from the field definitions. If you don't like the way your fields are 136 | being turned into html, set this parameter to a funciton that you have 137 | defined and you can completley control the creation of all fields. 138 | See createFieldDiv for instructions on how to create your custom function. 139 | ### cfg.customSubmitDataFunc 140 | 141 | Customize the post action if you want to use a token-based payment processor 142 | and you need to send credit card details to a different server before sending 143 | them to CivICRM.. 144 | 145 | If you define this function, it should accept the following arguments. 146 | 147 | - params - the fields submitted by the user, including the amount 148 | field 149 | - submitDataPost - after your function has done it's business, you 150 | should call this function, passing in params (which can be modified 151 | by your function, for example, to remove the credit card number), to 152 | complete the process and send the info back to CiviCRM. 153 | - customSubmitDataParams - any custom parameters passed by the user (see 154 | below). You may need to the user to include an api key, etc. 155 | 156 | See remoteform.stripe.js for an example. 157 | ### cfg.customInitFunc 158 | 159 | Trigger javascript code to run after the form is built. 160 | 161 | If you define this function, it should accept one argument. 162 | 163 | id: The html element id of the enclosing div. 164 | 165 | See remoteform.stripe.js for an example. 166 | ### cfg.customSubmitDataParams 167 | 168 | An object containing any data that is specific to your customSubmitDataFunc, 169 | such as an api key, etc. 170 | ### cfg.css 171 | 172 | Indicate classes that should be used on various parts of the form if you 173 | want more control over look and feel. The defaults are designed to work 174 | with bootstrap. If you are not using bootstrap, you may want to include 175 | the remoteform.css file which tries to make things look nice with the 176 | default classes. 177 | #### cfg.css.userSuccessMsg 178 | 179 | Default: alert alert-success 180 | #### cfg.css.FailureMsg 181 | 182 | Default: alert alert-warning 183 | #### cfg.css.button 184 | 185 | Default: btn btn-info 186 | #### cfg.css.form. 187 | 188 | Default: rf-form 189 | #### cfg.css.inputDiv 190 | 191 | Default: form-group 192 | #### cfg.css.checkDiv 193 | 194 | Default: form-check 195 | #### cfg.css.input 196 | 197 | Default: form-control 198 | #### cfg.css.checkInput 199 | 200 | Default: form-check-input 201 | #### cfg.css.textarea 202 | 203 | Default: form-control 204 | #### cfg.css.label 205 | 206 | Default: rf-label 207 | #### cfg.css.sr_only 208 | 209 | Default: sr-only 210 | #### cfg.css.checkLabel 211 | 212 | Default: form-check-label 213 | #### cfg.css.select 214 | 215 | Default: custom-select 216 | #### cfg.css.small 217 | 218 | Default: text-muted form-text 219 | ## Functions related to building html widgets. 220 | 221 | ### createFieldDiv 222 | 223 | ```createFieldDiv(key, def, type, createFieldFunc, wrapFieldFunc)``` 224 | 225 | Create a single field with associated label wrapped in a div. 226 | 227 | This function can be overridden using the createFieldDivFunc config 228 | parameter. 229 | 230 | #### Parameters: 231 | 232 | - key - the unique field key 233 | - type - the type of field to build 234 | - createFieldFunc - the function to use to build the field, override 235 | as needed. See createField for a model. 236 | - wrapFieldFunc - the function to use to build the div around the field, 237 | override as needed. See wrapField as a model. 238 | 239 | #### Returns: 240 | 241 | An HTML entity including both a field label and field. 242 | 243 | If you don't override this function, it will do the following: 244 | 245 | ``` 246 | var field = createFieldFunc(key, def, type); 247 | if (field === null) { 248 | return null; 249 | } 250 | return wrapFieldFunc(key, def, field); 251 | } 252 | ``` 253 | 254 | By overriding the class, you can do things like create your own 255 | createFieldFunc or wrapFieldFunc, then simply call createFieldDiv but pass 256 | it your own function names instead of the default ones. Or you can pick 257 | out a field type you want to cusotmize or even a field key and only change 258 | the behavior for that one. 259 | 260 | Here's an example of overriding createFieldFunc to change the list of 261 | groups displayed when a profile includes groups. 262 | 263 | ``` 264 | function myCreateFieldDiv(key, def, type, createFieldFunc, wrapFieldFunc) { 265 | if (key == 'group_id') { 266 | def.options = { 267 | 1: "Group one", 268 | 2: "group two" 269 | } 270 | } 271 | var field = createFieldFunc(key, def, type); 272 | if (field === null) { 273 | return null; 274 | } 275 | return wrapFieldFunc(key, def, field); 276 | } 277 | ``` 278 | 279 | Once you have defined this function, you could pass in the paramter: 280 | 281 | ``` 282 | createFieldDivFunc: myCreateFieldDiv 283 | ``` 284 | 285 | to your remoteFormConfig. 286 | 287 | ### getType 288 | 289 | ```getType(def)``` 290 | 291 | If you pass in a field definition provided by CiviCRM, this function 292 | returns an html input type, working around some CiviCRM idiosyncracies. 293 | 294 | #### Parameters 295 | - def - a CiviCRM provided field definition object. 296 | 297 | #### Returns 298 | A string field type 299 | ### createField 300 | 301 | ```createField(key, def, type)``` 302 | 303 | Return an HTML entity that renders the given field. 304 | 305 | #### Parameters 306 | - key - The unique string id for the field 307 | - def - The CiviCRM provided field definition object. 308 | - type - The string type for the field. 309 | 310 | ### Returns 311 | 312 | HTML entity. 313 | ### wrapField 314 | 315 | ```wrapField(key, def, field)``` 316 | 317 | Return an HTML entity that includes both the given field and a label. 318 | 319 | #### Parameters 320 | - key - The unique string id for the field 321 | - def - The CiviCRM provided field definition object. 322 | - field - An HTML entity with the field 323 | 324 | ### Returns 325 | 326 | HTML entity. 327 | Check if this is an "other amount" price set 328 | 329 | Some price set options should only be displayed if the user has 330 | clicked the "other amount" option. Unfortunately, it's hard to 331 | tell if an option is an other amount option. With normal price sets 332 | the option has the name "Other_Amount" - however, if you have a 333 | contribution page and you are not using price sets, then it's called 334 | Contribution_Amount. 335 | 336 | This function return true if we think this is an other amount 337 | option or false otherwise. 338 | Checkbox and Radio collections. 339 | Populate a location drop down with the appropriate values. 340 | 341 | We dynamically populate the state/province, county and country 342 | drop down lists by querying CiviCRM for the appropriate values. 343 | 344 | In the case of state province, the right values will depend on the 345 | chosen country. In the case of county, the right values will depend 346 | on the chosen state. 347 | -------------------------------------------------------------------------------- /remoteform.php: -------------------------------------------------------------------------------- 1 | E::ts('Remote Forms'), 54 | 'name' => 'Remote Forms', 55 | 'url' => 'civicrm/admin/remoteform', 56 | 'permission' => 'access CiviCRM', 57 | 'operator' => 'OR', 58 | 'separator' => 0, 59 | )); 60 | _remoteform_civix_navigationMenu($menu); 61 | } 62 | 63 | /* 64 | * Implementation of hook_idsException. 65 | * 66 | * Ensure we don't get caught in the IDS check. 67 | */ 68 | function remoteform_civicrm_idsException(&$skip) { 69 | $skip[] = 'civicrm/remoteform'; 70 | } 71 | 72 | /** 73 | * Get displayable code. 74 | * 75 | * Return the code that should be displayed so the user can copy and paste it. 76 | * 77 | */ 78 | function remoteform_get_displayable_code($id, $entity = 'Profile') { 79 | $query = NULL; 80 | $absolute = TRUE; 81 | $fragment = NULL; 82 | $frontend = TRUE; 83 | $htmlize = TRUE; 84 | $js_url = Civi::resources()->getUrl('net.ourpowerbase.remoteform', 'remoteform.js'); 85 | $css_url = Civi::resources()->getUrl('net.ourpowerbase.remoteform', 'remoteform.css'); 86 | $spin_css_url = Civi::resources()->getUrl('net.ourpowerbase.remoteform', 'spin.css'); 87 | $post_url = CRM_Utils_System::url('civicrm/remoteform', $query, $absolute, $fragment, $htmlize, $frontend); 88 | $base_url = parse_url(CIVICRM_UF_BASEURL, PHP_URL_HOST); 89 | 90 | $extra_js_urls = []; 91 | $extra_js_params = NULL; 92 | if ($entity == 'ContributionPage') { 93 | $type = strtolower(remoteform_get_payment_processor_type($id)); 94 | 95 | $extra_js_urls_func = 'remoteform' . $type . '_extra_js_urls'; 96 | $extra_js_params_func = 'remoteform' . $type . '_extra_js_params'; 97 | 98 | if (function_exists($extra_js_urls_func)) { 99 | $extra_js_urls += $extra_js_urls_func($id); 100 | } 101 | if (function_exists($extra_js_params_func)) { 102 | $extra_js_params = $extra_js_params_func($id); 103 | } 104 | } 105 | 106 | CRM_Utils_Hook::singleton()->invoke( 107 | ['id', 'extra_js_urls'], 108 | $id, 109 | $extra_js_urls, 110 | CRM_Utils_Hook::$_nullObject, CRM_Utils_Hook::$_nullObject, CRM_Utils_Hook::$_nullObject, CRM_Utils_Hook::$_nullObject, 111 | 'civicrm_remoteform_extraJsUrls'); 112 | CRM_Utils_Hook::singleton()->invoke( 113 | ['id', 'extra_js_params'], 114 | $id, 115 | $extra_js_params, 116 | CRM_Utils_Hook::$_nullObject, CRM_Utils_Hook::$_nullObject, CRM_Utils_Hook::$_nullObject, CRM_Utils_Hook::$_nullObject, 117 | 'civicrm_remoteform_extraJsParams'); 118 | 119 | $extra_js_url_tags = ''; 120 | foreach ($extra_js_urls as $extra_js_url) { 121 | $extra_js_url_tags .= htmlentities('') . '
'; 122 | } 123 | 124 | return 125 | htmlentities('') . '
'. 128 | htmlentities('') . '
' . 129 | htmlentities('') . '
' . 130 | htmlentities('
') . '
' . 131 | htmlentities('') . '
' . 132 | htmlentities('') . '
' . 133 | $extra_js_url_tags . 134 | htmlentities(''); 151 | } 152 | 153 | /** 154 | * Add field to enable remote forms for this entity. 155 | * 156 | */ 157 | function remoteform_add_enable_field($form, $name, $label, $code) { 158 | $templatePath = realpath(dirname(__FILE__)."/templates"); 159 | 160 | // Add the field element in the form 161 | $form->add('checkbox', 'remoteform_' . $name . '_enable', $label); 162 | 163 | // dynamically insert a template block in the page 164 | CRM_Core_Region::instance('page-body')->add(array( 165 | 'template' => "{$templatePath}/{$name}.tpl" 166 | )); 167 | $id = intval($form->getVar('_id')); 168 | 169 | $form->assign('remoteform_code', $code); 170 | $enabled = civicrm_api3('Setting', 'getvalue', array('name' => 'remoteform_enabled_' . $name)); 171 | if (is_null($enabled)) { 172 | $enabled = array(); 173 | } 174 | $defaults['remoteform_' . $name . '_enable'] = 0; 175 | if (in_array($id, $enabled)) { 176 | $defaults['remoteform_' . $name . '_enable'] = 1; 177 | } 178 | $form->setDefaults($defaults); 179 | } 180 | 181 | /** 182 | * Implements hook_civicrm_buildForm(). 183 | * 184 | * Add remoteform options to key forms in CiviCRM Core. 185 | * 186 | * @param string $formName 187 | * @param CRM_Core_Form $form 188 | */ 189 | function remoteform_civicrm_buildForm($formName, &$form) { 190 | if ($formName == 'CRM_Contribute_Form_ContributionPage_Settings') { 191 | // This form is called once as part of the regular page load and again via an ajax snippet. 192 | // We only want the new fields loaded once - so limit ourselves to the ajax snippet load. 193 | if (CRM_Utils_Request::retrieve('snippet', 'String', $form) == 'json') { 194 | // Sanity check: ensure the form only uses on payment processor. 195 | $id = intval($form->getVar('_id')); 196 | $results = \Civi\Api4\ContributionPage::get() 197 | ->addSelect('payment_processor') 198 | ->addWhere('id', '=', $id) 199 | ->execute()->first(); 200 | if (count($results['payment_processor'] ?? []) > 1 ) { 201 | $code = htmlentities(''); 202 | $message = E::ts('Please change to just one Payment Processor before enalbing Remote Form.'); 203 | } 204 | else { 205 | $code = remoteform_get_displayable_code($id, 'ContributionPage'); 206 | $message = E::ts('Allow remote submissions to this contribution page.'); 207 | } 208 | remoteform_add_enable_field($form, 'contribution_page', $message, $code); 209 | } 210 | } 211 | else if ($formName == 'CRM_UF_Form_Group') { 212 | $id = intval($form->getVar('_id')); 213 | $code = remoteform_get_displayable_code($id, 'Profile'); 214 | remoteform_add_enable_field($form, 'profile', E::ts('Allow remote submissions to this profile.'), $code); 215 | } 216 | } 217 | 218 | /** 219 | * Save remoteform enabled settings. 220 | * 221 | */ 222 | function remoteform_save_enabled_settings($form, $name) { 223 | $vals = $form->_submitValues; 224 | $id = intval($form->getVar('_id')); 225 | $enable = array_key_exists('remoteform_' . $name . '_enable', $vals) ? TRUE : FALSE; 226 | 227 | // Handle Default setting. 228 | $enabled = civicrm_api3('Setting', 'getvalue', array('name' => 'remoteform_enabled_' . $name)); 229 | if (is_null($enabled)) { 230 | $enabled = array(); 231 | } 232 | if ($enable) { 233 | if (!in_array($id, $enabled)) { 234 | // Update 235 | $enabled[] = $id; 236 | civicrm_api3('Setting', 'create', array('remoteform_enabled_' . $name => $enabled)); 237 | } 238 | } 239 | else { 240 | if (in_array($id, $enabled)) { 241 | $key = array_search($id, $enabled); 242 | unset($enabled[$key]); 243 | civicrm_api3('Setting', 'create', array('remoteform_enabled_' . $name => $enabled)); 244 | } 245 | } 246 | } 247 | 248 | /** 249 | * Implements hook__civicrm_postProcess(). 250 | * 251 | * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postProcess/ 252 | */ 253 | function remoteform_civicrm_postProcess($formName, &$form) { 254 | if ($formName == 'CRM_UF_Form_Group') { 255 | remoteform_save_enabled_settings($form, 'profile'); 256 | 257 | } 258 | elseif ($formName == 'CRM_Contribute_Form_ContributionPage_Settings') { 259 | remoteform_save_enabled_settings($form, 'contribution_page'); 260 | 261 | } 262 | } 263 | 264 | /** 265 | * Get name of payment processor type for contribution id. 266 | * 267 | */ 268 | function remoteform_get_payment_processor_type($id) { 269 | $details = remoteform_get_contribution_page_details($id); 270 | $payment_processor_id = $details['payment_processor']; 271 | // We don't support multiple payment processors, but let's not crash when they're enabled. 272 | if (is_array($payment_processor_id)) { 273 | $payment_processor_id = $payment_processor_id[0]; 274 | } 275 | if ($payment_processor_id) { 276 | $sql = "SELECT ppt.name FROM civicrm_payment_processor_type ppt JOIN 277 | civicrm_payment_processor pp ON pp.payment_processor_type_id = ppt.id 278 | WHERE pp.id = %0"; 279 | $dao = CRM_Core_DAO::executeQuery($sql, array(0 => array($payment_processor_id, 'Integer'))); 280 | $dao->fetch(); 281 | if (isset($dao->name)) { 282 | return $dao->name; 283 | } 284 | } 285 | return NULL; 286 | } 287 | 288 | /** 289 | * Get contribution page details. 290 | * 291 | * Return details about the contribution page. 292 | */ 293 | function remoteform_get_contribution_page_details($id) { 294 | $return = array( 295 | 'title', 296 | 'intro_text', 297 | 'thankyou_text', 298 | 'is_active', 299 | 'start_date', 300 | 'currency', 301 | 'min_amount', 302 | 'payment_processor' 303 | ); 304 | $cp_params = array( 305 | 'id' => $id, 306 | 'return' => $return, 307 | ); 308 | $result = civicrm_api3('ContributionPage', 'get', $cp_params); 309 | return $result['values'][$id]; 310 | 311 | } 312 | -------------------------------------------------------------------------------- /CRM/Remoteform/Page/RemoteForm.php: -------------------------------------------------------------------------------- 1 | printCorsHeaders(); 10 | $data = json_decode(stripslashes(file_get_contents("php://input"))); 11 | if (empty($data)) { 12 | $this->exitError(E::ts("No data was received.")); 13 | } 14 | try { 15 | $data = $this->sanitizeInput($data); 16 | } 17 | catch (Exception $e) { 18 | $this->exitError($e->getMessage()); 19 | } 20 | 21 | try { 22 | // CRM_Core_Error::debug_var('data', $data); 23 | // Special exception to check for dupes. 24 | if (strtolower($data['entity']) == 'stripepaymentintent' && strtolower($data['action']) == 'processpublic') { 25 | // Special exceptioin - we need to run an api4 call, not an api3 call. 26 | $result = \Civi\Api4\StripePaymentintent::processPublic(TRUE) 27 | ->setPaymentMethodID($data['params']['payment_method_id']) 28 | ->setAmount(strval($data['params']['amount'])) 29 | ->setCurrency($data['params']['currency']) 30 | ->setPaymentProcessorID($data['params']['id']) 31 | ->setIntentID(NULL) 32 | ->setDescription($data['params']['description']) 33 | ->setCsrfToken($data['params']['csrfToken']) 34 | ->execute(); 35 | $this->exitSuccess($result); 36 | } 37 | else { 38 | if (strtolower($data['entity']) == 'profile' && strtolower($data['action']) == 'submit') { 39 | $checkPerms = FALSE; 40 | $excludedContactIds = []; 41 | // issue#13 Workaround because the deduping is case-sensitive 42 | if (!empty($data['params']['email-primary'])) { 43 | $data['params']['email-Primary'] = $data['params']['email-primary']; 44 | } 45 | $dupes = CRM_Contact_BAO_Contact::getDuplicateContacts($data['params'], 'Individual', 'Unsupervised', $excludedContactIds, $checkPerms); 46 | $num = count($dupes); 47 | if ($num > 0) { 48 | // We have 1 or more dupes. We better do something. 49 | // First, let's see what the policy is for this profile. 50 | // 0 means issue warning and do not update, 1 means update the dupe, 2 means create dupe. 51 | $is_update_dupe = civicrm_api3('UFGroup', 'getvalue', [ 'return' => "is_update_dupe", 'id' => $data['params']['profile_id'] ]); 52 | 53 | if ($is_update_dupe == '0') { 54 | throw new CiviCRM_API3_Exception(E::ts("You are already in the database! Congrats.")); 55 | } 56 | elseif ($is_update_dupe == 1) { 57 | if ($num == 1) { 58 | // Just one. Ok, this must be the same contact. Update our params 59 | // to include the dupe contact id and we should be good. 60 | $data['params']['contact_id'] = array_pop($dupes); 61 | } 62 | else { 63 | // More than one dupe. Now what? In keeping with what happens when 64 | // you fill out a profile, we simply pick off the first one. 65 | $data['params']['contact_id'] = array_shift($dupes); 66 | } 67 | } 68 | elseif ($is_update_dupe == 2) { 69 | // No op. We don't have to do anything to create the dupe. 70 | } 71 | else { 72 | // Error 73 | throw new CiviCRM_API3_Exception(E::ts("Your profile has a mis-configured setting for duplicate handling.")); 74 | } 75 | } 76 | } 77 | $result = civicrm_api3($data['entity'], $data['action'], $data['params'] ); 78 | // Special exception - The API profile submit function doesn't add 79 | // contacts to a group or send email notification, even if the profile 80 | // specifies that it should. 81 | // See: https://lab.civicrm.org/dev/core/issues/581 82 | if (strtolower($data['entity']) == 'profile' && strtolower($data['action']) == 'submit') { 83 | $uf_group_id = $data['params']['profile_id']; 84 | $contact_id = $result['id']; 85 | $this->profilePostSubmit($uf_group_id, $contact_id); 86 | } 87 | // More exceptions... we never return values on submit to avoid leaks. 88 | if (strtolower($data['action']) == 'submit') { 89 | $result['values'] = []; 90 | } 91 | $this->exitSuccess($result['values']); 92 | } 93 | } 94 | catch (Exception $e) { 95 | $this->exitError($e->getMessage()); 96 | } 97 | } 98 | 99 | function exitError($data) { 100 | CRM_Utils_JSON::output(civicrm_api3_create_error($data)); 101 | } 102 | 103 | function exitSuccess($data) { 104 | CRM_Utils_JSON::output(civicrm_api3_create_success($data)); 105 | } 106 | 107 | function printCorsHeaders() { 108 | // Allow from any origin 109 | if (isset($_SERVER['HTTP_ORIGIN'])) { 110 | // CRM_Core_Error::debug_var('_SERVER', $_SERVER); 111 | $urls = explode("\n", civicrm_api3('setting', 'getvalue', array('name' => 'remoteform_cors_urls'))); 112 | foreach($urls as $url) { 113 | // Who knows what kind of spaces and line return nonesense we may have. 114 | // This regex should kill all the Control Characters (see 115 | // https://en.wikipedia.org/wiki/Control_character 116 | $url = preg_replace('/[\x00-\x1F\x7F]/', '', trim($url)); 117 | if ($_SERVER['HTTP_ORIGIN'] == $url) { 118 | header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}"); 119 | header('Access-Control-Allow-Credentials: true'); 120 | header('Access-Control-Max-Age: 86400'); // cache for 1 day 121 | continue; 122 | } 123 | } 124 | } 125 | 126 | // Access-Control headers are received during OPTIONS requests 127 | if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { 128 | if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) { 129 | // may also be using PUT, PATCH, HEAD etc 130 | header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); 131 | } 132 | if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { 133 | header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}"); 134 | } 135 | CRM_Utils_System::civiExit(); 136 | } 137 | } 138 | 139 | /** 140 | * Take user input object and return a safe array. 141 | **/ 142 | function sanitizeInput($input) { 143 | // Ensure the user is not logged in. If we allowed logged in users 144 | // then we are at risk of a CSRF attack. 145 | if (CRM_Utils_System::isUserLoggedIn()) { 146 | throw new CiviCRM_API3_Exception(E::ts('You cannot use JSSubmit while logged into CiviCRM.')); 147 | } 148 | 149 | $entity = $input->entity; 150 | $input_params = get_object_vars($input->params); 151 | $session_id = $input_params['session_id'] ?? NULL; 152 | if ($session_id) { 153 | session_id($session_id); 154 | } 155 | $action = $input->action; 156 | if ($entity == 'Profile') { 157 | // Ensure this site allows access to profiles. 158 | if (!CRM_Core_Permission::check('profile create')) { 159 | throw new CiviCRM_API3_Exception(E::ts("You don't have permission to create contacts via profiles.")); 160 | } 161 | 162 | // Let's see if this particular profile is allowed. 163 | $input_params = get_object_vars($input->params); 164 | $id = intval($input_params['profile_id']); 165 | $enabled = civicrm_api3('Setting', 'getvalue', array('name' => 'remoteform_enabled_profile')); 166 | if (!in_array($id, $enabled)) { 167 | throw new CiviCRM_API3_Exception(E::ts("This profile is not configured to accept remote form submissions.")); 168 | } 169 | 170 | if ($action == 'getfields') { 171 | // Sanitize input parameters. 172 | $api_action = $input_params['api_action'] == 'submit' ? 'submit' : NULL; 173 | $get_options = $input_params['get_options'] == 'all' ? 'all' : NULL; 174 | $params = array( 175 | 'profile_id' => $id, 176 | 'api_action' => $api_action, 177 | 'get_options' => $get_options 178 | ); 179 | return array( 180 | 'entity' => 'Profile', 181 | 'action' => 'getfields', 182 | 'params' => $params 183 | ); 184 | } 185 | if ($action == 'submit') { 186 | // Avoid updates by ensuring no contact_id is specified. 187 | unset($input_params['contact_id']); 188 | return array( 189 | 'entity' => 'Profile', 190 | 'action' => 'submit', 191 | 'params' => $input_params 192 | ); 193 | } 194 | else { 195 | throw new CiviCRM_API3_Exception(E::ts("That action is not allowed.")); 196 | } 197 | } 198 | else if ($entity == 'RemoteFormContributionPage') { 199 | // Ensure this site allows access to contributions. 200 | if (!CRM_Core_Permission::check('make online contributions')) { 201 | throw new CiviCRM_API3_Exception(E::ts("You don't have permission to create contributions.")); 202 | } 203 | 204 | // Make sure this contribution page is configured to accept remote submissions. 205 | $id = intval($input_params['contribution_page_id']); 206 | $enabled = civicrm_api3('Setting', 'getvalue', array('name' => 'remoteform_enabled_contribution_page')); 207 | if (!in_array($id, $enabled)) { 208 | throw new CiviCRM_API3_Exception(E::ts("This contribution page is not configured to accept remote form submissions.")); 209 | } 210 | if ($action == 'getfields') { 211 | // Sanitize input parameters. 212 | $api_action = $input_params['api_action'] == 'submit' ? 'submit' : NULL; 213 | $get_options = $input_params['get_options'] == 'all' ? 'all' : NULL; 214 | $test_mode = $input_params['test_mode'] == '1' ? '1' : NULL; 215 | $params = array( 216 | 'contribution_page_id' => intval($input_params['contribution_page_id']), 217 | 'api_action' => $api_action, 218 | 'get_options' => $get_options, 219 | 'test_mode' => $test_mode 220 | ); 221 | 222 | return array( 223 | 'entity' => 'RemoteFormContributionPage', 224 | 'action' => 'getfields', 225 | 'params' => $params 226 | ); 227 | } 228 | if ($action == 'submit') { 229 | $input_params['id'] = $input_params['contribution_page_id']; 230 | if (array_key_exists('credit_card_exp_date', $input_params)) { 231 | $input_params['credit_card_exp_date'] = (Array)$input_params['credit_card_exp_date']; 232 | } 233 | return array( 234 | 'entity' => 'RemoteFormContributionPage', 235 | 'action' => 'submit', 236 | 'params' => $input_params 237 | ); 238 | } 239 | else { 240 | throw new CiviCRM_API3_Exception(E::ts("That action is not allowed.")); 241 | } 242 | } 243 | else if ($entity == 'RemoteForm') { 244 | if ($action == 'Stateprovincesforcountry') { 245 | $params['country_id'] = isset($input_params['country_id']) ? intval($input_params['country_id']) : NULL; 246 | return array( 247 | 'entity' => 'RemoteForm', 248 | 'action' => 'Stateprovincesforcountry', 249 | 'params' => $params, 250 | ); 251 | } 252 | if ($action == 'Countiesforstateprovince') { 253 | $params['state_province_id'] = isset($input_params['state_province_id']) ? intval($input_params['state_province_id']) : NULL; 254 | return array( 255 | 'entity' => 'RemoteForm', 256 | 'action' => 'Countiesforstateprovince', 257 | 'params' => $params, 258 | ); 259 | } 260 | if ($action == 'Countries') { 261 | return array( 262 | 'entity' => 'RemoteForm', 263 | 'action' => 'Countries', 264 | 'params' => array(), 265 | ); 266 | } 267 | else { 268 | throw new CiviCRM_API3_Exception(E::ts("That action is not allowed.")); 269 | } 270 | } 271 | else if ($entity == 'StripePaymentintent') { 272 | if ($action != 'processPublic') { 273 | throw new CiviCRM_API3_Exception(E::ts("That action is not allowed.")); 274 | } 275 | $params = array( 276 | 'payment_method_id' => $input_params['payment_method_id'], 277 | 'amount' => $input_params['amount'], 278 | 'id' => $input_params['payment_processor_id'], 279 | 'currency' => $input_params['currency'], 280 | 'csrfToken' => $input_params['csrfToken'], 281 | 'description' => $input_params['description'] 282 | ); 283 | 284 | return array( 285 | 'entity' => 'StripePaymentintent', 286 | 'action' => 'processPublic', 287 | 'params' => $params, 288 | ); 289 | } 290 | else { 291 | throw new CiviCRM_API3_Exception(E::ts("That entity is not allowed: $entity.")); 292 | } 293 | } 294 | 295 | function profilePostSubmit($uf_group_id, $contact_id) { 296 | // See: https://github.com/civicrm/civicrm-core/pull/13410/ 297 | // This following code executes what is included in the pull request. When 298 | // the pull request is merged into CiviCRM, we will need to detect whether 299 | // or not to run the following code based on the version of CiviCRM in 300 | // which the code is merged. 301 | 302 | // Get notify and add to group for this profile. 303 | $profile_actions_params = array( 304 | 'id' => $uf_group_id, 305 | 'return' => array('add_to_group_id', 'notify'), 306 | ); 307 | $profile_actions = civicrm_api3('UFGroup', 'getsingle', $profile_actions_params); 308 | if (isset($profile_actions['add_to_group_id'])) { 309 | $method = 'Web'; 310 | CRM_Contact_BAO_GroupContact::addContactsToGroup(array($contact_id), $profile_actions['add_to_group_id'], $method); 311 | } 312 | if (isset($profile_actions['notify'])) { 313 | $val = CRM_Core_BAO_UFGroup::checkFieldsEmptyValues($uf_group_id, $contact_id, NULL); 314 | CRM_Core_BAO_UFGroup::commonSendMail($contact_id, $val); 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Package: net.ourpowerbase.remoteform 2 | Copyright (C) 2018, Jamie McClelland 3 | Licensed under the GNU Affero Public License 3.0 (below). 4 | 5 | ------------------------------------------------------------------------------- 6 | 7 | GNU AFFERO GENERAL PUBLIC LICENSE 8 | Version 3, 19 November 2007 9 | 10 | Copyright (C) 2007 Free Software Foundation, Inc. 11 | Everyone is permitted to copy and distribute verbatim copies 12 | of this license document, but changing it is not allowed. 13 | 14 | Preamble 15 | 16 | The GNU Affero General Public License is a free, copyleft license for 17 | software and other kinds of works, specifically designed to ensure 18 | cooperation with the community in the case of network server software. 19 | 20 | The licenses for most software and other practical works are designed 21 | to take away your freedom to share and change the works. By contrast, 22 | our General Public Licenses are intended to guarantee your freedom to 23 | share and change all versions of a program--to make sure it remains free 24 | software for all its users. 25 | 26 | When we speak of free software, we are referring to freedom, not 27 | price. Our General Public Licenses are designed to make sure that you 28 | have the freedom to distribute copies of free software (and charge for 29 | them if you wish), that you receive source code or can get it if you 30 | want it, that you can change the software or use pieces of it in new 31 | free programs, and that you know you can do these things. 32 | 33 | Developers that use our General Public Licenses protect your rights 34 | with two steps: (1) assert copyright on the software, and (2) offer 35 | you this License which gives you legal permission to copy, distribute 36 | and/or modify the software. 37 | 38 | A secondary benefit of defending all users' freedom is that 39 | improvements made in alternate versions of the program, if they 40 | receive widespread use, become available for other developers to 41 | incorporate. Many developers of free software are heartened and 42 | encouraged by the resulting cooperation. However, in the case of 43 | software used on network servers, this result may fail to come about. 44 | The GNU General Public License permits making a modified version and 45 | letting the public access it on a server without ever releasing its 46 | source code to the public. 47 | 48 | The GNU Affero General Public License is designed specifically to 49 | ensure that, in such cases, the modified source code becomes available 50 | to the community. It requires the operator of a network server to 51 | provide the source code of the modified version running there to the 52 | users of that server. Therefore, public use of a modified version, on 53 | a publicly accessible server, gives the public access to the source 54 | code of the modified version. 55 | 56 | An older license, called the Affero General Public License and 57 | published by Affero, was designed to accomplish similar goals. This is 58 | a different license, not a version of the Affero GPL, but Affero has 59 | released a new version of the Affero GPL which permits relicensing under 60 | this license. 61 | 62 | The precise terms and conditions for copying, distribution and 63 | modification follow. 64 | 65 | TERMS AND CONDITIONS 66 | 67 | 0. Definitions. 68 | 69 | "This License" refers to version 3 of the GNU Affero General Public License. 70 | 71 | "Copyright" also means copyright-like laws that apply to other kinds of 72 | works, such as semiconductor masks. 73 | 74 | "The Program" refers to any copyrightable work licensed under this 75 | License. Each licensee is addressed as "you". "Licensees" and 76 | "recipients" may be individuals or organizations. 77 | 78 | To "modify" a work means to copy from or adapt all or part of the work 79 | in a fashion requiring copyright permission, other than the making of an 80 | exact copy. The resulting work is called a "modified version" of the 81 | earlier work or a work "based on" the earlier work. 82 | 83 | A "covered work" means either the unmodified Program or a work based 84 | on the Program. 85 | 86 | To "propagate" a work means to do anything with it that, without 87 | permission, would make you directly or secondarily liable for 88 | infringement under applicable copyright law, except executing it on a 89 | computer or modifying a private copy. Propagation includes copying, 90 | distribution (with or without modification), making available to the 91 | public, and in some countries other activities as well. 92 | 93 | To "convey" a work means any kind of propagation that enables other 94 | parties to make or receive copies. Mere interaction with a user through 95 | a computer network, with no transfer of a copy, is not conveying. 96 | 97 | An interactive user interface displays "Appropriate Legal Notices" 98 | to the extent that it includes a convenient and prominently visible 99 | feature that (1) displays an appropriate copyright notice, and (2) 100 | tells the user that there is no warranty for the work (except to the 101 | extent that warranties are provided), that licensees may convey the 102 | work under this License, and how to view a copy of this License. If 103 | the interface presents a list of user commands or options, such as a 104 | menu, a prominent item in the list meets this criterion. 105 | 106 | 1. Source Code. 107 | 108 | The "source code" for a work means the preferred form of the work 109 | for making modifications to it. "Object code" means any non-source 110 | form of a work. 111 | 112 | A "Standard Interface" means an interface that either is an official 113 | standard defined by a recognized standards body, or, in the case of 114 | interfaces specified for a particular programming language, one that 115 | is widely used among developers working in that language. 116 | 117 | The "System Libraries" of an executable work include anything, other 118 | than the work as a whole, that (a) is included in the normal form of 119 | packaging a Major Component, but which is not part of that Major 120 | Component, and (b) serves only to enable use of the work with that 121 | Major Component, or to implement a Standard Interface for which an 122 | implementation is available to the public in source code form. A 123 | "Major Component", in this context, means a major essential component 124 | (kernel, window system, and so on) of the specific operating system 125 | (if any) on which the executable work runs, or a compiler used to 126 | produce the work, or an object code interpreter used to run it. 127 | 128 | The "Corresponding Source" for a work in object code form means all 129 | the source code needed to generate, install, and (for an executable 130 | work) run the object code and to modify the work, including scripts to 131 | control those activities. However, it does not include the work's 132 | System Libraries, or general-purpose tools or generally available free 133 | programs which are used unmodified in performing those activities but 134 | which are not part of the work. For example, Corresponding Source 135 | includes interface definition files associated with source files for 136 | the work, and the source code for shared libraries and dynamically 137 | linked subprograms that the work is specifically designed to require, 138 | such as by intimate data communication or control flow between those 139 | subprograms and other parts of the work. 140 | 141 | The Corresponding Source need not include anything that users 142 | can regenerate automatically from other parts of the Corresponding 143 | Source. 144 | 145 | The Corresponding Source for a work in source code form is that 146 | same work. 147 | 148 | 2. Basic Permissions. 149 | 150 | All rights granted under this License are granted for the term of 151 | copyright on the Program, and are irrevocable provided the stated 152 | conditions are met. This License explicitly affirms your unlimited 153 | permission to run the unmodified Program. The output from running a 154 | covered work is covered by this License only if the output, given its 155 | content, constitutes a covered work. This License acknowledges your 156 | rights of fair use or other equivalent, as provided by copyright law. 157 | 158 | You may make, run and propagate covered works that you do not 159 | convey, without conditions so long as your license otherwise remains 160 | in force. You may convey covered works to others for the sole purpose 161 | of having them make modifications exclusively for you, or provide you 162 | with facilities for running those works, provided that you comply with 163 | the terms of this License in conveying all material for which you do 164 | not control copyright. Those thus making or running the covered works 165 | for you must do so exclusively on your behalf, under your direction 166 | and control, on terms that prohibit them from making any copies of 167 | your copyrighted material outside their relationship with you. 168 | 169 | Conveying under any other circumstances is permitted solely under 170 | the conditions stated below. Sublicensing is not allowed; section 10 171 | makes it unnecessary. 172 | 173 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 174 | 175 | No covered work shall be deemed part of an effective technological 176 | measure under any applicable law fulfilling obligations under article 177 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 178 | similar laws prohibiting or restricting circumvention of such 179 | measures. 180 | 181 | When you convey a covered work, you waive any legal power to forbid 182 | circumvention of technological measures to the extent such circumvention 183 | is effected by exercising rights under this License with respect to 184 | the covered work, and you disclaim any intention to limit operation or 185 | modification of the work as a means of enforcing, against the work's 186 | users, your or third parties' legal rights to forbid circumvention of 187 | technological measures. 188 | 189 | 4. Conveying Verbatim Copies. 190 | 191 | You may convey verbatim copies of the Program's source code as you 192 | receive it, in any medium, provided that you conspicuously and 193 | appropriately publish on each copy an appropriate copyright notice; 194 | keep intact all notices stating that this License and any 195 | non-permissive terms added in accord with section 7 apply to the code; 196 | keep intact all notices of the absence of any warranty; and give all 197 | recipients a copy of this License along with the Program. 198 | 199 | You may charge any price or no price for each copy that you convey, 200 | and you may offer support or warranty protection for a fee. 201 | 202 | 5. Conveying Modified Source Versions. 203 | 204 | You may convey a work based on the Program, or the modifications to 205 | produce it from the Program, in the form of source code under the 206 | terms of section 4, provided that you also meet all of these conditions: 207 | 208 | a) The work must carry prominent notices stating that you modified 209 | it, and giving a relevant date. 210 | 211 | b) The work must carry prominent notices stating that it is 212 | released under this License and any conditions added under section 213 | 7. This requirement modifies the requirement in section 4 to 214 | "keep intact all notices". 215 | 216 | c) You must license the entire work, as a whole, under this 217 | License to anyone who comes into possession of a copy. This 218 | License will therefore apply, along with any applicable section 7 219 | additional terms, to the whole of the work, and all its parts, 220 | regardless of how they are packaged. This License gives no 221 | permission to license the work in any other way, but it does not 222 | invalidate such permission if you have separately received it. 223 | 224 | d) If the work has interactive user interfaces, each must display 225 | Appropriate Legal Notices; however, if the Program has interactive 226 | interfaces that do not display Appropriate Legal Notices, your 227 | work need not make them do so. 228 | 229 | A compilation of a covered work with other separate and independent 230 | works, which are not by their nature extensions of the covered work, 231 | and which are not combined with it such as to form a larger program, 232 | in or on a volume of a storage or distribution medium, is called an 233 | "aggregate" if the compilation and its resulting copyright are not 234 | used to limit the access or legal rights of the compilation's users 235 | beyond what the individual works permit. Inclusion of a covered work 236 | in an aggregate does not cause this License to apply to the other 237 | parts of the aggregate. 238 | 239 | 6. Conveying Non-Source Forms. 240 | 241 | You may convey a covered work in object code form under the terms 242 | of sections 4 and 5, provided that you also convey the 243 | machine-readable Corresponding Source under the terms of this License, 244 | in one of these ways: 245 | 246 | a) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by the 248 | Corresponding Source fixed on a durable physical medium 249 | customarily used for software interchange. 250 | 251 | b) Convey the object code in, or embodied in, a physical product 252 | (including a physical distribution medium), accompanied by a 253 | written offer, valid for at least three years and valid for as 254 | long as you offer spare parts or customer support for that product 255 | model, to give anyone who possesses the object code either (1) a 256 | copy of the Corresponding Source for all the software in the 257 | product that is covered by this License, on a durable physical 258 | medium customarily used for software interchange, for a price no 259 | more than your reasonable cost of physically performing this 260 | conveying of source, or (2) access to copy the 261 | Corresponding Source from a network server at no charge. 262 | 263 | c) Convey individual copies of the object code with a copy of the 264 | written offer to provide the Corresponding Source. This 265 | alternative is allowed only occasionally and noncommercially, and 266 | only if you received the object code with such an offer, in accord 267 | with subsection 6b. 268 | 269 | d) Convey the object code by offering access from a designated 270 | place (gratis or for a charge), and offer equivalent access to the 271 | Corresponding Source in the same way through the same place at no 272 | further charge. You need not require recipients to copy the 273 | Corresponding Source along with the object code. If the place to 274 | copy the object code is a network server, the Corresponding Source 275 | may be on a different server (operated by you or a third party) 276 | that supports equivalent copying facilities, provided you maintain 277 | clear directions next to the object code saying where to find the 278 | Corresponding Source. Regardless of what server hosts the 279 | Corresponding Source, you remain obligated to ensure that it is 280 | available for as long as needed to satisfy these requirements. 281 | 282 | e) Convey the object code using peer-to-peer transmission, provided 283 | you inform other peers where the object code and Corresponding 284 | Source of the work are being offered to the general public at no 285 | charge under subsection 6d. 286 | 287 | A separable portion of the object code, whose source code is excluded 288 | from the Corresponding Source as a System Library, need not be 289 | included in conveying the object code work. 290 | 291 | A "User Product" is either (1) a "consumer product", which means any 292 | tangible personal property which is normally used for personal, family, 293 | or household purposes, or (2) anything designed or sold for incorporation 294 | into a dwelling. In determining whether a product is a consumer product, 295 | doubtful cases shall be resolved in favor of coverage. For a particular 296 | product received by a particular user, "normally used" refers to a 297 | typical or common use of that class of product, regardless of the status 298 | of the particular user or of the way in which the particular user 299 | actually uses, or expects or is expected to use, the product. A product 300 | is a consumer product regardless of whether the product has substantial 301 | commercial, industrial or non-consumer uses, unless such uses represent 302 | the only significant mode of use of the product. 303 | 304 | "Installation Information" for a User Product means any methods, 305 | procedures, authorization keys, or other information required to install 306 | and execute modified versions of a covered work in that User Product from 307 | a modified version of its Corresponding Source. The information must 308 | suffice to ensure that the continued functioning of the modified object 309 | code is in no case prevented or interfered with solely because 310 | modification has been made. 311 | 312 | If you convey an object code work under this section in, or with, or 313 | specifically for use in, a User Product, and the conveying occurs as 314 | part of a transaction in which the right of possession and use of the 315 | User Product is transferred to the recipient in perpetuity or for a 316 | fixed term (regardless of how the transaction is characterized), the 317 | Corresponding Source conveyed under this section must be accompanied 318 | by the Installation Information. But this requirement does not apply 319 | if neither you nor any third party retains the ability to install 320 | modified object code on the User Product (for example, the work has 321 | been installed in ROM). 322 | 323 | The requirement to provide Installation Information does not include a 324 | requirement to continue to provide support service, warranty, or updates 325 | for a work that has been modified or installed by the recipient, or for 326 | the User Product in which it has been modified or installed. Access to a 327 | network may be denied when the modification itself materially and 328 | adversely affects the operation of the network or violates the rules and 329 | protocols for communication across the network. 330 | 331 | Corresponding Source conveyed, and Installation Information provided, 332 | in accord with this section must be in a format that is publicly 333 | documented (and with an implementation available to the public in 334 | source code form), and must require no special password or key for 335 | unpacking, reading or copying. 336 | 337 | 7. Additional Terms. 338 | 339 | "Additional permissions" are terms that supplement the terms of this 340 | License by making exceptions from one or more of its conditions. 341 | Additional permissions that are applicable to the entire Program shall 342 | be treated as though they were included in this License, to the extent 343 | that they are valid under applicable law. If additional permissions 344 | apply only to part of the Program, that part may be used separately 345 | under those permissions, but the entire Program remains governed by 346 | this License without regard to the additional permissions. 347 | 348 | When you convey a copy of a covered work, you may at your option 349 | remove any additional permissions from that copy, or from any part of 350 | it. (Additional permissions may be written to require their own 351 | removal in certain cases when you modify the work.) You may place 352 | additional permissions on material, added by you to a covered work, 353 | for which you have or can give appropriate copyright permission. 354 | 355 | Notwithstanding any other provision of this License, for material you 356 | add to a covered work, you may (if authorized by the copyright holders of 357 | that material) supplement the terms of this License with terms: 358 | 359 | a) Disclaiming warranty or limiting liability differently from the 360 | terms of sections 15 and 16 of this License; or 361 | 362 | b) Requiring preservation of specified reasonable legal notices or 363 | author attributions in that material or in the Appropriate Legal 364 | Notices displayed by works containing it; or 365 | 366 | c) Prohibiting misrepresentation of the origin of that material, or 367 | requiring that modified versions of such material be marked in 368 | reasonable ways as different from the original version; or 369 | 370 | d) Limiting the use for publicity purposes of names of licensors or 371 | authors of the material; or 372 | 373 | e) Declining to grant rights under trademark law for use of some 374 | trade names, trademarks, or service marks; or 375 | 376 | f) Requiring indemnification of licensors and authors of that 377 | material by anyone who conveys the material (or modified versions of 378 | it) with contractual assumptions of liability to the recipient, for 379 | any liability that these contractual assumptions directly impose on 380 | those licensors and authors. 381 | 382 | All other non-permissive additional terms are considered "further 383 | restrictions" within the meaning of section 10. If the Program as you 384 | received it, or any part of it, contains a notice stating that it is 385 | governed by this License along with a term that is a further 386 | restriction, you may remove that term. If a license document contains 387 | a further restriction but permits relicensing or conveying under this 388 | License, you may add to a covered work material governed by the terms 389 | of that license document, provided that the further restriction does 390 | not survive such relicensing or conveying. 391 | 392 | If you add terms to a covered work in accord with this section, you 393 | must place, in the relevant source files, a statement of the 394 | additional terms that apply to those files, or a notice indicating 395 | where to find the applicable terms. 396 | 397 | Additional terms, permissive or non-permissive, may be stated in the 398 | form of a separately written license, or stated as exceptions; 399 | the above requirements apply either way. 400 | 401 | 8. Termination. 402 | 403 | You may not propagate or modify a covered work except as expressly 404 | provided under this License. Any attempt otherwise to propagate or 405 | modify it is void, and will automatically terminate your rights under 406 | this License (including any patent licenses granted under the third 407 | paragraph of section 11). 408 | 409 | However, if you cease all violation of this License, then your 410 | license from a particular copyright holder is reinstated (a) 411 | provisionally, unless and until the copyright holder explicitly and 412 | finally terminates your license, and (b) permanently, if the copyright 413 | holder fails to notify you of the violation by some reasonable means 414 | prior to 60 days after the cessation. 415 | 416 | Moreover, your license from a particular copyright holder is 417 | reinstated permanently if the copyright holder notifies you of the 418 | violation by some reasonable means, this is the first time you have 419 | received notice of violation of this License (for any work) from that 420 | copyright holder, and you cure the violation prior to 30 days after 421 | your receipt of the notice. 422 | 423 | Termination of your rights under this section does not terminate the 424 | licenses of parties who have received copies or rights from you under 425 | this License. If your rights have been terminated and not permanently 426 | reinstated, you do not qualify to receive new licenses for the same 427 | material under section 10. 428 | 429 | 9. Acceptance Not Required for Having Copies. 430 | 431 | You are not required to accept this License in order to receive or 432 | run a copy of the Program. Ancillary propagation of a covered work 433 | occurring solely as a consequence of using peer-to-peer transmission 434 | to receive a copy likewise does not require acceptance. However, 435 | nothing other than this License grants you permission to propagate or 436 | modify any covered work. These actions infringe copyright if you do 437 | not accept this License. Therefore, by modifying or propagating a 438 | covered work, you indicate your acceptance of this License to do so. 439 | 440 | 10. Automatic Licensing of Downstream Recipients. 441 | 442 | Each time you convey a covered work, the recipient automatically 443 | receives a license from the original licensors, to run, modify and 444 | propagate that work, subject to this License. You are not responsible 445 | for enforcing compliance by third parties with this License. 446 | 447 | An "entity transaction" is a transaction transferring control of an 448 | organization, or substantially all assets of one, or subdividing an 449 | organization, or merging organizations. If propagation of a covered 450 | work results from an entity transaction, each party to that 451 | transaction who receives a copy of the work also receives whatever 452 | licenses to the work the party's predecessor in interest had or could 453 | give under the previous paragraph, plus a right to possession of the 454 | Corresponding Source of the work from the predecessor in interest, if 455 | the predecessor has it or can get it with reasonable efforts. 456 | 457 | You may not impose any further restrictions on the exercise of the 458 | rights granted or affirmed under this License. For example, you may 459 | not impose a license fee, royalty, or other charge for exercise of 460 | rights granted under this License, and you may not initiate litigation 461 | (including a cross-claim or counterclaim in a lawsuit) alleging that 462 | any patent claim is infringed by making, using, selling, offering for 463 | sale, or importing the Program or any portion of it. 464 | 465 | 11. Patents. 466 | 467 | A "contributor" is a copyright holder who authorizes use under this 468 | License of the Program or a work on which the Program is based. The 469 | work thus licensed is called the contributor's "contributor version". 470 | 471 | A contributor's "essential patent claims" are all patent claims 472 | owned or controlled by the contributor, whether already acquired or 473 | hereafter acquired, that would be infringed by some manner, permitted 474 | by this License, of making, using, or selling its contributor version, 475 | but do not include claims that would be infringed only as a 476 | consequence of further modification of the contributor version. For 477 | purposes of this definition, "control" includes the right to grant 478 | patent sublicenses in a manner consistent with the requirements of 479 | this License. 480 | 481 | Each contributor grants you a non-exclusive, worldwide, royalty-free 482 | patent license under the contributor's essential patent claims, to 483 | make, use, sell, offer for sale, import and otherwise run, modify and 484 | propagate the contents of its contributor version. 485 | 486 | In the following three paragraphs, a "patent license" is any express 487 | agreement or commitment, however denominated, not to enforce a patent 488 | (such as an express permission to practice a patent or covenant not to 489 | sue for patent infringement). To "grant" such a patent license to a 490 | party means to make such an agreement or commitment not to enforce a 491 | patent against the party. 492 | 493 | If you convey a covered work, knowingly relying on a patent license, 494 | and the Corresponding Source of the work is not available for anyone 495 | to copy, free of charge and under the terms of this License, through a 496 | publicly available network server or other readily accessible means, 497 | then you must either (1) cause the Corresponding Source to be so 498 | available, or (2) arrange to deprive yourself of the benefit of the 499 | patent license for this particular work, or (3) arrange, in a manner 500 | consistent with the requirements of this License, to extend the patent 501 | license to downstream recipients. "Knowingly relying" means you have 502 | actual knowledge that, but for the patent license, your conveying the 503 | covered work in a country, or your recipient's use of the covered work 504 | in a country, would infringe one or more identifiable patents in that 505 | country that you have reason to believe are valid. 506 | 507 | If, pursuant to or in connection with a single transaction or 508 | arrangement, you convey, or propagate by procuring conveyance of, a 509 | covered work, and grant a patent license to some of the parties 510 | receiving the covered work authorizing them to use, propagate, modify 511 | or convey a specific copy of the covered work, then the patent license 512 | you grant is automatically extended to all recipients of the covered 513 | work and works based on it. 514 | 515 | A patent license is "discriminatory" if it does not include within 516 | the scope of its coverage, prohibits the exercise of, or is 517 | conditioned on the non-exercise of one or more of the rights that are 518 | specifically granted under this License. You may not convey a covered 519 | work if you are a party to an arrangement with a third party that is 520 | in the business of distributing software, under which you make payment 521 | to the third party based on the extent of your activity of conveying 522 | the work, and under which the third party grants, to any of the 523 | parties who would receive the covered work from you, a discriminatory 524 | patent license (a) in connection with copies of the covered work 525 | conveyed by you (or copies made from those copies), or (b) primarily 526 | for and in connection with specific products or compilations that 527 | contain the covered work, unless you entered into that arrangement, 528 | or that patent license was granted, prior to 28 March 2007. 529 | 530 | Nothing in this License shall be construed as excluding or limiting 531 | any implied license or other defenses to infringement that may 532 | otherwise be available to you under applicable patent law. 533 | 534 | 12. No Surrender of Others' Freedom. 535 | 536 | If conditions are imposed on you (whether by court order, agreement or 537 | otherwise) that contradict the conditions of this License, they do not 538 | excuse you from the conditions of this License. If you cannot convey a 539 | covered work so as to satisfy simultaneously your obligations under this 540 | License and any other pertinent obligations, then as a consequence you may 541 | not convey it at all. For example, if you agree to terms that obligate you 542 | to collect a royalty for further conveying from those to whom you convey 543 | the Program, the only way you could satisfy both those terms and this 544 | License would be to refrain entirely from conveying the Program. 545 | 546 | 13. Remote Network Interaction; Use with the GNU General Public License. 547 | 548 | Notwithstanding any other provision of this License, if you modify the 549 | Program, your modified version must prominently offer all users 550 | interacting with it remotely through a computer network (if your version 551 | supports such interaction) an opportunity to receive the Corresponding 552 | Source of your version by providing access to the Corresponding Source 553 | from a network server at no charge, through some standard or customary 554 | means of facilitating copying of software. This Corresponding Source 555 | shall include the Corresponding Source for any work covered by version 3 556 | of the GNU General Public License that is incorporated pursuant to the 557 | following paragraph. 558 | 559 | Notwithstanding any other provision of this License, you have 560 | permission to link or combine any covered work with a work licensed 561 | under version 3 of the GNU General Public License into a single 562 | combined work, and to convey the resulting work. The terms of this 563 | License will continue to apply to the part which is the covered work, 564 | but the work with which it is combined will remain governed by version 565 | 3 of the GNU General Public License. 566 | 567 | 14. Revised Versions of this License. 568 | 569 | The Free Software Foundation may publish revised and/or new versions of 570 | the GNU Affero General Public License from time to time. Such new versions 571 | will be similar in spirit to the present version, but may differ in detail to 572 | address new problems or concerns. 573 | 574 | Each version is given a distinguishing version number. If the 575 | Program specifies that a certain numbered version of the GNU Affero General 576 | Public License "or any later version" applies to it, you have the 577 | option of following the terms and conditions either of that numbered 578 | version or of any later version published by the Free Software 579 | Foundation. If the Program does not specify a version number of the 580 | GNU Affero General Public License, you may choose any version ever published 581 | by the Free Software Foundation. 582 | 583 | If the Program specifies that a proxy can decide which future 584 | versions of the GNU Affero General Public License can be used, that proxy's 585 | public statement of acceptance of a version permanently authorizes you 586 | to choose that version for the Program. 587 | 588 | Later license versions may give you additional or different 589 | permissions. However, no additional obligations are imposed on any 590 | author or copyright holder as a result of your choosing to follow a 591 | later version. 592 | 593 | 15. Disclaimer of Warranty. 594 | 595 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 596 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 597 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 598 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 599 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 600 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 601 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 602 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 603 | 604 | 16. Limitation of Liability. 605 | 606 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 607 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 608 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 609 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 610 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 611 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 612 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 613 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 614 | SUCH DAMAGES. 615 | 616 | 17. Interpretation of Sections 15 and 16. 617 | 618 | If the disclaimer of warranty and limitation of liability provided 619 | above cannot be given local legal effect according to their terms, 620 | reviewing courts shall apply local law that most closely approximates 621 | an absolute waiver of all civil liability in connection with the 622 | Program, unless a warranty or assumption of liability accompanies a 623 | copy of the Program in return for a fee. 624 | 625 | END OF TERMS AND CONDITIONS 626 | 627 | How to Apply These Terms to Your New Programs 628 | 629 | If you develop a new program, and you want it to be of the greatest 630 | possible use to the public, the best way to achieve this is to make it 631 | free software which everyone can redistribute and change under these terms. 632 | 633 | To do so, attach the following notices to the program. It is safest 634 | to attach them to the start of each source file to most effectively 635 | state the exclusion of warranty; and each file should have at least 636 | the "copyright" line and a pointer to where the full notice is found. 637 | 638 | 639 | Copyright (C) 640 | 641 | This program is free software: you can redistribute it and/or modify 642 | it under the terms of the GNU Affero General Public License as published by 643 | the Free Software Foundation, either version 3 of the License, or 644 | (at your option) any later version. 645 | 646 | This program is distributed in the hope that it will be useful, 647 | but WITHOUT ANY WARRANTY; without even the implied warranty of 648 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 649 | GNU Affero General Public License for more details. 650 | 651 | You should have received a copy of the GNU Affero General Public License 652 | along with this program. If not, see . 653 | 654 | Also add information on how to contact you by electronic and paper mail. 655 | 656 | If your software can interact with users remotely through a computer 657 | network, you should also make sure that it provides a way for users to 658 | get its source. For example, if your program is a web application, its 659 | interface could display a "Source" link that leads users to an archive 660 | of the code. There are many ways you could offer source, and different 661 | solutions will be better for different programs; see section 13 for the 662 | specific requirements. 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU AGPL, see 667 | . 668 | -------------------------------------------------------------------------------- /remoteform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * # Remoteform 3 | * ## Introduction 4 | * 5 | * All example code uses YOURSITE.ORG in place of the domain name of your 6 | * actual CiviCRM installation. 7 | * 8 | * In addition, the examples are written from Drupal paths, but WordPress and 9 | * Joomla should work just as well by substituting the paths for ones 10 | * appropriate to your CMS. 11 | * 12 | * ## Overview 13 | * 14 | * When including a remoteform on a web site, there are three kinds of 15 | * resources to provide. 16 | * 17 | * ### CSS, Javascript and HTML 18 | * 19 | * Typically, you will start with: 20 | * 21 | * ``` 22 | * 23 | * ``` 24 | * 25 | * The CSS line is purely optional. The code generates HTML with Bootstrap 26 | * based classes, so if your web site uses the Bootstrap CSS framework you 27 | * should omit the CSS line entirely and it should integrate perfectly well. 28 | * 29 | * Alternatively, you can adjust your web site CSS based on the example css to 30 | * fully control the display. 31 | * 32 | * Or, if you use a different CSS framework, see below for how you can change 33 | * the CSS classes that are printed. 34 | * 35 | * Next up: 36 | * 37 | * ``` 38 | * 39 | * ``` 40 | * 41 | * This line is required, it pulls in the javascript that makes everything 42 | * work. The javascript has no dependencies and should not conflict with any 43 | * existing libraries in use by your site. 44 | * 45 | * And lastly: 46 | * 47 | * ``` 48 | *
49 | * ``` 50 | * 51 | * You have to provide a div that will enclose the form created. See below if 52 | * you want to use a div already present on your page but with a different id. 53 | * 54 | * ### The parameters 55 | * 56 | * You must pass a config option to the remoteForm function: 57 | * 58 | * ``` 59 | * var remoteFormConfig = { 60 | * url: "https://YOURSITE.ORG/civicrm/remoteform", 61 | * id: 1, 62 | * entity: "ContributionPage" 63 | * }; 64 | * ``` 65 | * 66 | * The minimum parameters are url, id and entity. See below for details on all 67 | * the available parameters. 68 | * 69 | * ### The function 70 | * 71 | * Finally, you have to call the function: 72 | * 73 | * ``` 74 | * remoteForm(remoteFormConfig); 75 | * ``` 76 | */ 77 | 78 | 79 | function remoteForm(config) { 80 | 81 | /** 82 | * ## Properties 83 | * 84 | * ### cfg 85 | * 86 | * cfg is a sanitized global configuration object based on config, which 87 | * is the object passed in by the user. All the parameters below can be 88 | * changed by adding or editing your ```var config``` line. For example, 89 | * if you read about cfg.parentElementId and decide you want to change the 90 | * parentElementId, you would pass the following to the remoteForm function: 91 | * 92 | * ``` 93 | * var config = { 94 | * url: "https://YOURSITE.ORG/civicrm/remoteform", 95 | * id: 1, 96 | * entity: "ContributionPage", 97 | * parentElementId: "my-parent-id" 98 | * }; 99 | * ``` 100 | */ 101 | var cfg = {}; 102 | 103 | /** 104 | * ### cfg.url 105 | * 106 | * The url of the CiviCRM installation we are posting to. Required. 107 | */ 108 | cfg.url = config.url || null; 109 | 110 | /** 111 | * ### cfg.id 112 | * 113 | * The id of the entity (profile id, contribution page id, etc.). Required. 114 | */ 115 | cfg.id = config.id|| null; 116 | if (!cfg.url || !cfg.id) { 117 | friendlyErr("Please include url and id in your configuration."); 118 | return false; 119 | } 120 | 121 | /** 122 | * ### cfg.parentElementId 123 | * 124 | * The id of the element to which the form will be appended. Default: remoteform. 125 | */ 126 | cfg.parentElementId = config.parentElementId || 'remoteForm'; 127 | 128 | /** 129 | * ### cfg.entity 130 | * 131 | * The CiviCRM entity we are creating (currently only Profile and ContributionPage are supported). 132 | * Default: Profile. 133 | */ 134 | cfg.entity = config.entity || 'Profile'; 135 | 136 | /** 137 | * ### cfg.paymentTestMode 138 | * 139 | * For ContributionPage entities only, indicates whether you should submit to the 140 | * test payment processor or the live payment processor. Default: false 141 | */ 142 | cfg.paymentTestMode = config.paymentTestMode || false; 143 | 144 | /** 145 | * ### cfg.autoInit 146 | * 147 | * How the form will be initialized - either true if the form will be 148 | * initialized on page load or false if a button will need to be clicked in 149 | * order to display the form. Default: true. 150 | */ 151 | cfg.autoInit = config.autoInit == false ? false : true; 152 | 153 | /** 154 | * ### cfg.initTxt 155 | * 156 | * If cfg.autoInit is false, the text displayed on the button to click 157 | * for the user to display the form. Default: Fill in the form. 158 | */ 159 | cfg.initTxt = config.initTxt || 'Fill in the form'; 160 | 161 | /** 162 | * ### cfg.submitTxt 163 | * 164 | * The text to display on the form's submit button. Default: Submit. 165 | */ 166 | cfg.submitTxt = config.submitTxt || 'Submit'; 167 | 168 | /** 169 | * ### cfg.cancelTxt 170 | * 171 | * The text to display on the form's cancel button. Default: Cancel. 172 | */ 173 | cfg.cancelTxt = config.cancelTxt || 'Cancel'; 174 | 175 | /** 176 | * ### cfg.successMsg 177 | * 178 | * The message displayed to the user upon successful submission of the 179 | * form. Default: Thank you! Your submission was received. 180 | */ 181 | cfg.successMsg = config.successMsg || 'Thank you! Your submission was received.'; 182 | 183 | /** 184 | * ### cfg.displayLabels 185 | * Whether or not the form labels should be displayed when placeholder 186 | * text could be used instead to save space. Default: false. 187 | */ 188 | cfg.displayLabels = config.displayLabels == true ? true : false; 189 | 190 | /** 191 | * ### cfg.createFieldDivFunc 192 | * 193 | * Custom function to override the function used to create html fields 194 | * from the field definitions. If you don't like the way your fields are 195 | * being turned into html, set this parameter to a funciton that you have 196 | * defined and you can completley control the creation of all fields. 197 | * See createFieldDiv for instructions on how to create your custom function. 198 | */ 199 | cfg.createFieldDivFunc = config.createFieldDivFunc || createFieldDiv; 200 | 201 | /** 202 | * ### cfg.customSubmitDataFunc 203 | * 204 | * Customize the post action if you want to use a token-based payment processor 205 | * and you need to send credit card details to a different server before sending 206 | * them to CivICRM.. 207 | * 208 | * If you define this function, it should accept the following arguments. 209 | * 210 | * - params - the fields submitted by the user, including the amount 211 | * field 212 | * - submitDataPost - after your function has done it's business, you 213 | * should call this function, passing in params (which can be modified 214 | * by your function, for example, to remove the credit card number), to 215 | * complete the process and send the info back to CiviCRM. 216 | * - customSubmitDataParams - any custom parameters passed by the user (see 217 | * below). You may need to the user to include an api key, etc. 218 | * 219 | * See remoteform.stripe.js for an example. 220 | */ 221 | cfg.customSubmitDataFunc = config.customSubmitDataFunc || null; 222 | 223 | /** 224 | * ### cfg.customInitFunc 225 | * 226 | * Trigger javascript code to run after the form is built. 227 | * 228 | * If you define this function, it should accept one argument. 229 | * 230 | * id: The html element id of the enclosing div. 231 | * 232 | * See remoteform.stripe.js for an example. 233 | */ 234 | cfg.customInitFunc = config.customInitFunc || null; 235 | 236 | /** 237 | * ### cfg.customSubmitDataParams 238 | * 239 | * An object containing any data that is specific to your customSubmitDataFunc, 240 | * such as an api key, etc. 241 | */ 242 | cfg.customSubmitDataParams = config.customSubmitDataParams || {}; 243 | if (!config.css) { 244 | config.css = {}; 245 | } 246 | 247 | /** 248 | * ### cfg.css 249 | * 250 | * Indicate classes that should be used on various parts of the form if you 251 | * want more control over look and feel. The defaults are designed to work 252 | * with bootstrap. If you are not using bootstrap, you may want to include 253 | * the remoteform.css file which tries to make things look nice with the 254 | * default classes. 255 | */ 256 | cfg.css = config.css || {}; 257 | 258 | /** 259 | * #### cfg.css.userSuccessMsg 260 | * 261 | * Default: alert alert-success 262 | */ 263 | cfg.css.userSuccessMsg = config.css.userSuccessMsg || 'alert alert-success'; 264 | 265 | /** 266 | * #### cfg.css.FailureMsg 267 | * 268 | * Default: alert alert-warning 269 | */ 270 | cfg.css.userFailureMsg = config.css.userFailureMsg || 'alert alert-warning'; 271 | 272 | /** 273 | * #### cfg.css.button 274 | * 275 | * Default: btn btn-info 276 | */ 277 | cfg.css.button = config.css.button || 'btn btn-info'; 278 | 279 | /** 280 | * #### cfg.css.form. 281 | * 282 | * Default: rf-form 283 | */ 284 | cfg.css.form = config.css.form || 'rf-form'; 285 | 286 | /** 287 | * #### cfg.css.inputDiv 288 | * 289 | * Default: form-group 290 | */ 291 | cfg.css.inputDiv = config.css.inputDiv || 'form-group'; 292 | 293 | /** 294 | * #### cfg.css.checkDiv 295 | * 296 | * Default: form-check 297 | */ 298 | cfg.css.checkDiv = config.css.checkDiv || 'form-check'; 299 | 300 | /** 301 | * #### cfg.css.input 302 | * 303 | * Default: form-control 304 | */ 305 | cfg.css.input = config.css.input || 'form-control'; 306 | 307 | /** 308 | * #### cfg.css.checkInput 309 | * 310 | * Default: form-check-input 311 | */ 312 | cfg.css.checkInput = config.css.checkInput || 'form-check-input'; 313 | 314 | /** 315 | * #### cfg.css.textarea 316 | * 317 | * Default: form-control 318 | */ 319 | cfg.css.textarea = config.css.textarea || 'form-control'; 320 | 321 | /** 322 | * #### cfg.css.label 323 | * 324 | * Default: rf-label 325 | */ 326 | cfg.css.label = config.css.label || 'rf-label'; 327 | 328 | /** 329 | * #### cfg.css.sr_only 330 | * 331 | * Default: sr-only 332 | */ 333 | cfg.css.sr_only = config.css.sr_only || 'sr-only'; 334 | 335 | /** 336 | * #### cfg.css.checkLabel 337 | * 338 | * Default: form-check-label 339 | */ 340 | cfg.css.checkLabel = config.css.checkLabel || 'form-check-label'; 341 | 342 | /** 343 | * #### cfg.css.select 344 | * 345 | * Default: custom-select 346 | */ 347 | cfg.css.select = config.css.select || 'custom-select'; 348 | 349 | /** 350 | * #### cfg.css.small 351 | * 352 | * Default: text-muted form-text 353 | */ 354 | cfg.css.small = config.css.small || 'text-muted form-text'; 355 | 356 | // Communicating with the user. 357 | function clearUserMsg() { 358 | userMsgDiv.innerHTML = ''; 359 | userMsgDiv.className = ''; 360 | } 361 | 362 | function userMsg(msg, type = 'error') { 363 | userMsgDiv.innerHTML = msg; 364 | if (type == 'error') { 365 | userMsgDiv.className = cfg.css.userFailureMsg; 366 | } 367 | else { 368 | userMsgDiv.className = cfg.css.userSuccessMsg; 369 | } 370 | } 371 | function adminMsg(msg) { 372 | console.log(msg); 373 | } 374 | function friendlyErr(err) { 375 | adminMsg(err); 376 | userMsg("Sorry, we encountered an error! See console log for more details."); 377 | } 378 | 379 | // Sanity checking 380 | if (cfg.entity != 'Profile' && cfg.entity != 'ContributionPage') { 381 | friendlyErr("Only Profile and ContributionPage entities is currently supported."); 382 | return false; 383 | } 384 | 385 | // Initialize our global entities. We should end up with a *parentDiv* that 386 | // contains a *userMsg* div (for giving feedback to the user), *form* 387 | // (containing the form the user will submit), a spinnner/overlay div to 388 | // provider user feedback when things take a while, and an *initButton* that 389 | // kicks everything off. These are all global variables. 390 | var parentDiv = document.getElementById(cfg.parentElementId); 391 | var form = document.createElement('form'); 392 | form.id = 'remoteForm-form-' + cfg.entity + cfg.id; 393 | form.className = cfg.css.form; 394 | 395 | var userMsgDiv = document.createElement('div'); 396 | parentDiv.appendChild(userMsgDiv); 397 | 398 | var spinnerFrameDiv = document.createElement('div'); 399 | spinnerFrameDiv.className = 'remoteForm-spinner-frame'; 400 | var spinnerDiv = document.createElement('div'); 401 | spinnerDiv.className = 'remoteForm-spinner'; 402 | parentDiv.appendChild(spinnerFrameDiv); 403 | parentDiv.appendChild(spinnerDiv); 404 | 405 | // Create button that has click event to kick things off. We need this 406 | // even if autoInit is true so that after submission we can re-submit. 407 | var initButton = document.createElement('button'); 408 | initButton.innerHTML = cfg.initTxt; 409 | initButton.className = cfg.css.button; 410 | initButton.addEventListener("click", function() { 411 | displayForm(); 412 | }); 413 | parentDiv.appendChild(initButton); 414 | 415 | // If the user wants to auto init the form, do so now. 416 | if (cfg.autoInit == 1) { 417 | displayForm(); 418 | } 419 | 420 | // Now we are done. Event handler code is below. 421 | 422 | // Make a request for a list of fields to display, then process the 423 | // response by passing it to buildForm. 424 | function displayForm() { 425 | spinnerFrameDiv.style.display = 'block'; 426 | spinnerDiv.style.display = 'block'; 427 | parentDiv.appendChild(form); 428 | 429 | // Clear any left over user messages. 430 | clearUserMsg(); 431 | 432 | var params; 433 | var submitEntity = cfg.entity; 434 | 435 | if (cfg.entity == 'Profile') { 436 | params = { 437 | profile_id: cfg.id, 438 | api_action: 'submit', 439 | get_options: 'all' 440 | }; 441 | } 442 | else if (cfg.entity == 'ContributionPage') { 443 | params = { 444 | contribution_page_id: cfg.id, 445 | api_action: 'submit', 446 | get_options: 'all' 447 | }; 448 | // Override the entity to use our own ContributionPage entity 449 | // because the built-in one doesn't handle our use case. 450 | submitEntity = 'RemoteFormContributionPage'; 451 | 452 | // Add testing mode if necessary. 453 | if (cfg.paymentTestMode) { 454 | userMsg("In testing mode."); 455 | params["test_mode"] = true; 456 | } 457 | } 458 | var args = { 459 | action: 'getfields', 460 | entity: submitEntity, 461 | params: params 462 | }; 463 | 464 | // Once we get a response, send the response to processGetFieldsResponse. 465 | post(args, processGetFieldsResponse); 466 | } 467 | 468 | // Validate the response we get, then pass the validated fields to the 469 | // buildForm function to build the fields. 470 | function processGetFieldsResponse(data) { 471 | if (data['is_error'] == 1) { 472 | friendlyErr(data['error_message']); 473 | return; 474 | } 475 | if (validateFields(data['values'])) { 476 | buildForm(data['values']); 477 | // Hide the init button. 478 | initButton.style.display = 'none'; 479 | if (cfg.customInitFunc) { 480 | cfg.customInitFunc(cfg); 481 | } 482 | } 483 | else { 484 | friendlyErr("Failed to validate fields. You may be trying to use an entity that is too complicated for me."); 485 | } 486 | spinnerFrameDiv.style.display = 'none'; 487 | spinnerDiv.style.display = 'none'; 488 | } 489 | 490 | // We don't support all entities - just a few and a limited set of 491 | // functionalities for the ones we do support. This function is 492 | // designed to stop if we can't handle something too complex. 493 | function validateFields(fields) { 494 | if (cfg.entity == 'ContributionPage') { 495 | // We can only handle one payment processor since we don't have 496 | // provide the user the choice of which to use. 497 | if (fields.control.payment_processor.length == 0) { 498 | adminMsg("Your contribution page does not have a payment processor selected."); 499 | return false; 500 | } 501 | // Make sure we get a single, numeric value for the payment processor 502 | // (if more than one is provided, we get an array) 503 | if (isNaN(parseFloat(fields.control.payment_processor)) || !isFinite(fields.control.payment_processor)) { 504 | adminMsg("Your contribution page has more than one payment processor selected. Please only check off one payment processor."); 505 | console.log(fields); 506 | return false; 507 | } 508 | } 509 | return true; 510 | } 511 | 512 | // When the user has filled in all their data and click submit, submitData 513 | // is invoked. If you want to first process a credit card, then you can 514 | // pass the configuration parameter customSubmitDataFunc to override. 515 | function submitData(fields) { 516 | var params = processSubmitData(fields); 517 | spinnerFrameDiv.style.display = 'block'; 518 | spinnerDiv.style.display = 'block'; 519 | 520 | console.log("Restting it to block"); 521 | if (cfg.customSubmitDataFunc) { 522 | cfg.customSubmitDataFunc(params, submitDataPost, cfg.customSubmitDataParams, post); 523 | } 524 | else { 525 | submitDataPost(params); 526 | } 527 | } 528 | 529 | function submitDataPost(params) { 530 | post(params, processSubmitDataResponse); 531 | } 532 | 533 | // Take what the user submitted, and parse it into a more easily usable 534 | // object. 535 | function processSubmitData(fields) { 536 | var params; 537 | if (cfg.entity == 'Profile') { 538 | params = { 539 | action: 'submit', 540 | entity: cfg.entity, 541 | params: { 542 | profile_id: cfg.id 543 | } 544 | }; 545 | } 546 | else if (cfg.entity == 'ContributionPage') { 547 | params = { 548 | action: 'submit', 549 | entity: 'RemoteFormContributionPage', 550 | params: { 551 | contribution_page_id: cfg.id 552 | } 553 | }; 554 | // We have to submit a total amount. This will be calculated when 555 | // we process the price set fields below. 556 | var amount = 0.00; 557 | var payment_processor = fields.control.payment_processor; 558 | 559 | // Check to see if it's a test 560 | if (cfg.paymentTestMode) { 561 | params["params"]["test_mode"] = true; 562 | } 563 | } 564 | for (var key in fields) { 565 | if (key.startsWith('placeholder_')) { 566 | continue; 567 | } 568 | if (key.startsWith('formatting_')) { 569 | continue; 570 | } 571 | if (fields.hasOwnProperty(key)) { 572 | var def = fields[key]; 573 | if (!def.entity) { 574 | continue; 575 | } 576 | 577 | var field_name = key; 578 | 579 | type = getType(def); 580 | 581 | // Pick a variable type - single value or multiple or dict? 582 | if (key == 'credit_card_exp_date_M') { 583 | // Credit card expiration date is a dict with month and year keys. 584 | var value = {}; 585 | } 586 | else if (type == 'checkbox') { 587 | // Checkboxes submit a list of values. 588 | var value = []; 589 | } 590 | else { 591 | // Everything else is a simple variable. 592 | var value = null; 593 | } 594 | 595 | // Obtain the value (varies depending on type and field). 596 | 597 | // Credit card expiration date has to be submitted 598 | // as a an array with M and Y elements. 599 | if (key == 'credit_card_exp_date_Y') { 600 | // Skip it, we'll pick it up on Month below. 601 | continue; 602 | } 603 | else if (key == 'credit_card_exp_date_M') { 604 | field_name = 'credit_card_exp_date'; 605 | var value = { 606 | 'M': document.getElementById('credit_card_exp_date_M').value, 607 | 'Y': document.getElementById('credit_card_exp_date_Y').value 608 | }; 609 | } 610 | else if (type == 'hidden') { 611 | value = def.default_value; 612 | } 613 | else if (type == 'checkbox' || type == 'radio') { 614 | var options = document.getElementsByName(key); 615 | for (var i = 0; i < options.length; i++) { 616 | if (options[i].checked) { 617 | // If this is a price set field, then we will need to calculate 618 | // the amount. Either it will be an 'Other_Amount' option, which 619 | // means we have to find the Other_Amount field to get the amount 620 | // or it will have the amount as a data-amount attribute. 621 | if (/price_[0-9]+/.test(field_name)) { 622 | if (options[i].hasAttribute('data-is-other-amount')) { 623 | // Get the total from the Other_Amount field. 624 | amount = parseFloat(document.getElementById('Other_Amount').value); 625 | // the data-is-other-amount attribute is set to the other amount price field. 626 | field_name = 'price_' + options[i].getAttribute('data-is-other-amount'); 627 | value = amount; 628 | } 629 | else if (options[i].hasAttribute('data-amount')) { 630 | amount = parseFloat(options[i].getAttribute('data-amount')); 631 | value = options[i].value; 632 | } 633 | } 634 | else if (type == 'checkbox') { 635 | value.push(options[i].value); 636 | } 637 | else { 638 | value = options[i].value; 639 | } 640 | } 641 | } 642 | } 643 | else { 644 | var value = document.getElementById(key).value; 645 | } 646 | 647 | params['params'][field_name] = value; 648 | } 649 | } 650 | if (amount) { 651 | params['params']['amount'] = amount; 652 | } 653 | if (payment_processor) { 654 | params['params']['payment_processor_id'] = payment_processor; 655 | } 656 | return params; 657 | } 658 | 659 | function processSubmitDataResponse(data) { 660 | if (data['is_error'] == 1) { 661 | userMsg(data['error_message']); 662 | return; 663 | } 664 | else { 665 | // Success! 666 | resetForm(cfg.successMsg); 667 | spinnerFrameDiv.style.display = 'none'; 668 | spinnerDiv.style.display = 'none'; 669 | 670 | } 671 | } 672 | 673 | function resetForm(msg) { 674 | initButton.style.display = 'inline'; 675 | // Remove all fields to prepare for a new submission. 676 | while (form.firstChild) { 677 | form.removeChild(form.firstChild); 678 | } 679 | if (form.parentElement) { 680 | form.parentElement.removeChild(form); 681 | } 682 | userMsg(msg, 'success'); 683 | } 684 | 685 | // Post data to the CiviCRM server. 686 | function post(params, onSuccess = console.log, onError = friendlyErr, url = cfg.url) { 687 | var request = new XMLHttpRequest(); 688 | request.open('POST', url, true); 689 | request.onreadystatechange = function() { 690 | if (request.readyState === 4) { 691 | if (request.status >= 200 && request.status < 400) { 692 | try { 693 | onSuccess(JSON.parse(request.responseText)); 694 | } 695 | catch (err) { 696 | onError(err); 697 | } 698 | } else { 699 | onError(new Error('Response returned with non-OK status')); 700 | console.log(url); 701 | console.log(params); 702 | console.log(request); 703 | } 704 | } 705 | }; 706 | //request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 707 | request.setRequestHeader("Content-type", "application/json"); 708 | //var data = encodeURIComponent(JSON.stringify(params)); 709 | var data = JSON.stringify(params); 710 | request.send(data); 711 | //request.send(params); 712 | } 713 | 714 | /** 715 | * ## Functions related to building html widgets. 716 | * 717 | */ 718 | 719 | // This function starts everything - returns the built form. 720 | function buildForm(fields) { 721 | for (var key in fields) { 722 | if (fields.hasOwnProperty(key)) { 723 | var def = fields[key]; 724 | if (!def.entity) { 725 | continue; 726 | } 727 | var field; 728 | var type = getType(def); 729 | var html = cfg.createFieldDivFunc(key, def, type, createField, wrapField); 730 | if (html) { 731 | form.appendChild(html); 732 | } 733 | } 734 | }; 735 | // Now add submit and cancel buttons. 736 | var submitButton = createSubmit(); 737 | submitButton.value = cfg.submitTxt; 738 | submitButton.className = cfg.css.button; 739 | 740 | var cancelButton = createSubmit(); 741 | cancelButton.value = cfg.cancelTxt; 742 | cancelButton.className = cfg.css.button; 743 | cancelButton.addEventListener('click', function() { 744 | resetForm("Action canceled"); 745 | }); 746 | 747 | var submitDiv = document.createElement('div'); 748 | submitDiv.className = cfg.css.inputDiv; 749 | submitDiv.id = 'remoteform-submit'; 750 | submitDiv.appendChild(submitButton); 751 | submitDiv.appendChild(cancelButton); 752 | form.appendChild(submitDiv); 753 | 754 | // Add a submit listener to the form rather than a click listener 755 | // to the button so we can take advantage of html5 validation which 756 | // is triggered on submission of a form. 757 | form.addEventListener('submit', function(event) { 758 | event.preventDefault(); 759 | event.stopImmediatePropagation(); 760 | submitData(fields); 761 | }); 762 | } 763 | 764 | /** 765 | * ### createFieldDiv 766 | * 767 | * ```createFieldDiv(key, def, type, createFieldFunc, wrapFieldFunc)``` 768 | * 769 | * Create a single field with associated label wrapped in a div. 770 | * 771 | * This function can be overridden using the createFieldDivFunc config 772 | * parameter. 773 | * 774 | * #### Parameters: 775 | * 776 | * - key - the unique field key 777 | * - type - the type of field to build 778 | * - createFieldFunc - the function to use to build the field, override 779 | * as needed. See createField for a model. 780 | * - wrapFieldFunc - the function to use to build the div around the field, 781 | * override as needed. See wrapField as a model. 782 | * 783 | * #### Returns: 784 | * 785 | * An HTML entity including both a field label and field. 786 | * 787 | * If you don't override this function, it will do the following: 788 | * 789 | * ``` 790 | * var field = createFieldFunc(key, def, type); 791 | * if (field === null) { 792 | * return null; 793 | * } 794 | * return wrapFieldFunc(key, def, field); 795 | * } 796 | * ``` 797 | * 798 | * By overriding the class, you can do things like create your own 799 | * createFieldFunc or wrapFieldFunc, then simply call createFieldDiv but pass 800 | * it your own function names instead of the default ones. Or you can pick 801 | * out a field type you want to cusotmize or even a field key and only change 802 | * the behavior for that one. 803 | * 804 | * Here's an example of overriding createFieldFunc to change the list of 805 | * groups displayed when a profile includes groups. 806 | * 807 | * ``` 808 | * function myCreateFieldDiv(key, def, type, createFieldFunc, wrapFieldFunc) { 809 | * if (key == 'group_id') { 810 | * def.options = { 811 | * 1: "Group one", 812 | * 2: "group two" 813 | * } 814 | * } 815 | * var field = createFieldFunc(key, def, type); 816 | * if (field === null) { 817 | * return null; 818 | * } 819 | * return wrapFieldFunc(key, def, field); 820 | * } 821 | * ``` 822 | * 823 | * Once you have defined this function, you could pass in the paramter: 824 | * 825 | * ``` 826 | * createFieldDivFunc: myCreateFieldDiv 827 | * ``` 828 | * 829 | * to your remoteFormConfig. 830 | * 831 | */ 832 | function createFieldDiv(key, def, type, createFieldFunc, wrapFieldFunc) { 833 | var field = createFieldFunc(key, def, type); 834 | if (field === null) { 835 | return null; 836 | } 837 | return wrapFieldFunc(key, def, field); 838 | } 839 | 840 | /** 841 | * ### getType 842 | * 843 | * ```getType(def)``` 844 | * 845 | * If you pass in a field definition provided by CiviCRM, this function 846 | * returns an html input type, working around some CiviCRM idiosyncracies. 847 | * 848 | * #### Parameters 849 | * - def - a CiviCRM provided field definition object. 850 | * 851 | * #### Returns 852 | * A string field type 853 | */ 854 | function getType(def) { 855 | var type; 856 | if (def.html && def.html.type) { 857 | type = def.html.type.toLowerCase(); 858 | } 859 | else if (def.html_type) { 860 | type = def.html_type.toLowerCase(); 861 | } 862 | if (!type) { 863 | return null; 864 | } 865 | if (type == 'text' && def.entity == 'email') { 866 | type = 'email'; 867 | } 868 | if (type == 'select date') { 869 | type = 'date'; 870 | } 871 | if (type == 'chainselect') { 872 | type = 'select'; 873 | } 874 | return type; 875 | } 876 | 877 | /** 878 | * ### createField 879 | * 880 | * ```createField(key, def, type)``` 881 | * 882 | * Return an HTML entity that renders the given field. 883 | * 884 | * #### Parameters 885 | * - key - The unique string id for the field 886 | * - def - The CiviCRM provided field definition object. 887 | * - type - The string type for the field. 888 | * 889 | * ### Returns 890 | * 891 | * HTML entity. 892 | */ 893 | function createField(key, def, type) { 894 | switch(type) { 895 | case 'select': 896 | return createSelect(key, def); 897 | break; 898 | case 'checkbox': 899 | case 'radio': 900 | return createCheckboxesOrRadios(key, def, type); 901 | case 'textarea': 902 | case 'richtexteditor': 903 | return createTextArea(key, def); 904 | case 'hidden': 905 | return null; 906 | default: 907 | // text, date, email, etc. 908 | return createTextInput(key, def, type); 909 | } 910 | } 911 | 912 | /** 913 | * ### wrapField 914 | * 915 | * ```wrapField(key, def, field)``` 916 | * 917 | * Return an HTML entity that includes both the given field and a label. 918 | * 919 | * #### Parameters 920 | * - key - The unique string id for the field 921 | * - def - The CiviCRM provided field definition object. 922 | * - field - An HTML entity with the field 923 | * 924 | * ### Returns 925 | * 926 | * HTML entity. 927 | */ 928 | 929 | function wrapField(key, def, field) { 930 | var div = document.createElement('div'); 931 | div.className = cfg.css.inputDiv; 932 | 933 | if (typeof def.html_type != 'undefined' && def.html_type == 'formatting') { 934 | var formatting = document.createElement('div'); 935 | formatting.innerHTML = def.help_pre; 936 | div.appendChild(formatting); 937 | return div; 938 | } 939 | 940 | if (def.help_pre) { 941 | var small = document.createElement('small'); 942 | small.className = cfg.css.small; 943 | small.innerHTML = def.help_pre; 944 | div.appendChild(small); 945 | } 946 | 947 | var label = document.createElement('label'); 948 | if (cfg.displayLabels == true) { 949 | label.className = cfg.css.label; 950 | } 951 | else { 952 | // sr_only will hide except for screen readers. 953 | label.className = cfg.css.sr_only; 954 | } 955 | label.htmlFor = key; 956 | label.innerHTML = def.title; 957 | div.appendChild(label); 958 | 959 | div.appendChild(field); 960 | if (def.help_post) { 961 | var small = document.createElement('small'); 962 | small.className = cfg.css.small; 963 | small.innerHTML = def.help_post; 964 | div.appendChild(small); 965 | } 966 | return div; 967 | } 968 | 969 | // Helper for creating fields. 970 | function createInput(key, def, inputType = null) { 971 | var field = document.createElement('input'); 972 | if (key) { 973 | field.id = key; 974 | } 975 | if (inputType) { 976 | field.type = inputType; 977 | } 978 | if (def["api.required"] == 1) { 979 | field.setAttribute('required', 'required'); 980 | } 981 | if (def.title) { 982 | field.placeholder = def.title; 983 | } 984 | field.className = cfg.css.input; 985 | if (def.default_value) { 986 | field.value = def.default_value; 987 | } 988 | return field; 989 | } 990 | 991 | function createTextInput(key, def, type) { 992 | return createInput(key, def, type); 993 | } 994 | 995 | function createSubmit() { 996 | var def = {}; 997 | var key = null; 998 | return createInput(key, def, 'submit'); 999 | } 1000 | 1001 | /** 1002 | * Check if this is an "other amount" price set 1003 | * 1004 | * Some price set options should only be displayed if the user has 1005 | * clicked the "other amount" option. Unfortunately, it's hard to 1006 | * tell if an option is an other amount option. With normal price sets 1007 | * the option has the name "Other_Amount" - however, if you have a 1008 | * contribution page and you are not using price sets, then it's called 1009 | * Contribution_Amount. 1010 | * 1011 | * This function return true if we think this is an other amount 1012 | * option or false otherwise. 1013 | */ 1014 | function isOtherAmountOption(option) { 1015 | if (option["name"] == 'Other_Amount') { 1016 | return true; 1017 | } 1018 | else if(option["name"] == 'Contribution_Amount') { 1019 | return true; 1020 | } 1021 | return false; 1022 | } 1023 | 1024 | /** 1025 | * Checkbox and Radio collections. 1026 | */ 1027 | function createCheckboxesOrRadios(key, def, type) { 1028 | // Creating enclosing div for the collection. 1029 | var collectionDiv = document.createElement('div'); 1030 | 1031 | // One label for the collection (we include a label even if 1032 | // cfg.displayLabels is false becaues there is no other way to show it. 1033 | var label = document.createElement('label'); 1034 | // Always create a label, but don't create two if we already display labels. 1035 | if (cfg.displayLabels !== true) { 1036 | label.className = cfg.css.label; 1037 | label.innerHTML = def.title; 1038 | collectionDiv.appendChild(label); 1039 | } 1040 | 1041 | // Another div to enclose just the options. 1042 | var optionsDiv = document.createElement('div'); 1043 | 1044 | var isPriceSet = false; 1045 | 1046 | // Keep track of whether or not this price set has an "other amount" 1047 | // option. If so, we have to display it when requested and hide it when 1048 | // not requested. This variable keeps track of whether one exists for 1049 | // this particular priceset. 1050 | var pricesetHasOtherAmountOption = false; 1051 | 1052 | // Treat price sets differently. 1053 | if (/price_[0-9]+/.test(key)) { 1054 | isPriceSet = true; 1055 | 1056 | // If there is an other_amount option, we need to know up front so 1057 | // we can add event listeners that will make a new other amount 1058 | // text box appear. 1059 | for (var optionId in def.options) { 1060 | if (def.options.hasOwnProperty(optionId)) { 1061 | if (isOtherAmountOption(def.options[optionId])) { 1062 | pricesetHasOtherAmountOption = true; 1063 | break; 1064 | } 1065 | } 1066 | } 1067 | } 1068 | 1069 | // Now iterate over options again to build out the html. 1070 | for (var optionId in def.options) { 1071 | if (def.options.hasOwnProperty(optionId)) { 1072 | // Another div to enclose this particular option. 1073 | var optionDiv = document.createElement('div'); 1074 | 1075 | // We use the same class for both radio and checkbox. 1076 | optionDiv.className = cfg.css.checkDiv; 1077 | 1078 | // Create the input. 1079 | var optionInput = document.createElement('input'); 1080 | optionInput.type = type; 1081 | 1082 | // We set an id so the label below can properly reference the right 1083 | // input. 1084 | optionInput.id = def.name + '-' + optionId; 1085 | optionInput.className = cfg.css.checkInput; 1086 | 1087 | // We use the name field to find the values when we submit. This 1088 | // value has to be unique (in case we have multiple pricesets). 1089 | optionInput.name = key; 1090 | 1091 | // Option display on the option type. 1092 | var optionDisplay = null; 1093 | if (isPriceSet) { 1094 | // Priceset options are a dict of values. 1095 | var optionObj = def.options[optionId]; 1096 | var prefix; 1097 | if (optionObj['currency'] == 'USD') { 1098 | prefix = '$'; 1099 | } 1100 | 1101 | // Price set options called "Other_Amount" are handled differently. 1102 | var optionDisplay; 1103 | 1104 | if (isOtherAmountOption(optionObj)) { 1105 | // Don't display "amount" (because with other_amount it is 1106 | // set to is the minimum amount). 1107 | optionDisplay = optionObj['label']; 1108 | 1109 | // Add an attribute so we know it is an Other_Amount field when 1110 | // we are calculating the total amount to submit and we know which 1111 | // price field it belongs to. 1112 | optionInput.setAttribute('data-is-other-amount', optionObj['price_field_id']); 1113 | 1114 | // If clicked, show other amount text box. 1115 | optionInput.addEventListener('click', function(e) { 1116 | // If Other_Amount is chosen, display box for user to enter 1117 | // the other amount. It should be inserted after the enclosing 1118 | // div of the other amount option. 1119 | var referenceNode = document.getElementById(optionInput.id).parentNode; 1120 | var otherAmountDef = { 1121 | 'api.required': 1, 1122 | title: 'Other Amount' 1123 | }; 1124 | 1125 | var otherAmountEl = cfg.createFieldDivFunc('Other_Amount', otherAmountDef, 'text', createField, wrapField); 1126 | referenceNode.parentNode.insertBefore(otherAmountEl, referenceNode.nextSibling); 1127 | }); 1128 | } 1129 | else { 1130 | optionDisplay = optionObj['label'] ? optionObj['label'] + ' - ' : ''; 1131 | optionDisplay += prefix + parseFloat(optionObj['amount']).toFixed(2); 1132 | optionInput.setAttribute('data-amount', optionObj['amount']); 1133 | if (pricesetHasOtherAmountOption) { 1134 | // This is not an other amount field, but since there is 1135 | // an other amount option, we have to hide the other amount 1136 | // text field if it is clicked on. 1137 | optionInput.addEventListener('click', function(e) { 1138 | // If we have not clicked the other amount option, then the other amount 1139 | // field may not even exist. 1140 | if (document.getElementById('Other_Amount')) { 1141 | document.getElementById('Other_Amount').style.display = 'none'; 1142 | } 1143 | }); 1144 | } 1145 | } 1146 | } 1147 | else { 1148 | optionDisplay = def.options[optionId]; 1149 | } 1150 | 1151 | optionInput.value = optionId; 1152 | if (def.default_value == optionId) { 1153 | optionInput.checked = true; 1154 | } 1155 | 1156 | // Create the label. 1157 | var optionLabel = document.createElement('label'); 1158 | optionLabel.htmlFor = optionInput.id; 1159 | 1160 | // We have both simple options (the label is the value, e.g. 1161 | // options = [ { key: label }, { key: label} ] and also 1162 | // complex options (used for price sets) which have more data: 1163 | // options = [ {key: { label: label, amount: amount, name: name}, etc. 1164 | 1165 | optionLabel.innerHTML = optionDisplay; 1166 | optionLabel.className = cfg.css.checkLabel; 1167 | 1168 | // Insert all our elements. 1169 | optionDiv.appendChild(optionInput); 1170 | optionDiv.appendChild(optionLabel); 1171 | optionsDiv.appendChild(optionDiv); 1172 | } 1173 | } 1174 | collectionDiv.appendChild(optionsDiv); 1175 | return collectionDiv; 1176 | } 1177 | 1178 | /** 1179 | * Populate a location drop down with the appropriate values. 1180 | * 1181 | * We dynamically populate the state/province, county and country 1182 | * drop down lists by querying CiviCRM for the appropriate values. 1183 | * 1184 | * In the case of state province, the right values will depend on the 1185 | * chosen country. In the case of county, the right values will depend 1186 | * on the chosen state. 1187 | **/ 1188 | function populateLocationOptions(loc, chosen = null, selectInput = null) { 1189 | // Try to find the right chosen field. 1190 | if (chosen === null) { 1191 | if (loc == 'state_province') { 1192 | var country_elems = document.getElementsByClassName('remoteform-country'); 1193 | if (country_elems[0]) { 1194 | // If there is more than one country field, we take the first. 1195 | chosen = country_elems[0].value; 1196 | } 1197 | } 1198 | } 1199 | 1200 | if (selectInput === null) { 1201 | // Find the selectInput element to populate. 1202 | var elementId = 'remoteform-' + loc; 1203 | var target_elems = document.getElementsByClassName(elementId); 1204 | if (target_elems[0]) { 1205 | // If there is more than one, we take the first. 1206 | selectInput = target_elems[0]; 1207 | } 1208 | else { 1209 | console.log("Could not find the target element."); 1210 | return; 1211 | } 1212 | } 1213 | 1214 | var action = null; 1215 | var key_field = null; 1216 | var params = {}; 1217 | var args = { 1218 | params: {} 1219 | } 1220 | 1221 | var label = null; 1222 | if (loc == 'state-province') { 1223 | action = 'Stateprovincesforcountry'; 1224 | key_field = 'country_id'; 1225 | args['params']['country_id'] = chosen; 1226 | label = 'State'; 1227 | } 1228 | if (loc == 'county') { 1229 | action = 'Countiesforstateprovince'; 1230 | key_field = 'county_id'; 1231 | args['params']['state_province_id'] = chosen; 1232 | label = 'County'; 1233 | } 1234 | else if (loc == 'country') { 1235 | action = 'Countries'; 1236 | label = 'Country'; 1237 | } 1238 | args['action'] = action; 1239 | args['entity'] = 'RemoteForm'; 1240 | 1241 | post(args, function(data) { 1242 | var optionEl; 1243 | // Purge existing options. 1244 | selectInput.innerHTML = ''; 1245 | if (cfg.displayLabels == false) { 1246 | // If we are not showing labels, then create an initial option with 1247 | // no value that displays the label in the drop down. 1248 | optionEl = document.createElement('option'); 1249 | optionEl.value = ''; 1250 | optionEl.innerHTML = '-- select ' + label + ' --'; 1251 | selectInput.appendChild(optionEl); 1252 | } 1253 | Object.keys(data['values']).forEach(function(key) { 1254 | value = data['values'][key]; 1255 | optionEl = document.createElement('option'); 1256 | optionEl.value = key; 1257 | optionEl.innerHTML = value; 1258 | selectInput.appendChild(optionEl); 1259 | }); 1260 | }); 1261 | } 1262 | 1263 | // Country, state, and county fields are related - given the country, 1264 | // we want to show the right states, given the state, we want to show 1265 | // the right counties. This function handles the logic of setting the 1266 | // proper callback functions and querying the civicrm database to 1267 | // get the correct option lists depending on other values on the form. 1268 | function handleLocationOptions(selectInput, def) { 1269 | var loc; 1270 | var chosen = null; 1271 | // Add special classes so we can be sure to find these elements later 1272 | // using getElementsByClass. 1273 | if (def.name == 'country_id') { 1274 | // We need to add a callback. 1275 | selectInput.addEventListener('change', function() { 1276 | populateLocationOptions('state-province', this.value); 1277 | }); 1278 | loc = 'country'; 1279 | } 1280 | else if (def.name == 'county_id') { 1281 | selectInput.className += ' remoteform-county'; 1282 | var chosen = document.querySelectorAll(".remoteform-state-province")[0].value; 1283 | if (!chosen) { 1284 | // FIXME - this is a hack. If no state is chosen, it's probably because 1285 | // the list of states have not yet been loaded. If this is the US... 1286 | // then I expect the first state to be Alabama, which will end up being 1287 | // select. So we'll load the Alabama counties instead of having no 1288 | // counties loaded. 1289 | chosen = "1000"; 1290 | } 1291 | loc = 'county'; 1292 | } 1293 | else if (def.name == 'state_province_id') { 1294 | // We need to add a callback. 1295 | selectInput.addEventListener('change', function() { 1296 | populateLocationOptions('county', this.value); 1297 | }); 1298 | selectInput.className += ' remoteform-state-province'; 1299 | loc = 'state-province'; 1300 | } 1301 | 1302 | populateLocationOptions(loc, chosen, selectInput); 1303 | } 1304 | 1305 | function createSelect(key, def) { 1306 | // Create the select element. 1307 | var selectInput = document.createElement('select'); 1308 | selectInput.id = key; 1309 | if (def["api.required"] == 1) { 1310 | selectInput.setAttribute('required', 'required'); 1311 | } 1312 | selectInput.className = cfg.css.select; 1313 | 1314 | if (def.name == 'country_id' || def.name == 'county_id' || def.name == 'state_province_id' ) { 1315 | handleLocationOptions(selectInput, def); 1316 | } 1317 | else { 1318 | if (cfg.displayLabels == false) { 1319 | // If we are not showing labels, then create an initial option with 1320 | // no value that displays the label in the drop down. 1321 | optionEl = document.createElement('option'); 1322 | optionEl.value = ''; 1323 | optionEl.innerHTML = '--' + def.title + '--'; 1324 | selectInput.appendChild(optionEl); 1325 | } 1326 | for (var option in def.options) { 1327 | if (def.options.hasOwnProperty(option)) { 1328 | var optionDef = def.options[option]; 1329 | optionEl = document.createElement('option'); 1330 | optionEl.value = option; 1331 | optionEl.innerHTML = def.options[option]; 1332 | selectInput.appendChild(optionEl); 1333 | } 1334 | } 1335 | } 1336 | return selectInput; 1337 | } 1338 | 1339 | function createTextArea(key, def) { 1340 | field = document.createElement('textarea'); 1341 | if (def["api.required"] == 1) { 1342 | field.setAttribute('required', 'required'); 1343 | } 1344 | if (def.title) { 1345 | field.placeholder = def.title; 1346 | } 1347 | field.className = cfg.css.input; 1348 | if (key) { 1349 | field.id = key; 1350 | } 1351 | if (def.default_value) { 1352 | field.innerHTML = def.default_value; 1353 | } 1354 | field.className = cfg.css.textarea; 1355 | return field; 1356 | } 1357 | 1358 | } 1359 | 1360 | 1361 | --------------------------------------------------------------------------------