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.
{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 |
10 |
11 |
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 | 
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 | 
33 |
34 | Third, copy and paste the provided javascript code to your remote web site and
35 | you are done.
36 |
37 | 
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 | 
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 | 
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 | 
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 | 
78 |
79 | Click the Console tab, and check for messages.
80 |
81 | 
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 |
--------------------------------------------------------------------------------