Commit | Line | Data |
---|---|---|
7f254ad8 AE |
1 | <?php |
2 | ||
3 | /** | |
4 | * @file | |
5 | * Front-end form validation and post-processing. | |
6 | */ | |
7 | ||
8 | module_load_include('inc', 'webform_civicrm', 'includes/wf_crm_webform_base'); | |
9 | ||
10 | class wf_crm_webform_postprocess extends wf_crm_webform_base { | |
11 | // Variables used during validation | |
12 | private $form; | |
13 | private $form_state; | |
14 | private $crmValues; | |
15 | private $rawValues; | |
16 | private $multiPageDataLoaded; | |
17 | private $billing_params = array(); | |
18 | private $totalContribution = 0; | |
19 | private $contributionIsIncomplete = FALSE; | |
20 | private $contributionIsPayLater = FALSE; | |
21 | ||
22 | // During validation this contains an array of known contact ids and the placeholder 0 for valid contacts | |
23 | // During submission processing this contains an array of known contact ids | |
24 | private $existing_contacts = array(); | |
25 | ||
26 | // Variables used during submission processing | |
27 | private $submission; | |
28 | private $update = array(); | |
29 | private $all_fields; | |
30 | private $all_sets; | |
31 | private $shared_address = array(); | |
32 | ||
33 | // Cache an instance of this object | |
34 | // @see self::singleton() | |
35 | static $singleton; | |
36 | ||
37 | const | |
38 | BILLING_MODE_LIVE = 1, | |
39 | BILLING_MODE_MIXED = 3; | |
40 | ||
41 | function __construct($node) { | |
42 | civicrm_initialize(); | |
43 | $this->node = $node; | |
44 | $this->settings = $node->webform_civicrm; | |
45 | $this->data = $this->settings['data']; | |
46 | $this->enabled = wf_crm_enabled_fields($node); | |
47 | $this->all_fields = wf_crm_get_fields(); | |
48 | $this->all_sets = wf_crm_get_fields('sets'); | |
49 | } | |
50 | ||
51 | /** | |
52 | * This is first called during form validation. We create an instance of this object and stash it in a static variable. | |
53 | * It is destroyed and rebuilt between each page submission, but after (successful) validation of the final page, | |
54 | * this cache allows the object instance to persist throughout (final) validate, preSave and postSave operations. | |
55 | * @param stdClass $node | |
56 | * @return wf_crm_webform_postprocess | |
57 | */ | |
58 | static function singleton($node) { | |
59 | if (!self::$singleton) { | |
60 | self::$singleton = new wf_crm_webform_postprocess($node); | |
61 | } | |
62 | return self::$singleton; | |
63 | } | |
64 | ||
65 | /** | |
66 | * Called after a webform is submitted | |
67 | * Or, for a multipage form, called after each page | |
68 | * @param array $form | |
69 | * @param array $form_state (reference) | |
70 | */ | |
71 | public function validate($form, &$form_state) { | |
72 | $this->form = $form; | |
73 | $this->form_state = &$form_state; | |
74 | $this->rawValues = _webform_client_form_submit_flatten($this->node, wf_crm_aval($this->form_state, 'values:submitted')); | |
75 | $this->crmValues = wf_crm_enabled_fields($this->node, $this->rawValues); | |
76 | // Even though this object is destroyed between page submissions, this trick allows us to persist some data - see below | |
77 | $this->ent = wf_crm_aval($form_state, 'civicrm:ent', array()); | |
78 | ||
79 | $this->hiddenFieldValidation(); | |
80 | $this->validateThisPage($this->form['submitted']); | |
81 | ||
82 | if (!empty($this->data['participant']) && !empty($this->data['participant_reg_type'])) { | |
83 | $this->loadMultiPageData(); | |
84 | $this->validateParticipants(); | |
85 | } | |
86 | ||
87 | // Process live contribution. If the transaction is unsuccessful it will trigger a form validation error. | |
88 | if ($this->contribution_page) { | |
89 | // Ensure contribution js is still loaded if the form has to refresh | |
90 | $this->addPaymentJs(); | |
91 | $this->loadMultiPageData(); | |
92 | if ($this->tallyLineItems()) { | |
93 | if ($this->isLivePaymentProcessor() && $this->isPaymentPage() && !form_get_errors()) { | |
94 | if ($this->validateBillingFields()) { | |
95 | if ($this->createBillingContact()) { | |
96 | $this->submitLivePayment(); | |
97 | } | |
98 | } | |
99 | } | |
100 | } | |
101 | } | |
102 | // Even though this object is destroyed between page submissions, this trick allows us to persist some data - see above | |
103 | $form_state['civicrm']['ent'] = $this->ent; | |
104 | $form_state['civicrm']['line_items'] = $this->line_items; | |
105 | } | |
106 | ||
107 | /** | |
108 | * Process webform submission when it is about to be saved. Called by the following hook: | |
109 | * @see webform_civicrm_webform_submission_presave | |
110 | * @param stdClass $submission | |
111 | */ | |
112 | public function preSave(&$submission) { | |
113 | $this->submission = &$submission; | |
114 | $this->data = $this->settings['data']; | |
115 | // Check for existing submission | |
116 | $this->setUpdateParam(); | |
117 | // Fill $this->id from existing contacts | |
118 | $this->getExistingContactIds(); | |
119 | ||
120 | // While saving a draft, just skip to postSave and write the record | |
121 | if (!empty($this->submission->is_draft)) { | |
122 | return; | |
123 | } | |
124 | ||
125 | $this->fillDataFromSubmission(); | |
126 | ||
127 | // Create/update contacts | |
128 | foreach ($this->data['contact'] as $c => $contact) { | |
129 | if (empty($this->ent['contact'][$c]['id'])) { | |
130 | // Don't create contact if we don't have a name or email | |
131 | if ($this->isContactEmpty($contact)) { | |
132 | $this->ent['contact'][$c]['id'] = 0; | |
133 | continue; | |
134 | } | |
135 | $this->ent['contact'][$c]['id'] = $this->findDuplicateContact($contact); | |
136 | } | |
137 | ||
138 | // Current employer must wait for ContactRef ids to be filled | |
139 | unset($contact['contact'][1]['employer_id']); | |
140 | ||
141 | $newContact = empty($this->ent['contact'][$c]['id']); | |
142 | ||
143 | // Create new contact | |
144 | if ($newContact) { | |
145 | $this->ent['contact'][$c]['id'] = $this->createContact($contact); | |
146 | } | |
147 | if ($c == 1) { | |
148 | $this->setLoggingContact(); | |
149 | } | |
150 | // Update existing contact | |
151 | if (!$newContact) { | |
152 | $this->updateContact($contact, $c); | |
153 | } | |
154 | } | |
155 | // $this->ent['contact'] will now contain all contacts in order, with 0 as a placeholder id for any contact not saved | |
156 | ksort($this->ent['contact']); | |
157 | ||
158 | // Once all contacts are saved we can fill contact ref fields | |
159 | $this->fillContactRefs(); | |
160 | ||
161 | // Save a non-live transaction | |
162 | if (empty($this->ent['contribution'][1]['id']) && $this->totalContribution) { | |
163 | $this->createDeferredPayment(); | |
164 | } | |
165 | ||
166 | // Create/update other data associated with contacts | |
167 | foreach ($this->data['contact'] as $c => $contact) { | |
168 | $cid = $this->ent['contact'][$c]['id']; | |
169 | if (!$cid) { | |
170 | continue; | |
171 | } | |
172 | $this->saveCurrentEmployer($contact, $cid); | |
173 | ||
174 | $this->saveCustomData($contact, $cid, 'Contact', !empty($this->existing_contacts[$c])); | |
175 | ||
176 | $this->fillHiddenContactFields($cid, $c); | |
177 | ||
178 | $this->saveContactLocation($contact, $cid, $c); | |
179 | ||
180 | $this->saveGroupsAndTags($contact, $cid, $c); | |
181 | ||
182 | // Process relationships | |
183 | foreach (wf_crm_aval($contact, 'relationship', array()) as $n => $params) { | |
184 | $relationship_type_id = wf_crm_aval($params, 'relationship_type_id'); | |
185 | if ($relationship_type_id) { | |
186 | foreach ((array) $relationship_type_id as $params['relationship_type_id']) { | |
187 | $this->processRelationship($params, $cid, $this->ent['contact'][$n]['id']); | |
188 | } | |
189 | } | |
190 | } | |
191 | // Process event participation | |
192 | if (isset($this->all_sets['participant']) && !empty($this->data['participant_reg_type'])) { | |
193 | $this->processParticipants($c, $cid); | |
194 | } | |
195 | } | |
196 | // We do this after all contacts and addresses exist | |
197 | $this->processSharedAddresses(); | |
198 | ||
199 | // Process memberships after relationships have been created | |
200 | foreach ($this->ent['contact'] as $c => $contact) { | |
201 | if ($contact['id'] && isset($this->all_sets['membership']) && !empty($this->data['membership'][$c]['number_of_membership'])) { | |
202 | $this->processMemberships($c, $contact['id']); | |
203 | } | |
204 | } | |
205 | } | |
206 | ||
207 | /** | |
208 | * Process webform submission after it is has been saved. Called by the following hooks: | |
209 | * @see webform_civicrm_webform_submission_insert | |
210 | * @see webform_civicrm_webform_submission_update | |
211 | * @param stdClass $submission | |
212 | */ | |
213 | public function postSave($submission) { | |
214 | $this->submission = $submission; | |
215 | if (empty($this->submission->is_draft)) { | |
216 | // Save cases | |
217 | if (!empty($this->data['case']['number_of_case'])) { | |
218 | $this->processCases(); | |
219 | } | |
220 | // Save activities | |
221 | if (!empty($this->data['activity']['number_of_activity'])) { | |
222 | $this->processActivities(); | |
223 | } | |
224 | // Save grants | |
225 | if (isset($this->data['grant']['number_of_grant'])) { | |
226 | $this->processGrants(); | |
227 | } | |
228 | // Save contribution custom data & line-items | |
229 | if (!empty($this->ent['contribution'][1]['id'])) { | |
230 | $this->processContribution(); | |
231 | } | |
232 | } | |
233 | // Write record; we do this when creating, updating, or saving a draft of a webform submission. | |
234 | $record = $this->formatSubmission(); | |
235 | drupal_write_record('webform_civicrm_submissions', $record, $this->update); | |
236 | ||
237 | // Calling an IPN payment processor will result in a redirect so this happens after everything else | |
238 | if (empty($this->submission->is_draft) && !empty($this->ent['contribution'][1]['id']) && $this->contributionIsIncomplete && !$this->contributionIsPayLater) { | |
239 | $this->submitIPNPayment(); | |
240 | } | |
241 | // Send receipt | |
242 | if (empty($this->submission->is_draft) | |
243 | && !empty($this->ent['contribution'][1]['id']) | |
244 | && !empty($this->contribution_page['is_email_receipt']) | |
245 | && (!$this->contributionIsIncomplete || $this->contributionIsPayLater) ) { | |
246 | $this->sendReceipt(); | |
247 | } | |
248 | } | |
249 | ||
250 | /** | |
251 | * Send receipt | |
252 | */ | |
253 | private function sendReceipt(){ | |
254 | // tax integration | |
255 | if (!is_null($this->tax_rate)) { | |
256 | $template = CRM_Core_Smarty::singleton(); | |
257 | $template->assign('dataArray', array( "{$this->tax_rate}" => $this->tax_rate/100 )); | |
258 | } | |
259 | if ($this->contributionIsIncomplete) { | |
260 | $template = CRM_Core_Smarty::singleton(); | |
261 | $template->assign('is_pay_later', 1); | |
262 | } | |
263 | $contribute_id = $this->ent['contribution'][1]['id']; | |
264 | wf_civicrm_api('contribution', 'sendconfirmation', array('id' => $contribute_id) + $this->contribution_page); | |
265 | } | |
266 | ||
267 | /** | |
268 | * Formats submission data as expected by the schema | |
269 | */ | |
270 | private function formatSubmission() { | |
271 | $data = $this->ent; | |
272 | unset($data['contact']); | |
273 | $record = array( | |
274 | 'sid' => $this->submission->sid, | |
275 | 'contact_id' => '-', | |
276 | 'civicrm_data' => $data, | |
277 | ); | |
278 | foreach ($this->ent['contact'] as $contact) { | |
279 | $record['contact_id'] .= $contact['id'] . '-'; | |
280 | } | |
281 | return $record; | |
282 | } | |
283 | ||
284 | /** | |
285 | * Force Drupal to ignore errors for fields hidden by this module | |
286 | */ | |
287 | private function hiddenFieldValidation() { | |
288 | $errors = form_get_errors(); | |
289 | foreach ((array) $errors as $key => $error) { | |
290 | $pieces = wf_crm_explode_key(substr($key, strrpos($key, '][') + 2)); | |
291 | if ($pieces) { | |
292 | list( , $c, $ent, $n, $table, $name) = $pieces; | |
293 | if ($this->isFieldHiddenByExistingContactSettings($ent, $c, $table, $n, $name)) { | |
294 | $this->unsetError($key); | |
295 | } | |
296 | elseif ($table == 'address' && !empty($this->crmValues["civicrm_{$c}_contact_{$n}_address_master_id"])) { | |
297 | $master_id = $this->crmValues["civicrm_{$c}_contact_{$n}_address_master_id"]; | |
298 | // If widget is checkboxes, need to filter the array | |
299 | if (!is_array($master_id) || array_filter($master_id)) { | |
300 | $this->unsetError($key); | |
301 | } | |
302 | } | |
303 | } | |
304 | } | |
305 | } | |
306 | ||
307 | /** | |
308 | * Recursive validation callback for webform page submission | |
309 | * | |
310 | * @param array $elements | |
311 | * FAPI form array | |
312 | */ | |
313 | private function validateThisPage($elements) { | |
314 | // Recurse through form elements. | |
315 | foreach (element_children($elements) as $key) { | |
316 | if (is_array($elements[$key]) && ($element = $elements[$key])) { | |
317 | $this->validateThisPage($elements[$key]); | |
318 | if (!empty($element['#civicrm_data_type']) | |
319 | && substr(wf_crm_aval($element, '#type', ''), 0, 4) === 'text' | |
320 | && isset($element['#value']) | |
321 | && $element['#value'] !== '') { | |
322 | $dt = $element['#civicrm_data_type']; | |
323 | // Validate state/prov abbreviation | |
324 | if ($dt == 'state_province_abbr') { | |
325 | $ckey = str_replace('state_province', 'country', $key); | |
326 | if (!empty($this->crmValues[$ckey]) && is_numeric($this->crmValues[$ckey])) { | |
327 | $country_id = $this->crmValues[$ckey]; | |
328 | } | |
329 | else { | |
330 | $config = CRM_Core_Config::singleton(); | |
331 | $country_id = $config->defaultContactCountry; | |
332 | } | |
333 | $states = wf_crm_get_states($country_id); | |
334 | if ($states && !array_key_exists(strtoupper($element['#value']), $states)) { | |
335 | $countries = wf_crm_apivalues('address', 'getoptions', array('field' => 'country_id')); | |
336 | form_error($element, t('Mismatch: "@state" is not a state/province of %country. Please enter a valid state/province abbreviation for %field.', array('@state' => $element['#value'], '%country' => $countries[$country_id], '%field' => $element['#title']))); | |
337 | } | |
338 | } | |
339 | // Strings and files don't need any validation | |
340 | elseif ($dt !== 'String' && $dt !== 'Memo' && $dt !== 'File' | |
341 | && CRM_Utils_Type::escape($element['#value'], $dt, FALSE) === NULL) { | |
342 | // Allow data type names to be translated | |
343 | switch ($dt) { | |
344 | case 'Int': | |
345 | $dt = t('an integer'); | |
346 | break; | |
347 | case 'Float': | |
348 | $dt = t('a number'); | |
349 | break; | |
350 | case 'Link': | |
351 | $dt = t('a web address starting with http://'); | |
352 | break; | |
353 | case 'Money': | |
354 | $dt = t('a currency value'); | |
355 | break; | |
356 | } | |
357 | form_error($element, t('Please enter @type for %field.', array('@type' => $dt, '%field' => $element['#title']))); | |
358 | } | |
359 | } | |
360 | } | |
361 | } | |
362 | } | |
363 | ||
364 | /** | |
365 | * Validate event participants and add line items | |
366 | */ | |
367 | private function validateParticipants() { | |
368 | // If we have no valid contacts on the form, don't bother continuing | |
369 | if (!$this->existing_contacts) { | |
370 | return; | |
371 | } | |
372 | $count = $this->data['participant_reg_type'] == 'all' ? count($this->existing_contacts) : 1; | |
373 | // Collect selected events | |
374 | foreach ($this->data['participant'] as $c => $par) { | |
375 | if ($this->data['participant_reg_type'] == 'all') { | |
376 | $contacts = $this->existing_contacts; | |
377 | } | |
378 | elseif (isset($this->existing_contacts[$c])) { | |
379 | $contacts = array($this->existing_contacts[$c]); | |
380 | } | |
381 | else { | |
382 | continue; | |
383 | } | |
384 | foreach (wf_crm_aval($par, 'participant', array()) as $n => $p) { | |
385 | foreach (array_filter(wf_crm_aval($p, 'event_id', array())) as $id_and_type) { | |
386 | list($eid) = explode('-', $id_and_type); | |
387 | if (is_numeric($eid)) { | |
388 | $this->events[$eid]['ended'] = TRUE; | |
389 | $this->events[$eid]['title'] = t('this event'); | |
390 | $this->events[$eid]['count'] = wf_crm_aval($this->events, "$eid:count", 0) + $count; | |
391 | if (!empty($p['fee_amount'])) { | |
392 | $this->line_items[] = array( | |
393 | 'qty' => $count, | |
394 | 'entity_table' => 'civicrm_participant', | |
395 | 'event_id' => $eid, | |
396 | 'contact_ids' => $contacts, | |
397 | 'unit_price' => $p['fee_amount'], | |
398 | 'element' => "civicrm_{$c}_participant_{$n}_participant_{$id_and_type}", | |
399 | ); | |
400 | } | |
401 | } | |
402 | } | |
403 | } | |
404 | } | |
405 | // Subtract events already registered for - this only works with known contacts | |
406 | $cids = array_filter($this->existing_contacts); | |
407 | if ($this->events && $cids) { | |
408 | $dao = CRM_Core_DAO::executeQuery("SELECT event_id, contact_id | |
409 | FROM civicrm_participant p, civicrm_participant_status_type s | |
410 | WHERE s.id = p.status_id AND s.is_counted = 1 | |
411 | AND event_id IN (" . implode(',', array_keys($this->events)) . ") | |
412 | AND contact_id IN (" . implode(',', $cids) . ") | |
413 | AND is_test = 0"); | |
414 | while ($dao->fetch()) { | |
415 | if (isset($this->events[$dao->event_id])) { | |
416 | if (!(--$this->events[$dao->event_id]['count'])) { | |
417 | unset($this->events[$dao->event_id]); | |
418 | } | |
419 | } | |
420 | foreach ($this->line_items as $k => &$item) { | |
421 | if ($dao->event_id == $item['event_id'] && in_array($dao->contact_id, $item['contact_ids'])) { | |
422 | unset($this->line_items[$k]['contact_ids'][array_search($dao->contact_id, $item['contact_ids'])]); | |
423 | if (!(--$item['qty'])) { | |
424 | unset($this->line_items[$k]); | |
425 | } | |
426 | } | |
427 | } | |
428 | } | |
429 | $dao->free(); | |
430 | } | |
431 | $this->loadEvents(); | |
432 | // Add event info to line items | |
433 | $format = wf_crm_aval($this->data['reg_options'], 'title_display', 'title'); | |
434 | foreach ($this->line_items as &$item) { | |
435 | $item['label'] = wf_crm_format_event($this->events[$item['event_id']], $format); | |
436 | $item['financial_type_id'] = wf_crm_aval($this->events[$item['event_id']], 'financial_type_id', 'Event Fee'); | |
437 | } | |
438 | // Form Validation | |
439 | if (!empty($this->data['reg_options']['validate'])) { | |
440 | foreach ($this->events as $eid => $event) { | |
441 | if ($event['ended']) { | |
442 | form_set_error($eid, t('Sorry, you can no longer register for %event.', array('%event' => $event['title']))); | |
443 | } | |
444 | elseif ($event['max_participants'] && $event['count'] > $event['remaining']) { | |
445 | if (!empty($event['full'])) { | |
446 | form_set_error($eid, '<em>' . $event['title'] . '</em>: ' . $event['full_message']); | |
447 | } | |
448 | else { | |
449 | form_set_error($eid, format_plural($event['remaining'], | |
450 | 'Sorry, you tried to register !count people for %event but there is only 1 space remaining.', | |
451 | 'Sorry, you tried to register !count people for %event but there are only @count spaces remaining.', | |
452 | array('%event' => $event['title'], '!count' => $event['count']))); | |
453 | } | |
454 | } | |
455 | } | |
456 | } | |
457 | } | |
458 | ||
459 | /** | |
460 | * Load entire webform submission during validation, including contact ids and $this->data | |
461 | * Used when validation for one page needs access to submitted values from other pages | |
462 | */ | |
463 | private function loadMultiPageData() { | |
464 | if (!$this->multiPageDataLoaded) { | |
465 | $this->multiPageDataLoaded = TRUE; | |
466 | if (!empty($this->form_state['storage']['submitted']) | |
467 | && wf_crm_aval($this->form_state, 'storage:page_num', 1) > 1) | |
468 | { | |
469 | $this->rawValues += $this->form_state['storage']['submitted']; | |
470 | $this->crmValues = wf_crm_enabled_fields($this->node, $this->rawValues); | |
471 | } | |
472 | ||
473 | // Check how many valid contacts we have | |
474 | foreach ($this->data['contact'] as $c => $contact) { | |
475 | // Check if we have a contact_id | |
476 | $fid = "civicrm_{$c}_contact_1_contact_existing"; | |
477 | if ($this->verifyExistingContact(wf_crm_aval($this->crmValues, $fid), $fid)) { | |
478 | $this->existing_contacts[$c] = $this->crmValues[$fid]; | |
479 | } | |
480 | // Or else see if enough info was entered to create a contact - use 0 as a placeholder for unknown cid | |
481 | elseif (wf_crm_name_field_exists($this->crmValues, $c, $contact['contact'][1]['contact_type'])) { | |
482 | $this->existing_contacts[$c] = 0; | |
483 | } | |
484 | } | |
485 | ||
486 | // Fill data array with submitted form values | |
487 | $this->fillDataFromSubmission(); | |
488 | } | |
489 | } | |
490 | ||
491 | /** | |
492 | * If this is an update op, set param for drupal_write_record() | |
493 | */ | |
494 | private function setUpdateParam() { | |
495 | if (!empty($this->submission->sid)) { | |
496 | $submitted = array($this->submission->sid => new stdClass()); | |
497 | webform_civicrm_webform_submission_load($submitted); | |
498 | if (isset($submitted[$this->submission->sid]->civicrm)) { | |
499 | $this->update = 'sid'; | |
500 | } | |
501 | } | |
502 | } | |
503 | ||
504 | /** | |
505 | * Fetch contact ids from "existing contact" fields | |
506 | */ | |
507 | private function getExistingContactIds() { | |
508 | foreach ($this->enabled as $field_key => $fid) { | |
509 | if (substr($field_key, -8) == 'existing') { | |
510 | list(, $c, ) = explode('_', $field_key, 3); | |
511 | $cid = wf_crm_aval($this->submissionValue($fid), 0); | |
512 | $this->ent['contact'][$c]['id'] = $this->verifyExistingContact($cid, $field_key); | |
513 | if ($this->ent['contact'][$c]['id']) { | |
514 | $this->existing_contacts[$c] = $cid; | |
515 | } | |
516 | } | |
517 | } | |
518 | } | |
519 | ||
520 | /** | |
521 | * Ensure we have a valid contact id in a contact ref field | |
522 | * @param $cid | |
523 | * @param $fid | |
524 | * @return int | |
525 | */ | |
526 | private function verifyExistingContact($cid, $fid) { | |
527 | if (wf_crm_is_positive($cid) && !empty($this->enabled[$fid])) { | |
528 | module_load_include('inc', 'webform_civicrm', 'includes/contact_component'); | |
529 | $component = $this->getComponent($fid); | |
530 | $filters = wf_crm_search_filters($this->node, $component); | |
531 | // Verify access to this contact | |
532 | if (wf_crm_contact_access($component, $filters, $cid) !== FALSE) { | |
533 | return $cid; | |
534 | } | |
535 | } | |
536 | return 0; | |
537 | } | |
538 | ||
539 | /** | |
540 | * Check if at least one required field was filled for a contact | |
541 | * @param array $contact | |
542 | * @return bool | |
543 | */ | |
544 | private function isContactEmpty($contact) { | |
545 | $contact_type = $contact['contact'][1]['contact_type']; | |
546 | foreach (wf_crm_required_contact_fields($contact_type) as $f) { | |
547 | if (!empty($contact[$f['table']][1][$f['name']])) { | |
548 | return FALSE; | |
549 | } | |
550 | } | |
551 | return TRUE; | |
552 | } | |
553 | ||
554 | /** | |
555 | * Search for an existing contact using default strict rule | |
556 | * @param array $contact | |
557 | * @return int | |
558 | */ | |
559 | private function findDuplicateContact($contact) { | |
560 | $dupes = $rule_type = $rule_id = NULL; | |
561 | $rule = wf_crm_aval($contact, 'matching_rule', 'Unsupervised', TRUE); | |
562 | if ($rule) { | |
563 | $params = array('check_permission' => FALSE); | |
564 | foreach ($contact as $table => $field) { | |
565 | if (is_array($field) && !empty($field[1])) { | |
566 | if (substr($table, 0, 2) == 'cg') { | |
567 | //TODO pass custom data to deduper | |
568 | } | |
569 | // If sharing an address, use the master | |
570 | elseif ($table == 'address' && !empty($field[1]['master_id'])) { | |
571 | $m = $field[1]['master_id']; | |
572 | // If master address is exposed to the form, use it | |
573 | if (!empty($contact[$m]['address'][1])) { | |
574 | $params['civicrm_address'] = $contact[$m]['address'][1]; | |
575 | } | |
576 | // Else look up the master contact's address | |
577 | elseif (!empty($this->existing_contacts[$m])) { | |
578 | $masters = wf_civicrm_api('address', 'get', | |
579 | array( | |
580 | 'contact_id' => $this->ent['contact'][$m]['id'], | |
581 | 'sort' => 'is_primary DESC' | |
582 | )); | |
583 | if (!empty($masters['values'])) { | |
584 | $params['civicrm_address'] = array_shift($masters['values']); | |
585 | } | |
586 | } | |
587 | } | |
588 | elseif (in_array($table, array( | |
589 | 'contact', | |
590 | 'address', | |
591 | 'email', | |
592 | 'phone', | |
593 | 'website' | |
594 | ))) { | |
595 | $params['civicrm_' . $table] = $field[1]; | |
596 | } | |
597 | } | |
598 | } | |
599 | // This is either a default type (Unsupervised or Supervised) or the id of a specific rule | |
600 | if (is_numeric($rule)) { | |
601 | $rule_id = $rule; | |
602 | } | |
603 | else { | |
604 | $rule_type = $rule; | |
605 | } | |
606 | $dupes = CRM_Dedupe_Finder::dupesByParams($params, ucfirst($contact['contact'][1]['contact_type']), $rule_type, array(), $rule_id); | |
607 | } | |
608 | if ($dupes) { | |
609 | return $dupes[0]; | |
610 | } | |
611 | return 0; | |
612 | } | |
613 | ||
614 | /** | |
615 | * Create a new contact | |
616 | * @param array $contact | |
617 | * @return int | |
618 | */ | |
619 | private function createContact($contact) { | |
620 | $params = $contact['contact'][1]; | |
621 | // CiviCRM API is too picky about this, imho | |
622 | $params['contact_type'] = ucfirst($params['contact_type']); | |
623 | unset($params['contact_id'], $params['id']); | |
624 | if (!isset($params['source'])) { | |
625 | $params['source'] = $this->settings['new_contact_source']; | |
626 | } | |
627 | // If creating individual with no first/last name, | |
628 | // set display name and sort_name | |
629 | if ($params['contact_type'] == 'Individual' && empty($params['first_name']) && empty($params['last_name'])) { | |
630 | $params['display_name'] = $params['sort_name'] = empty($params['nick_name']) ? $contact['email'][1]['email'] : $params['nick_name']; | |
631 | } | |
632 | $result = wf_civicrm_api('contact', 'create', $params); | |
633 | return wf_crm_aval($result, 'id', 0); | |
634 | } | |
635 | ||
636 | /** | |
637 | * Update a contact | |
638 | * @param array $contact | |
639 | * @param int $c | |
640 | */ | |
641 | private function updateContact($contact, $c) { | |
642 | $params = $contact['contact'][1]; | |
643 | unset($params['contact_type'], $params['contact_id']); | |
644 | // Fetch data from existing multivalued fields | |
645 | $fetch = $multi = array(); | |
646 | foreach ($this->all_fields as $fid => $field) { | |
647 | if (!empty($field['extra']['multiple']) && substr($fid, 0, 7) == 'contact') { | |
648 | list(, $name) = explode('_', $fid, 2); | |
649 | if ($name != 'privacy' && isset($params[$name])) { | |
650 | $fetch["return.$name"] = 1; | |
651 | $multi[] = $name; | |
652 | } | |
653 | } | |
654 | } | |
655 | // Merge data from existing multivalued fields | |
656 | if ($multi) { | |
657 | $existing = wf_civicrm_api('contact', 'get', array('id' => $this->ent['contact'][$c]['id']) + $fetch); | |
658 | $existing = wf_crm_aval($existing, 'values:' . $this->ent['contact'][$c]['id'], array()); | |
659 | foreach ($multi as $name) { | |
660 | $exist = array_filter(drupal_map_assoc((array) wf_crm_aval($existing, $name, array()))); | |
661 | // Only known contacts are allowed to empty a field | |
662 | if (!empty($this->existing_contacts[$c])) { | |
663 | foreach ($this->getExposedOptions("civicrm_{$c}_contact_1_contact_$name") as $k => $v) { | |
664 | unset($exist[$k]); | |
665 | } | |
666 | } | |
667 | $params[$name] = array_unique(array_merge($params[$name], $exist)); | |
668 | } | |
669 | } | |
670 | $params['id'] = $this->ent['contact'][$c]['id']; | |
671 | wf_civicrm_api('contact', 'create', $params); | |
672 | } | |
673 | ||
674 | /** | |
675 | * Save current employer for a contact | |
676 | * @param array $contact | |
677 | * @param int $cid | |
678 | */ | |
679 | function saveCurrentEmployer($contact, $cid) { | |
680 | if ($contact['contact'][1]['contact_type'] == 'individual' && !empty($contact['contact'][1]['employer_id'])) { | |
681 | wf_civicrm_api('contact', 'create', array( | |
682 | 'id' => $cid, | |
683 | 'employer_id' => $contact['contact'][1]['employer_id'], | |
684 | )); | |
685 | } | |
686 | } | |
687 | ||
688 | /** | |
689 | * Fill values for hidden ID & CS fields | |
690 | * @param int $c | |
691 | * @param int $cid | |
692 | */ | |
693 | private function fillHiddenContactFields($cid, $c) { | |
694 | $fid = 'civicrm_' . $c . '_contact_1_contact_'; | |
695 | if (!empty($this->enabled[$fid . 'contact_id'])) { | |
696 | $this->submissionValue($this->enabled[$fid . 'contact_id'], $cid); | |
697 | } | |
698 | if (!empty($this->enabled[$fid . 'existing'])) { | |
699 | $this->submissionValue($this->enabled[$fid . 'existing'], $cid); | |
700 | } | |
701 | if (!empty($this->enabled[$fid . 'external_identifier']) && !empty($this->existing_contacts[$c])) { | |
702 | $exid = wf_civicrm_api('contact', 'get', array('contact_id' => $cid, 'return.external_identifier' => 1)); | |
703 | $this->submissionValue($this->enabled[$fid . 'external_identifier'], wf_crm_aval($exid, "values:$cid:external_identifier")); | |
704 | } | |
705 | if (!empty($this->enabled[$fid . 'cs'])) { | |
706 | $cs = $this->submissionValue($this->enabled[$fid . 'cs']); | |
707 | $life = !empty($cs[0]) ? intval(24 * $cs[0]) : 'inf'; | |
708 | $cs = CRM_Contact_BAO_Contact_Utils::generateChecksum($cid, NULL, $life); | |
709 | $this->submissionValue($this->enabled[$fid . 'cs'], $cs); | |
710 | } | |
711 | } | |
712 | ||
713 | /** | |
714 | * Save location data for a contact | |
715 | * @param array $contact | |
716 | * @param int $cid | |
717 | * @param int $c | |
718 | */ | |
719 | private function saveContactLocation($contact, $cid, $c) { | |
720 | foreach (wf_crm_location_fields() as $location) { | |
721 | if (!empty($contact[$location])) { | |
722 | $existing = array(); | |
723 | $params = array('contact_id' => $cid); | |
724 | if ($location != 'website') { | |
725 | $params['options'] = array('sort' => 'is_primary DESC'); | |
726 | } | |
727 | $result = wf_civicrm_api($location, 'get', $params); | |
728 | if (!empty($result['values'])) { | |
729 | $contact[$location] = self::reorderLocationValues($contact[$location], $result['values'], $location); | |
730 | // start array index at 1 | |
731 | $existing = array_merge(array(array()), $result['values']); | |
732 | } | |
733 | foreach ($contact[$location] as $i => $params) { | |
734 | // Translate state/prov abbr to id | |
735 | if (!empty($params['state_province_id'])) { | |
736 | $config = CRM_Core_Config::singleton(); | |
737 | if (!($params['state_province_id'] = wf_crm_state_abbr($params['state_province_id'], 'id', wf_crm_aval($params, 'country_id', $config->defaultContactCountry)))) { | |
738 | $params['state_province_id'] = ''; | |
739 | } | |
740 | } | |
741 | // Substitute county stub ('-' is a hack to get around required field when there are no available counties) | |
742 | if (isset($params['county_id']) && $params['county_id'] === '-') { | |
743 | $params['county_id'] = ''; | |
744 | } | |
745 | // Update drupal email address | |
746 | if ($location == 'email' && !empty($params['email']) && $i == 1) { | |
747 | $uid = wf_crm_user_cid($cid, 'contact'); | |
748 | if ($uid) { | |
749 | $user = user_load($uid); | |
750 | if ($params['email'] != $user->mail) { | |
751 | // Verify this email is unique before saving it to user | |
752 | $args = array(':mail' => $params['email']); | |
753 | if (!(db_query("SELECT count(uid) FROM {users} WHERE mail = :mail", $args)->fetchField())) { | |
754 | user_save($user, array('mail' => $params['email'])); | |
755 | } | |
756 | } | |
757 | } | |
758 | } | |
759 | // Check if anything was changed, else skip the update | |
760 | if (!empty($existing[$i])) { | |
761 | $same = TRUE; | |
762 | foreach ($params as $param => $val) { | |
763 | if ($val != (string) wf_crm_aval($existing[$i], $param, '')) { | |
764 | $same = FALSE; | |
765 | } | |
766 | } | |
767 | if ($same) { | |
768 | continue; | |
769 | } | |
770 | } | |
771 | if ($location == 'address') { | |
772 | // Store shared addresses for later since we haven't necessarily processed | |
773 | // the contact this address is shared with yet. | |
774 | if (!empty($params['master_id'])) { | |
775 | $this->shared_address[$cid][$i] = array( | |
776 | 'id' => wf_crm_aval($existing, "$i:id"), | |
777 | 'mc' => $params['master_id'], | |
778 | 'loc' => $params['location_type_id'], | |
779 | ); | |
780 | continue; | |
781 | } | |
782 | // Reset calculated values when updating an address | |
783 | $params['master_id'] = $params['geo_code_1'] = $params['geo_code_2'] = 'null'; | |
784 | } | |
785 | $params['contact_id'] = $cid; | |
786 | if (!empty($existing[$i])) { | |
787 | $params['id'] = $existing[$i]['id']; | |
788 | } | |
789 | if ($this->locationIsEmpty($location, $params)) { | |
790 | // Delete this location if nothing was entered and this is a known contact | |
791 | if (!empty($this->existing_contacts[$c]) && !empty($params['id'])) { | |
792 | wf_civicrm_api($location, 'delete', $params); | |
793 | } | |
794 | continue; | |
795 | } | |
796 | if ($location != 'website') { | |
797 | if (empty($params['location_type_id'])) { | |
798 | $params['location_type_id'] = wf_crm_aval($existing, "$i:location_type_id", 1); | |
799 | } | |
800 | $params['is_primary'] = $i == 1 ? 1 : 0; | |
801 | } | |
802 | wf_civicrm_api($location, 'create', $params); | |
803 | } | |
804 | } | |
805 | } | |
806 | } | |
807 | ||
808 | /** | |
809 | * Save groups and tags for a contact | |
810 | * @param array $contact | |
811 | * @param int $cid | |
812 | * @param int $c | |
813 | */ | |
814 | private function saveGroupsAndTags($contact, $cid, $c) { | |
815 | // Process groups & tags | |
816 | foreach ($this->all_fields as $fid => $field) { | |
817 | list($set, $type) = explode('_', $fid, 2); | |
818 | if ($set == 'other') { | |
819 | $field_name = 'civicrm_' . $c . '_contact_1_' . $fid; | |
820 | if (!empty($contact['other'][1][$type]) || isset($this->enabled[$field_name])) { | |
821 | $add = wf_crm_aval($contact, "other:1:$type", array()); | |
822 | $remove = empty($this->existing_contacts[$c]) ? array() : $this->getExposedOptions($field_name, $add); | |
823 | $this->addOrRemoveMultivaluedData($field['table'], 'contact', $cid, $add, $remove); | |
824 | } | |
825 | } | |
826 | } | |
827 | } | |
828 | ||
829 | /** | |
830 | * Handle adding/removing multivalued data for a contact/activity/etc. | |
831 | * Currently used only for groups and tags, but written with expansion in mind. | |
832 | * | |
833 | * @param $data_type | |
834 | * 'group' or 'tag' | |
835 | * @param $entity_type | |
836 | * Parent entity: 'contact' etc. | |
837 | * @param $id | |
838 | * Entity id | |
839 | * @param $add | |
840 | * Groups/tags to add | |
841 | * @param array $remove | |
842 | * Groups/tags to remove | |
843 | */ | |
844 | private function addOrRemoveMultivaluedData($data_type, $entity_type, $id, $add, $remove = array()) { | |
845 | $confirmations_sent = $existing = $params = array(); | |
846 | $add = drupal_map_assoc($add); | |
847 | static $mailing_lists = array(); | |
848 | ||
849 | switch ($data_type) { | |
850 | case 'group': | |
851 | $api = 'group_contact'; | |
852 | break; | |
853 | case 'tag': | |
854 | $api = 'entity_tag'; | |
855 | break; | |
856 | default: | |
857 | $api = $data_type; | |
858 | } | |
859 | if (!empty($add) || !empty($remove)) { | |
860 | // Retrieve current records for this entity | |
861 | if ($entity_type == 'contact') { | |
862 | $params['contact_id'] = $id; | |
863 | } | |
864 | else { | |
865 | $params['entity_id'] = $id; | |
866 | $params['entity_type'] = 'civicrm_' . $entity_type; | |
867 | } | |
868 | $fetch = wf_civicrm_api($api, 'get', $params); | |
869 | foreach (wf_crm_aval($fetch, 'values', array()) as $i) { | |
870 | $existing[] = $i[$data_type . '_id']; | |
871 | unset($add[$i[$data_type . '_id']]); | |
872 | } | |
873 | foreach ($remove as $i => $name) { | |
874 | if (!in_array($i, $existing)) { | |
875 | unset($remove[$i]); | |
876 | } | |
877 | } | |
878 | } | |
879 | if (!empty($add)) { | |
880 | // Prepare for sending subscription confirmations | |
881 | if ($data_type == 'group' && !empty($this->settings['confirm_subscription'])) { | |
882 | // Retrieve this contact's primary email address and perform error-checking | |
883 | $result = wf_civicrm_api('email', 'get', array('contact_id' => $id, 'options' => array('sort' => 'is_primary DESC'))); | |
884 | if (!empty($result['values'])) { | |
885 | foreach ($result['values'] as $value) { | |
886 | if (($value['is_primary'] || empty($email)) && strpos($value['email'], '@')) { | |
887 | $email = $value['email']; | |
888 | } | |
889 | } | |
890 | $mailer_params = array( | |
891 | 'contact_id' => $id, | |
892 | 'email' => $email, | |
893 | ); | |
894 | if (empty($mailing_lists)) { | |
895 | $mailing_lists = wf_crm_apivalues('group', 'get', array('visibility' => 'Public Pages', 'group_type' => 2), 'title'); | |
896 | } | |
897 | } | |
898 | } | |
899 | foreach ($add as $a) { | |
900 | $params[$data_type . '_id'] = $mailer_params['group_id'] = $a; | |
901 | if ($data_type == 'group' && isset($mailing_lists[$a]) && !empty($email)) { | |
902 | $result = wf_civicrm_api('mailing_event_subscribe', 'create', $mailer_params); | |
903 | if (empty($result['is_error'])) { | |
904 | $confirmations_sent[] = check_plain($mailing_lists[$a]); | |
905 | } | |
906 | else { | |
907 | wf_civicrm_api($api, 'create', $params); | |
908 | } | |
909 | } | |
910 | else { | |
911 | wf_civicrm_api($api, 'create', $params); | |
912 | } | |
913 | } | |
914 | if ($confirmations_sent) { | |
915 | drupal_set_message(t('A message has been sent to %email to confirm subscription to !group.', array('%email' => $email, '!group' => '<em>' . implode('</em> ' . t('and') . ' <em>', $confirmations_sent) . '</em>'))); | |
916 | } | |
917 | } | |
918 | // Remove data from entity | |
919 | foreach ($remove as $a => $name) { | |
920 | $params[$data_type . '_id'] = $a; | |
921 | wf_civicrm_api($api, 'delete', $params); | |
922 | } | |
923 | if (!empty($remove) && $data_type == 'group') { | |
924 | $display_name = wf_civicrm_api('contact', 'get', array('contact_id' => $id, 'return.display_name' => 1)); | |
925 | $display_name = wf_crm_aval($display_name, "values:$id:display_name", t('Contact')); | |
926 | drupal_set_message(t('%contact has been removed from !group.', array('%contact' => $display_name, '!group' => '<em>' . implode('</em> ' . t('and') . ' <em>', $remove) . '</em>'))); | |
927 | } | |
928 | } | |
929 | ||
930 | /** | |
931 | * Add/update relationship for a pair of contacts | |
932 | * | |
933 | * @param $params | |
934 | * Params array for relationship api | |
935 | * @param $cid1 | |
936 | * Contact id | |
937 | * @param $cid2 | |
938 | * Contact id | |
939 | */ | |
940 | private function processRelationship($params, $cid1, $cid2) { | |
941 | if (!empty($params['relationship_type_id']) && $cid2 && $cid1 != $cid2) { | |
942 | list($type, $side) = explode('_', $params['relationship_type_id']); | |
943 | $existing = $this->getRelationship(array($params['relationship_type_id']), $cid1, $cid2); | |
944 | $perm = wf_crm_aval($params, 'relationship_permission'); | |
945 | // Swap contacts if this is an inverse relationship | |
946 | if ($side == 'b' || ($existing && $existing['contact_id_a'] != $cid1)) { | |
947 | list($cid1, $cid2) = array($cid2, $cid1); | |
948 | if ($perm == 1 || $perm == 2) { | |
949 | $perm = $perm == 1 ? 2 : 1; | |
950 | } | |
951 | } | |
952 | $params += $existing; | |
953 | $params['contact_id_a'] = $cid1; | |
954 | $params['contact_id_b'] = $cid2; | |
955 | $params['relationship_type_id'] = $type; | |
956 | if ($perm) { | |
957 | $params['is_permission_a_b'] = $params['is_permission_b_a'] = $perm == 3 ? 1 : 0; | |
958 | if ($perm == 1 || $perm == 2) { | |
959 | $params['is_permission_' . ($perm == 1 ? 'a_b' : 'b_a')] = 1; | |
960 | } | |
961 | } | |
962 | unset($params['relationship_permission']); | |
963 | wf_civicrm_api('relationship', 'create', $params); | |
964 | } | |
965 | } | |
966 | ||
967 | /** | |
968 | * Process event participation for a contact | |
969 | * @param int $c | |
970 | * @param int $cid | |
971 | */ | |
972 | private function processParticipants($c, $cid) { | |
973 | static $registered_by_id = array(); | |
974 | $n = $this->data['participant_reg_type'] == 'separate' ? $c : 1; | |
975 | if ($p = wf_crm_aval($this->data, "participant:$n:participant")) { | |
976 | // Fetch existing participant records | |
977 | $existing = array(); | |
978 | $dao = CRM_Core_DAO::executeQuery("SELECT id, event_id FROM civicrm_participant WHERE contact_id = $cid AND is_test = 0"); | |
979 | while ($dao->fetch()) { | |
980 | $existing[$dao->event_id] = $dao->id; | |
981 | } | |
982 | foreach ($p as $e => $params) { | |
983 | $remove = array(); | |
984 | $fid = "civicrm_{$c}_participant_{$e}_participant_event_id"; | |
985 | // Automatic status - de-selected events will be cancelled if 'disable_unregister' is not selected | |
986 | if (empty($this->data['reg_options']['disable_unregister'])) { | |
987 | if (empty($params['status_id'])) { | |
988 | foreach ($this->getExposedOptions($fid) as $eid => $title) { | |
989 | list($eid) = explode('-', $eid); | |
990 | if (isset($existing[$eid])) { | |
991 | $remove[$eid] = $title; | |
992 | } | |
993 | } | |
994 | } | |
995 | } | |
996 | if (!empty($params['event_id'])) { | |
997 | $params['contact_id'] = $cid; | |
998 | if (empty($params['campaign_id']) || empty($this->all_fields['participant_campaign_id'])) { | |
999 | unset($params['campaign_id']); | |
1000 | } | |
1001 | // Reformat custom data from nested arrays | |
1002 | $custom = array(); | |
1003 | foreach ($this->data['participant'][$n] as $key => $vals) { | |
1004 | if (substr($key, 0, 2) == 'cg' && isset($vals[$e])) { | |
1005 | $custom[$key][1] = $vals[$e]; | |
1006 | } | |
1007 | } | |
1008 | // Loop through event ids to support multi-valued form elements | |
1009 | $this->events = (array) $params['event_id']; | |
1010 | foreach ($this->events as $i => $id_and_type) { | |
1011 | if (!empty($id_and_type)) { | |
1012 | list($eid) = explode('-', $id_and_type); | |
1013 | $params['event_id'] = $eid; | |
1014 | unset($remove[$eid], $params['registered_by_id'], $params['id'], $params['source']); | |
1015 | // Is existing participant? | |
1016 | if (!empty($existing[$eid])) { | |
1017 | $params['id'] = $existing[$params['event_id']]; | |
1018 | } | |
1019 | else { | |
1020 | if (isset($this->data['contact'][$c]['contact'][1]['source'])) { | |
1021 | $params['source'] = $this->data['contact'][$c]['contact'][1]['source']; | |
1022 | } | |
1023 | else { | |
1024 | $params['source'] = $this->settings['new_contact_source']; | |
1025 | } | |
1026 | if ($c > 1 && !empty($registered_by_id[$e][$i])) { | |
1027 | $params['registered_by_id'] = $registered_by_id[$e][$i]; | |
1028 | } | |
1029 | } | |
1030 | // Automatic status | |
1031 | if (empty($params['status_id']) && empty($params['id'])) { | |
1032 | $params['status_id'] = 'Registered'; | |
1033 | // Pending payment status | |
1034 | if ($this->contributionIsIncomplete && !empty($params['fee_amount'])) { | |
1035 | $params['status_id'] = $this->contributionIsPayLater ? 'Pending from pay later' : 'Pending from incomplete transaction'; | |
1036 | } | |
1037 | } | |
1038 | // Do not update status of existing participant in "Automatic" mode | |
1039 | if (empty($params['status_id'])) { | |
1040 | unset($params['status_id']); | |
1041 | } | |
1042 | $result = wf_civicrm_api('participant', 'create', $params); | |
1043 | // Update line-item | |
1044 | foreach ($this->line_items as &$item) { | |
1045 | if ($item['element'] == "civicrm_{$n}_participant_{$e}_participant_{$id_and_type}") { | |
1046 | if (empty($item['participant_id'])) { | |
1047 | $item['participant_id'] = $item['entity_id'] = $result['id']; | |
1048 | } | |
1049 | $item['participant_count'] = wf_crm_aval($item, 'participant_count', 0) + 1; | |
1050 | break; | |
1051 | } | |
1052 | } | |
1053 | // When registering contact 1, store id to apply to other contacts | |
1054 | if ($c == 1) { | |
1055 | $registered_by_id[$e][$i] = $result['id']; | |
1056 | } | |
1057 | if ($custom) { | |
1058 | $this->saveCustomData($custom, $result['id'], 'Participant'); | |
1059 | } | |
1060 | } | |
1061 | } | |
1062 | } | |
1063 | foreach ($remove as $eid => $title) { | |
1064 | wf_civicrm_api('participant', 'create', array('status_id' => "Cancelled", 'id' => $existing[$eid])); | |
1065 | drupal_set_message(t('Registration cancelled for !event', array('!event' => $title))); | |
1066 | } | |
1067 | } | |
1068 | } | |
1069 | } | |
1070 | ||
1071 | /** | |
1072 | * Process memberships for a contact | |
1073 | * Called during webform submission | |
1074 | * @param int $c | |
1075 | * @param int $cid | |
1076 | */ | |
1077 | private function processMemberships($c, $cid) { | |
1078 | static $types; | |
1079 | if (!isset($types)) { | |
1080 | $types = wf_crm_apivalues('membership_type', 'get'); | |
1081 | } | |
1082 | $existing = $this->findMemberships($cid); | |
1083 | foreach (wf_crm_aval($this->data, "membership:$c:membership", array()) as $n => $params) { | |
1084 | $membershipStatus = ""; | |
1085 | $membershipEndDate = ""; | |
1086 | $is_active = FALSE; | |
1087 | ||
1088 | if (empty($params['membership_type_id'])) { | |
1089 | continue; | |
1090 | } | |
1091 | // Search for existing membership to renew - must belong to same domain and organization | |
1092 | // But not necessarily the same membership type to allow for upsell | |
1093 | if (!empty($params['num_terms'])) { | |
1094 | $type = $types[$params['membership_type_id']]; | |
1095 | foreach ($existing as $mem) { | |
1096 | $existing_type = $types[$mem['membership_type_id']]; | |
1097 | if ($type['domain_id'] == $existing_type['domain_id'] && $type['member_of_contact_id'] == $existing_type['member_of_contact_id']) { | |
1098 | $params['id'] = $mem['id']; | |
1099 | // If we have an exact match, look no further | |
1100 | if ($mem['membership_type_id'] == $params['membership_type_id']) { | |
1101 | $is_active = $mem['is_active']; | |
1102 | $membershipStatus = $mem['status']; | |
1103 | $membershipEndDate = $mem['end_date']; | |
1104 | break; | |
1105 | } | |
1106 | } | |
1107 | } | |
1108 | } | |
1109 | if (empty($params['id'])) { | |
1110 | if (isset($this->data['contact'][$c]['contact'][1]['source'])) { | |
1111 | $params['source'] = $this->data['contact'][$c]['contact'][1]['source']; | |
1112 | } | |
1113 | else { | |
1114 | $params['source'] = $this->settings['new_contact_source']; | |
1115 | } | |
1116 | } | |
1117 | // Automatic status | |
1118 | if (empty($params['status_id'])) { | |
1119 | unset($params['status_id']); | |
1120 | // Pending payment status | |
1121 | if ($this->contributionIsIncomplete && $this->getMembershipTypeField($params['membership_type_id'], 'minimum_fee')) { | |
1122 | if ($is_active == FALSE) { | |
1123 | $params['status_id'] = 'Pending'; | |
1124 | } else { | |
1125 | $params['status_id'] = $membershipStatus; | |
1126 | $params['end_date'] = $membershipEndDate; | |
1127 | } | |
1128 | } | |
1129 | } | |
1130 | // Override status | |
1131 | else { | |
1132 | $params['is_override'] = 1; | |
1133 | } | |
1134 | $params['contact_id'] = $cid; | |
1135 | // The api won't let us manually set status without this weird param | |
1136 | $params['skipStatusCal'] = !empty($params['status_id']); | |
1137 | ||
1138 | $result = wf_civicrm_api('membership', 'create', $params); | |
1139 | ||
1140 | if (!empty($result['id'])) { | |
1141 | // Issue #2516924 If existing membership create renewal activity | |
1142 | if (!empty($params['id'])) { | |
1143 | $membership = $result['values'][$result['id']]; | |
1144 | $actParams = array( | |
1145 | 'source_record_id' => $result['id'], | |
1146 | 'activity_type_id' => 'Membership Renewal', | |
1147 | 'target_id' => $cid, | |
1148 | ); | |
1149 | $memType = wf_civicrm_api('MembershipType', 'getsingle', array('id' => $membership['membership_type_id'])); | |
1150 | $memStatus = wf_civicrm_api('MembershipStatus', 'getsingle', array('id' => $membership['status_id'])); | |
1151 | $actParams['subject'] = ts("%1 - Status: %2", array(1 => $memType['name'], 2 => $memStatus['label'])); | |
1152 | wf_civicrm_api('Activity', 'create', $actParams); | |
1153 | } | |
1154 | ||
1155 | foreach ($this->line_items as &$item) { | |
1156 | if ($item['element'] == "civicrm_{$c}_membership_{$n}") { | |
1157 | $item['membership_id'] = $result['id']; | |
1158 | break; | |
1159 | } | |
1160 | } | |
1161 | } | |
1162 | } | |
1163 | } | |
1164 | ||
1165 | /** | |
1166 | * Process shared addresses | |
1167 | */ | |
1168 | private function processSharedAddresses() { | |
1169 | foreach ($this->shared_address as $cid => $shared) { | |
1170 | foreach ($shared as $i => $addr) { | |
1171 | if (!empty($this->ent['contact'][$addr['mc']]['id'])) { | |
1172 | $masters = wf_civicrm_api('address', 'get', array('contact_id' => $this->ent['contact'][$addr['mc']]['id'], 'options' => array('sort' => 'is_primary DESC'))); | |
1173 | if (!empty($masters['values'])) { | |
1174 | $masters = array_values($masters['values']); | |
1175 | // Pick the address with the same location type; default to primary. | |
1176 | $params = $masters[0]; | |
1177 | foreach ($masters as $m) { | |
1178 | if ($m['location_type_id'] == $addr['loc']) { | |
1179 | $params = $m; | |
1180 | break; | |
1181 | } | |
1182 | } | |
1183 | $params['master_id'] = $params['id']; | |
1184 | $params['id'] = $addr['id']; | |
1185 | $params['contact_id'] = $cid; | |
1186 | $params['is_primary'] = $i == 1; | |
1187 | wf_civicrm_api('address', 'create', $params); | |
1188 | } | |
1189 | } | |
1190 | } | |
1191 | } | |
1192 | } | |
1193 | ||
1194 | /** | |
1195 | * Save case data | |
1196 | */ | |
1197 | private function processCases() { | |
1198 | foreach (wf_crm_aval($this->data, 'case', array()) as $n => $data) { | |
1199 | if (is_array($data) && !empty($data['case'][1]['client_id'])) { | |
1200 | $params = $data['case'][1]; | |
1201 | // Set some defaults in create mode | |
1202 | if (empty($this->ent['case'][$n]['id'])) { | |
1203 | if (empty($params['case_type_id'])) { | |
1204 | // Abort if no case type. | |
1205 | continue; | |
1206 | } | |
1207 | // Automatic status... for lack of anything fancier just pick the first option ("Ongoing" on a standard install) | |
1208 | if (empty($params['status_id'])) { | |
1209 | $options = wf_crm_apivalues('case', 'getoptions', array('field' => 'status_id')); | |
1210 | $params['status_id'] = current(array_keys($options)); | |
1211 | } | |
1212 | if (empty($params['subject'])) { | |
1213 | $params['subject'] = check_plain($this->node->title); | |
1214 | } | |
1215 | // Automatic creator_id - default to current user or contact 1 | |
1216 | if (empty($data['case'][1]['creator_id'])) { | |
1217 | if (user_is_logged_in()) { | |
1218 | $params['creator_id'] = wf_crm_user_cid(); | |
1219 | } | |
1220 | elseif (!empty($this->ent['contact'][1]['id'])) { | |
1221 | $params['creator_id'] = $this->ent['contact'][1]['id']; | |
1222 | } | |
1223 | else { | |
1224 | // Abort if no creator available | |
1225 | continue; | |
1226 | } | |
1227 | } | |
1228 | } | |
1229 | // Update mode | |
1230 | else { | |
1231 | $params['id'] = $this->ent['case'][$n]['id']; | |
1232 | // These params aren't allowed in update mode | |
1233 | unset($params['creator_id'], $params['case_type_id']); | |
1234 | } | |
1235 | // Allow "automatic" status to pass-thru | |
1236 | if (empty($params['status_id'])) { | |
1237 | unset($params['status_id']); | |
1238 | } | |
1239 | $result = wf_civicrm_api('case', 'create', $params); | |
1240 | // Final processing if save was successful | |
1241 | if (!empty($result['id'])) { | |
1242 | // Store id | |
1243 | $this->ent['case'][$n]['id'] = $result['id']; | |
1244 | // Save custom field data | |
1245 | $this->saveCustomData($data, $result['id'], 'Case', FALSE); | |
1246 | // Save case roles | |
1247 | foreach ($params as $param => $val) { | |
1248 | if ($val && strpos($param, 'role_') === 0) { | |
1249 | foreach ((array) $params['client_id'] as $client) { | |
1250 | wf_civicrm_api('relationship', 'create', array( | |
1251 | 'relationship_type_id' => substr($param, 5), | |
1252 | 'contact_id_a' => $client, | |
1253 | 'contact_id_b' => $val, | |
1254 | 'case_id' => $result['id'], | |
1255 | )); | |
1256 | } | |
1257 | } | |
1258 | } | |
1259 | } | |
1260 | } | |
1261 | } | |
1262 | } | |
1263 | ||
1264 | /** | |
1265 | * Save activity data | |
1266 | */ | |
1267 | private function processActivities() { | |
1268 | foreach (wf_crm_aval($this->data, 'activity', array()) as $n => $data) { | |
1269 | if (is_array($data)) { | |
1270 | $params = $data['activity'][1]; | |
1271 | // Create mode | |
1272 | if (empty($this->ent['activity'][$n]['id'])) { | |
1273 | // Automatic status based on whether activity_date_time is in the future | |
1274 | if (empty($params['status_id'])) { | |
1275 | $params['status_id'] = strtotime(wf_crm_aval($params, 'activity_date_time', 'now')) > time() ? 'Scheduled' : 'Completed'; | |
1276 | } | |
1277 | // Automatic source_contact_id - default to current user or contact 1 | |
1278 | if (empty($data['activity'][1]['source_contact_id'])) { | |
1279 | if (user_is_logged_in()) { | |
1280 | $params['source_contact_id'] = wf_crm_user_cid(); | |
1281 | } | |
1282 | elseif (!empty($this->ent['contact'][1]['id'])) { | |
1283 | $params['source_contact_id'] = $this->ent['contact'][1]['id']; | |
1284 | } | |
1285 | } | |
1286 | // Format details as html | |
1287 | $params['details'] = nl2br(wf_crm_aval($params, 'details', '')); | |
1288 | // Add webform results to details | |
1289 | if (!empty($this->data['activity'][$n]['details']['entire_result'])) { | |
1290 | module_load_include('inc', 'webform', 'includes/webform.submissions'); | |
1291 | $submission = webform_submission_render($this->node, $this->submission, NULL, 'html'); | |
1292 | $params['details'] .= drupal_render($submission); | |
1293 | } | |
1294 | if (!empty($this->data['activity'][$n]['details']['view_link'])) { | |
1295 | $params['details'] .= '<p>' . l(t('View Webform Submission'), "node/{$this->node->nid}/submission/{$this->submission->sid}", array('absolute' => TRUE, 'alias' => TRUE)) . '</p>'; | |
1296 | } | |
1297 | if (!empty($this->data['activity'][$n]['details']['edit_link'])) { | |
1298 | $params['details'] .= '<p>' . l(t('Edit Submission'), "node/{$this->node->nid}/submission/{$this->submission->sid}/edit", array('absolute' => TRUE, 'alias' => TRUE)) . '</p>'; | |
1299 | } | |
1300 | } | |
1301 | // Update mode | |
1302 | else { | |
1303 | $params['id'] = $this->ent['activity'][$n]['id']; | |
1304 | } | |
1305 | // Allow "automatic" values to pass-thru if empty | |
1306 | foreach ($params as $field => $value) { | |
1307 | if ((isset($this->all_fields["activity_$field"]['empty_option']) || isset($this->all_fields["activity_$field"]['exposed_empty_option'])) && !$value) { | |
1308 | unset($params[$field]); | |
1309 | } | |
1310 | } | |
1311 | // Handle survey data | |
1312 | if (!empty($this->data['activity'][$n]['activity'][1]['survey_id'])) { | |
1313 | $params['source_record_id'] = $this->data['activity'][$n]['activity'][1]['survey_id']; | |
1314 | // Set default subject | |
1315 | if (empty($params['id']) && empty($params['subject'])) { | |
1316 | $survey = wf_civicrm_api('survey', 'getsingle', array('id' => $this->data['activity'][$n]['activity'][1]['survey_id'], 'return' => 'title')); | |
1317 | $params['subject'] = wf_crm_aval($survey, 'title', ''); | |
1318 | } | |
1319 | } | |
1320 | // File on case | |
1321 | if (!empty($this->data['activity'][$n]['case_type_id'])) { | |
1322 | // Webform case | |
1323 | if ($this->data['activity'][$n]['case_type_id'][0] === '#') { | |
1324 | $case_num = substr($this->data['activity'][$n]['case_type_id'], 1); | |
1325 | if (!empty($this->ent['case'][$case_num]['id'])) { | |
1326 | $params['case_id'] = $this->ent['case'][$case_num]['id']; | |
1327 | } | |
1328 | } | |
1329 | // Search for case by criteria | |
1330 | else { | |
1331 | $case_contact = $this->ent['contact'][$this->data['activity'][$n]['case_contact_id']]['id']; | |
1332 | if ($case_contact) { | |
1333 | // Proceed only if this activity is not already filed on a case | |
1334 | if (empty($params['id']) || !wf_crm_apivalues('case', 'get', array('activity_id' => $params['id']))) { | |
1335 | $case = $this->findCaseForContact($case_contact, array( | |
1336 | 'status_id' => $this->data['activity'][$n]['case_status_id'], | |
1337 | 'case_type_id' => $this->data['activity'][$n]['case_type_id'], | |
1338 | )); | |
1339 | if ($case) { | |
1340 | $params['case_id'] = $case['id']; | |
1341 | } | |
1342 | } | |
1343 | } | |
1344 | } | |
1345 | } | |
1346 | $result = wf_civicrm_api('activity', 'create', $params); | |
1347 | // Final processing if save was successful | |
1348 | if (!empty($result['id'])) { | |
1349 | // Store id | |
1350 | $this->ent['activity'][$n]['id'] = $result['id']; | |
1351 | // Save custom data & attachments | |
1352 | $this->saveCustomData($data, $result['id'], 'Activity', FALSE); | |
1353 | if (isset($data['activityupload'])) { | |
1354 | $this->processAttachments('activity', $n, $result['id'], empty($params['id'])); | |
1355 | } | |
1356 | if (!empty($params['assignee_contact_id'])) { | |
1357 | if (CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'activity_assignee_notification')) { | |
1358 | // Send email to assignees. TODO: Move to CiviCRM API? | |
1359 | $assignee = wf_civicrm_api('contact', 'getsingle', array('id' => $params['assignee_contact_id'])); | |
1360 | if (!empty($assignee['email'])) { | |
1361 | $mail = array($assignee['email'] => $assignee); | |
1362 | // Include attachments while sending a copy of activity. | |
1363 | $attachments = CRM_Core_BAO_File::getEntityFile('civicrm_activity', $this->ent['act'][1]); | |
1364 | CRM_Case_BAO_Case::sendActivityCopy(NULL, $result['id'], $mail, $attachments, NULL); | |
1365 | } | |
1366 | } | |
1367 | } | |
1368 | } | |
1369 | } | |
1370 | } | |
1371 | } | |
1372 | ||
1373 | /** | |
1374 | * Save grants | |
1375 | */ | |
1376 | private function processGrants() { | |
1377 | foreach (wf_crm_aval($this->data, 'grant', array()) as $n => $data) { | |
1378 | if (is_array($data) && !empty($data['grant'][1]['contact_id'])) { | |
1379 | $params = $data['grant'][1]; | |
1380 | // Set some defaults in create mode | |
1381 | if (empty($this->ent['grant'][$n]['id'])) { | |
1382 | // Automatic status... for lack of anything fancier just pick the first option ("Submitted" on a standard install) | |
1383 | if (empty($params['status_id'])) { | |
1384 | $options = wf_crm_apivalues('grant', 'getoptions', array('field' => 'status_id')); | |
1385 | $params['status_id'] = current(array_keys($options)); | |
1386 | } | |
1387 | if (empty($params['application_received_date'])) { | |
1388 | $params['application_received_date'] = 'now'; | |
1389 | } | |
1390 | if (empty($params['grant_report_received'])) { | |
1391 | $params['grant_report_received'] = '0'; | |
1392 | } | |
1393 | } | |
1394 | // Update mode | |
1395 | else { | |
1396 | $params['id'] = $this->ent['grant'][$n]['id']; | |
1397 | } | |
1398 | // Allow "automatic" status to pass-thru | |
1399 | if (empty($params['status_id'])) { | |
1400 | unset($params['status_id']); | |
1401 | } | |
1402 | $result = wf_civicrm_api('grant', 'create', $params); | |
1403 | // Final processing if save was successful | |
1404 | if (!empty($result['id'])) { | |
1405 | // Store id | |
1406 | $this->ent['grant'][$n]['id'] = $result['id']; | |
1407 | // Save custom data & attachments | |
1408 | $this->saveCustomData($data, $result['id'], 'Grant', FALSE); | |
1409 | if (isset($data['grantupload'])) { | |
1410 | $this->processAttachments('grant', $n, $result['id'], empty($params['id'])); | |
1411 | } | |
1412 | } | |
1413 | } | |
1414 | } | |
1415 | } | |
1416 | ||
1417 | /** | |
1418 | * Calculate line-items for this webform submission | |
1419 | */ | |
1420 | private function tallyLineItems() { | |
1421 | // Contribution | |
1422 | $fid = 'civicrm_1_contribution_1_contribution_total_amount'; | |
1423 | if (isset($this->enabled[$fid]) || $this->getData($fid) > 0) { | |
1424 | $this->line_items[] = array( | |
1425 | 'qty' => 1, | |
1426 | 'unit_price' => $this->getData($fid), | |
1427 | 'financial_type_id' => $this->contribution_page['financial_type_id'], | |
1428 | 'label' => wf_crm_aval($this->node->webform['components'], $this->enabled[$fid] . ':name', t('Contribution')), | |
1429 | 'element' => 'civicrm_1_contribution_1', | |
1430 | 'entity_table' => 'civicrm_contribution', | |
1431 | ); | |
1432 | } | |
1433 | // Membership | |
1434 | foreach (wf_crm_aval($this->data, 'membership', array()) as $c => $memberships) { | |
1435 | if (isset($this->existing_contacts[$c]) && !empty($memberships['number_of_membership'])) { | |
1436 | foreach ($memberships['membership'] as $n => $item) { | |
1437 | if (!empty($item['membership_type_id'])) { | |
1438 | $type = $item['membership_type_id']; | |
1439 | $price = $this->getMembershipTypeField($type, 'minimum_fee'); | |
1440 | ||
1441 | //if number of terms is set, regard membership fee field as price per term | |
1442 | //if you choose to set dates manually while membership fee field is enabled, take the membership fee as total cost of this membership | |
1443 | if (isset($item['fee_amount'])) { | |
1444 | $price = $item['fee_amount']; | |
1445 | if (empty($item['num_terms'])) { | |
1446 | $item['num_terms'] = 1; | |
1447 | } | |
1448 | } | |
1449 | ||
1450 | if ($price) { | |
1451 | $this->line_items[] = array( | |
1452 | 'qty' => $item['num_terms'], | |
1453 | 'unit_price' => $price, | |
1454 | 'financial_type_id' => $this->getMembershipTypeField($type, 'financial_type_id'), | |
1455 | 'label' => $this->getMembershipTypeField($type, 'name'), | |
1456 | 'element' => "civicrm_{$c}_membership_{$n}", | |
1457 | 'entity_table' => 'civicrm_membership', | |
1458 | ); | |
1459 | } | |
1460 | } | |
1461 | } | |
1462 | } | |
1463 | } | |
1464 | // Calculate totals | |
1465 | $this->totalContribution = 0; | |
1466 | foreach ($this->line_items as &$item) { | |
1467 | // tax integration | |
1468 | if (!is_null($this->tax_rate)) { | |
1469 | $item['line_total'] = $item['unit_price'] * (int) $item['qty']; | |
1470 | $item['tax_amount'] = ($this->tax_rate / 100) * $item['line_total']; | |
1471 | $this->totalContribution += (1 + $this->tax_rate / 100) * $item['unit_price'] * (int) $item['qty']; | |
1472 | } | |
1473 | else { | |
1474 | $this->totalContribution += $item['line_total'] = $item['unit_price'] * (int) $item['qty']; | |
1475 | } | |
1476 | } | |
1477 | return $this->totalContribution; | |
1478 | } | |
1479 | ||
1480 | /** | |
1481 | * Are billing fields exposed to this webform page? | |
1482 | * @return bool | |
1483 | */ | |
1484 | private function isPaymentPage() { | |
1485 | $page = wf_crm_aval($this->form_state, 'storage:page_num', 1); | |
1486 | $field = $this->getComponent('civicrm_1_contribution_1_contribution_contribution_page_id'); | |
1487 | return $page == $field['page_num']; | |
1488 | } | |
1489 | ||
1490 | /** | |
1491 | * @return bool | |
1492 | */ | |
1493 | private function isLivePaymentProcessor() { | |
1494 | if ($this->payment_processor) { | |
1495 | if ($this->payment_processor['billing_mode'] == self::BILLING_MODE_LIVE) { | |
1496 | return TRUE; | |
1497 | } | |
1498 | // In mixed mode (where there is e.g. a PayPal button + credit card fields) the cc field will contain a placeholder if the button was clicked | |
1499 | if ($this->payment_processor['billing_mode'] == self::BILLING_MODE_MIXED && wf_crm_aval($_POST, 'credit_card_number') != 'express') { | |
1500 | return TRUE; | |
1501 | } | |
1502 | } | |
1503 | return FALSE; | |
1504 | } | |
1505 | ||
1506 | /** | |
1507 | * Normalize and validate billing input | |
1508 | * @return bool | |
1509 | */ | |
1510 | private function validateBillingFields() { | |
1511 | $valid = TRUE; | |
1512 | $params = $card_errors = array(); | |
1513 | // These are hard-coded in CiviCRM so we may as well hard-code them here | |
1514 | // Value = translated label to be shown during validation or FALSE if not required | |
1515 | $billing_fields = array( | |
1516 | 'credit_card_number' => ts('Card Number'), | |
1517 | 'cvv2' => ts('Security Code'), | |
1518 | 'credit_card_type' => ts('Card Type'), | |
1519 | 'billing_first_name' => ts('Billing First Name'), | |
1520 | 'billing_middle_name' => FALSE, | |
1521 | 'billing_last_name' => ts('Billing Last Name'), | |
1522 | 'billing_street_address-5' => ts('Street Address'), | |
1523 | 'billing_city-5' => ts('City'), | |
1524 | 'billing_country_id-5' => ts('Country'), | |
1525 | 'billing_state_province_id-5' => FALSE, | |
1526 | 'billing_postal_code-5' => ts('Postal Code'), | |
1527 | ); | |
1528 | if (!empty($_POST['stripe_token'])) { | |
1529 | // Using Stripe payment processor - cc fields not posted | |
1530 | $billing_fields['credit_card_number'] = FALSE; | |
1531 | $billing_fields['cvv2'] = FALSE; | |
1532 | } | |
1533 | if (!empty($_POST['bank_account_type'])) { | |
1534 | // unset/bypass the CC validation if we're doing Direct Debit (ACHEFT) - in that case we have a Bank Account Type | |
1535 | $billing_fields['credit_card_number'] = FALSE; | |
1536 | $billing_fields['cvv2'] = FALSE; | |
1537 | $billing_fields['credit_card_type'] = FALSE; | |
1538 | $_POST['credit_card_exp_date']['M'] = '12'; | |
1539 | $_POST['credit_card_exp_date']['Y'] = '2099'; | |
1540 | } | |
1541 | foreach ($billing_fields as $field => $label) { | |
1542 | if (empty($_POST[$field]) && $label !== FALSE) { | |
1543 | form_set_error($field, t('!name field is required.', array('!name' => check_plain($label)))); | |
1544 | $valid = FALSE; | |
1545 | } | |
1546 | if (!empty($_POST[$field])) { | |
1547 | $name = str_replace('billing_', '', str_replace('-5', '', $field)); | |
1548 | $submitted[$name] = $params[$name] = $params[$field] = $_POST[$field]; | |
1549 | } | |
1550 | } | |
1551 | // Validate country | |
1552 | if (!empty($params['country_id'])) { | |
1553 | if (!array_key_exists($params['country_id'], wf_crm_apivalues('address', 'getoptions', array('field' => 'country_id')))) { | |
1554 | form_set_error('billing_country_id-5', t('Illegal value entered for Country')); | |
1555 | $valid = $params['country_id'] = FALSE; | |
1556 | } | |
1557 | } | |
1558 | // Validate state/province | |
1559 | if (!empty($params['country_id'])) { | |
1560 | $states = wf_crm_apivalues('address', 'getoptions', array('field' => 'state_province_id', 'country_id' => $params['country_id'])); | |
1561 | if ($states && (empty($params['state_province_id']) || !isset($states[$params['state_province_id']]))) { | |
1562 | form_set_error('billing_state_province_id-5', t('!name field is required.', array('!name' => check_plain(ts('State/Province'))))); | |
1563 | $valid = FALSE; | |
1564 | } | |
1565 | } | |
1566 | // Validate credit card number & cvv2 | |
1567 | CRM_Core_Payment_Form::validateCreditCard($params, $card_errors); | |
1568 | foreach ($card_errors as $field => $msg) { | |
1569 | form_set_error($field, $msg); | |
1570 | $valid = FALSE; | |
1571 | } | |
1572 | // Check expiration date | |
1573 | $submitted['credit_card_exp_date[Y]'] = $params['year'] = wf_crm_aval($_POST, 'credit_card_exp_date:Y', 0); | |
1574 | // There seems to be some inconsistency with capitalization here | |
1575 | $params['month'] = (int) wf_crm_aval($_POST, 'credit_card_exp_date:M', wf_crm_aval($_POST, 'credit_card_exp_date:m', 0)); | |
1576 | $submitted['credit_card_exp_date[M]'] = $submitted['credit_card_exp_date[m]'] = $params['month']; | |
1577 | if ($params['year'] < date('Y') || ($params['year'] == date('Y') && $params['month'] < date('n'))) { | |
1578 | form_set_error('billing', ts('Credit card expiration date cannot be a past date.')); | |
1579 | $valid = FALSE; | |
1580 | } | |
1581 | ||
1582 | for ($i = 1; $i <= $this->data['contact'][1]['number_of_email']; ++$i) { | |
1583 | if (!empty($this->crmValues["civicrm_1_contact_{$i}_email_email"])) { | |
1584 | $params['email'] = $this->crmValues["civicrm_1_contact_{$i}_email_email"]; | |
1585 | break; | |
1586 | } | |
1587 | } | |
1588 | if (empty($params['email'])) { | |
1589 | form_set_error('billing_email', ts('An email address is required to complete this transaction.')); | |
1590 | $valid = FALSE; | |
1591 | } | |
1592 | if ($valid) { | |
1593 | $this->billing_params = $params; | |
1594 | } | |
1595 | // Since billing fields are not "real" form fields they get cleared if the page reloads. | |
1596 | // We add a bit of js to fix this annoyance. | |
1597 | drupal_add_js(array('webform_civicrm' => array('billingSubmission' => $submitted)), 'setting'); | |
1598 | return $valid; | |
1599 | } | |
1600 | ||
1601 | /** | |
1602 | * Create contact 1 if not already existing (required by contribution.transact) | |
1603 | * @return int | |
1604 | */ | |
1605 | private function createBillingContact() { | |
1606 | $cid = wf_crm_aval($this->existing_contacts, 1); | |
1607 | if (!$cid) { | |
1608 | $contact = $this->data['contact'][1]; | |
1609 | // Only use middle name from billing if we are using the rest of the billing name as well | |
1610 | if (empty($contact['contact'][1]['first_name']) && !empty($this->billing_params['middle_name'])) { | |
1611 | $contact['contact'][1]['middle_name'] = $this->billing_params['middle_name']; | |
1612 | } | |
1613 | $contact['contact'][1] += array( | |
1614 | 'first_name' => $this->billing_params['first_name'], | |
1615 | 'last_name' => $this->billing_params['last_name'], | |
1616 | ); | |
1617 | $cid = $this->findDuplicateContact($contact); | |
1618 | } | |
1619 | $address = array( | |
1620 | 'street_address' => $this->billing_params['street_address'], | |
1621 | 'city' => $this->billing_params['city'], | |
1622 | 'country_id' => $this->billing_params['country_id'], | |
1623 | 'state_province_id' => wf_crm_aval($this->billing_params, 'state_province_id'), | |
1624 | 'postal_code' => $this->billing_params['postal_code'], | |
1625 | 'location_type_id' => 'Billing', | |
1626 | ); | |
1627 | $email = array( | |
1628 | 'email' => $this->billing_params['email'], | |
1629 | 'location_type_id' => 'Billing', | |
1630 | ); | |
1631 | if (!$cid) { | |
1632 | $cid = $this->createContact($contact); | |
1633 | } | |
1634 | else { | |
1635 | foreach (array('address', 'email') as $loc) { | |
1636 | $result = wf_civicrm_api($loc, 'get', array( | |
1637 | 'contact_id' => $cid, | |
1638 | 'location_type_id' => 'Billing', | |
1639 | )); | |
1640 | // Use first id if we have any results | |
1641 | if (!empty($result['values'])) { | |
1642 | $ids = array_keys($result['values']); | |
1643 | ${$loc}['id'] = $ids[0]; | |
1644 | } | |
1645 | } | |
1646 | } | |
1647 | if ($cid) { | |
1648 | $address['contact_id'] = $email['contact_id'] = $this->ent['contact'][1]['id'] = $cid; | |
1649 | wf_civicrm_api('address', 'create', $address); | |
1650 | wf_civicrm_api('email', 'create', $email); | |
1651 | } | |
1652 | return $cid; | |
1653 | } | |
1654 | ||
1655 | /** | |
1656 | * Execute payment processor transaction | |
1657 | * This happens during form validation and sets a form error if unsuccessful | |
1658 | */ | |
1659 | private function submitLivePayment() { | |
1660 | $result = wf_civicrm_api('contribution', 'transact', $this->contributionParams()); | |
1661 | if (empty($result['id'])) { | |
1662 | if (!empty($result['error_message'])) { | |
1663 | form_set_error('contribution', $result['error_message']); | |
1664 | } | |
1665 | else { | |
1666 | form_set_error('contribution', t('Transaction failed. Please verify all billing fields are correct.')); | |
1667 | } | |
1668 | return; | |
1669 | } | |
1670 | $this->ent['contribution'][1]['id'] = $result['id']; | |
1671 | } | |
1672 | ||
1673 | /** | |
1674 | * Create Incomplete (Pay-Later or IPN) Contribution | |
1675 | */ | |
1676 | private function createDeferredPayment() { | |
1677 | $this->contributionIsIncomplete = TRUE; | |
1678 | $this->contributionIsPayLater = empty($this->data['contribution'][1]['contribution'][1]['payment_processor_id']); | |
1679 | $params = $this->contributionParams(); | |
1680 | $params['contribution_status_id'] = 'Pending'; | |
1681 | $params['is_pay_later'] = $this->contributionIsPayLater; | |
1682 | //Fix IPN payments marked as paid by cheque | |
1683 | if (empty($params['payment_instrument_id'])) { | |
1684 | if ($params['payment_processor_id'] && $this->contribution_page['is_monetary']) { | |
1685 | $defaultPaymentInstrument = CRM_Core_OptionGroup::values('payment_instrument', FALSE, FALSE, FALSE, 'AND is_default = 1'); | |
1686 | $params['payment_instrument_id'] = key($defaultPaymentInstrument); | |
1687 | } | |
1688 | } | |
1689 | $result = wf_civicrm_api('contribution', 'create', $params); | |
1690 | $this->ent['contribution'][1]['id'] = $result['id']; | |
1691 | } | |
1692 | ||
1693 | /** | |
1694 | * Call IPN payment processor to redirect to payment site | |
1695 | */ | |
1696 | private function submitIPNPayment() { | |
1697 | $config = CRM_Core_Config::singleton(); | |
1698 | $params = $this->data['contribution'][1]['contribution'][1]; | |
1699 | $processor_type = wf_civicrm_api('payment_processor', 'getsingle', array('id' => $params['payment_processor_id'])); | |
1700 | if (version_compare($this->civicrm_version, 4.7, '<')) { | |
1701 | $mode = empty($params['is_test']) ? 'live' : 'test'; | |
1702 | $paymentProcessor = CRM_Core_Payment::singleton($mode, $processor_type); | |
1703 | } | |
1704 | else { | |
1705 | $paymentProcessor = \Civi\Payment\System::singleton()->getByName($processor_type, $params['is_test']); | |
1706 | } | |
1707 | // Add contact details to params (most processors want a first_name and last_name) | |
1708 | $contact = wf_civicrm_api('contact', 'getsingle', array('id' => $this->ent['contact'][1]['id'])); | |
1709 | $params += $contact; | |
1710 | $params['contributionID'] = $params['id'] = $this->ent['contribution'][1]['id']; | |
1711 | // Generate a fake qfKey in case payment processor redirects to contribution thank-you page | |
1712 | $params['qfKey'] = $this->getQfKey(); | |
1713 | ||
1714 | $params['contactID'] = $params['contact_id']; | |
1715 | $params['currency'] = $params['currencyID'] = $this->contribution_page['currency']; | |
1716 | $params['total_amount'] = $this->totalContribution; | |
1717 | // Some processors want this one way, some want it the other | |
1718 | $params['amount'] = $params['total_amount']; | |
1719 | ||
1720 | $params['financial_type_id'] = $this->contribution_page['financial_type_id']; | |
1721 | ||
1722 | $params['source'] = $this->settings['new_contact_source']; | |
1723 | $params['item_name'] = t('Webform Payment: @title', array('@title' => $this->node->title)); | |
1724 | ||
1725 | if (method_exists($paymentProcessor, 'setSuccessUrl')) { | |
1726 | $paymentProcessor->setSuccessUrl($this->getIpnRedirectUrl('success')); | |
1727 | $paymentProcessor->setCancelUrl($this->getIpnRedirectUrl('cancel')); | |
1728 | } | |
1729 | // Legacy paypal settings. Remove when dropping support for CiviCRM 4.6 and below. | |
1730 | // @see webform_civicrm_civicrm_alterPaymentProcessorParams | |
1731 | $params['webform_redirect_cancel'] = $this->getIpnRedirectUrl('cancel'); | |
1732 | $params['webform_redirect_success'] = $this->getIpnRedirectUrl('success'); | |
1733 | ||
1734 | $paymentProcessor->doTransferCheckout($params, 'contribute'); | |
1735 | } | |
1736 | ||
1737 | /** | |
1738 | * @param $type | |
1739 | * @return string | |
1740 | */ | |
1741 | public function getIpnRedirectUrl($type) { | |
1742 | $url = trim($this->node->webform['redirect_url']); | |
1743 | if ($url == '<none>' || $type == 'cancel') { | |
1744 | $url = url('node/' . $this->node->nid, array('absolute' => TRUE)); | |
1745 | } | |
1746 | elseif ($url == '<confirmation>') { | |
1747 | $query = array('sid' => $this->submission->sid); | |
1748 | // Add token if user is not authenticated, inline with 'webform_client_form_submit()' | |
1749 | if (user_is_anonymous()) { | |
1750 | $query['token'] = webform_get_submission_access_token($this->submission); | |
1751 | } | |
1752 | $url = url("node/{$this->node->nid}/done", array('query' => $query, 'absolute' => TRUE)); | |
1753 | } | |
1754 | else { | |
1755 | $parsed = webform_replace_url_tokens($url, $this->node, $this->submission); | |
1756 | $parsed[1]['absolute'] = TRUE; | |
1757 | $url = url($parsed[0], $parsed[1]); | |
1758 | } | |
1759 | return $url; | |
1760 | } | |
1761 | ||
1762 | /** | |
1763 | * Format contribution params for transact/pay-later | |
1764 | * @return array | |
1765 | */ | |
1766 | private function contributionParams() { | |
1767 | $params = $this->billing_params + $this->data['contribution'][1]['contribution'][1]; | |
1768 | $params['financial_type_id'] = $this->contribution_page['financial_type_id']; | |
1769 | $params['currency'] = $params['currencyID'] = $this->contribution_page['currency']; | |
1770 | $params['skipRecentView'] = $params['skipLineItem'] = 1; | |
1771 | $params['contact_id'] = $this->ent['contact'][1]['id']; | |
1772 | $params['total_amount'] = $this->totalContribution; | |
1773 | ||
1774 | // Some processors use this for matching and updating the contribution status | |
1775 | if (!$this->contributionIsPayLater) { | |
1776 | $params['invoice_id'] = $this->data['contribution'][1]['contribution'][1]['invoiceID'] = md5(uniqid(rand(), TRUE)); | |
1777 | } | |
1778 | ||
1779 | // tax integration | |
1780 | // TODO: needs review if this is changed in 4.6 | |
1781 | if (!is_null($this->tax_rate)) { | |
1782 | $params['non_deductible_amount'] = $this->totalContribution; | |
1783 | $params['tax_amount'] = ($params['total_amount'] / ($this->tax_rate + 100)) * $this->tax_rate; | |
1784 | } | |
1785 | $params['description'] = t('Webform Payment: @title', array('@title' => $this->node->title)); | |
1786 | if (!isset($params['source'])) { | |
1787 | $params['source'] = $this->settings['new_contact_source']; | |
1788 | } | |
1789 | ||
1790 | // pass all submitted values to payment processor | |
1791 | foreach ($_POST as $key => $value) { | |
1792 | if (empty($params[$key])) { | |
1793 | $params[$key] = $value; | |
1794 | } | |
1795 | } | |
1796 | ||
1797 | // Fix bug for testing. | |
1798 | if ($params['is_test'] == 1) { | |
1799 | $liveProcessorName = wf_civicrm_api('payment_processor', 'getvalue', array( | |
1800 | 'id' => $params['payment_processor_id'], | |
1801 | 'return' => 'name', | |
1802 | )); | |
1803 | // Lookup current domain for multisite support | |
1804 | static $domain = 0; | |
1805 | if (!$domain) { | |
1806 | $domain = wf_civicrm_api('domain', 'get', array('current_domain' => 1, 'return' => 'id')); | |
1807 | $domain = wf_crm_aval($domain, 'id', 1); | |
1808 | } | |
1809 | $params['payment_processor_id'] = wf_civicrm_api('payment_processor', 'getvalue', array( | |
1810 | 'return' => 'id', | |
1811 | 'name' => $liveProcessorName, | |
1812 | 'is_test' => 1, | |
1813 | 'domain_id' => $domain, | |
1814 | )); | |
1815 | } | |
1816 | ||
1817 | // doPayment requries payment_processor and payment_processor_mode fields. | |
1818 | if (version_compare($this->civicrm_version, '4.7', '>=')) { | |
1819 | $params['payment_processor'] = $params['payment_processor_id']; | |
1820 | } | |
1821 | ||
1822 | // Save this stuff for later | |
1823 | unset($params['soft'], $params['honor_contact_id'], $params['honor_type_id']); | |
1824 | return $params; | |
1825 | } | |
1826 | ||
1827 | /** | |
1828 | * Post-processing of contribution | |
1829 | * This happens during form post-processing | |
1830 | */ | |
1831 | private function processContribution() { | |
1832 | $contribution = $this->data['contribution'][1]['contribution'][1]; | |
1833 | $id = $this->ent['contribution'][1]['id']; | |
1834 | // Save custom data | |
1835 | $this->saveCustomData($this->data['contribution'][1], $id, 'Contribution', FALSE); | |
1836 | // Save soft credits | |
1837 | if (!empty($contribution['soft'])) { | |
1838 | foreach (array_filter($contribution['soft']) as $cid) { | |
1839 | wf_civicrm_api('contribution_soft', 'create', array( | |
1840 | 'contact_id' => $cid, | |
1841 | 'contribution_id' => $id, | |
1842 | 'amount' => $contribution['total_amount'], | |
1843 | 'currency' => $this->contribution_page['currency'], | |
1844 | )); | |
1845 | } | |
1846 | } | |
1847 | // Save honoree | |
1848 | // FIXME: these api params were deprecated in 4.5, should be switched to use soft-credits when we drop support for 4.4 | |
1849 | if (!empty($contribution['honor_contact_id']) && !empty($contribution['honor_type_id'])) { | |
1850 | wf_civicrm_api('contribution', 'create', array( | |
1851 | 'id' => $id, | |
1852 | 'total_amount' => $contribution['total_amount'], | |
1853 | 'honor_contact_id' => $contribution['honor_contact_id'], | |
1854 | 'honor_type_id' => $contribution['honor_type_id'], | |
1855 | )); | |
1856 | } | |
1857 | ||
1858 | $contributionResult = CRM_Contribute_BAO_Contribution::getValues(array('id' => $id), CRM_Core_DAO::$_nullArray, CRM_Core_DAO::$_nullArray); | |
1859 | ||
1860 | // Save line-items | |
1861 | foreach ($this->line_items as &$item) { | |
1862 | if (empty($item['line_total'])) { | |
1863 | continue; | |
1864 | } | |
1865 | if (empty($item['entity_id'])) { | |
1866 | $item['entity_id'] = $id; | |
1867 | } | |
1868 | // tax integration | |
1869 | if (empty($item['contribution_id'])) { | |
1870 | $item['contribution_id'] = $id; | |
1871 | } | |
1872 | // Membership | |
1873 | if (!empty($item['membership_id'])) { | |
1874 | $item['entity_id'] = $item['membership_id']; | |
1875 | $lineItemArray = wf_civicrm_api('LineItem', 'get', array( | |
1876 | 'entity_table' => "civicrm_membership", | |
1877 | 'entity_id' => $item['entity_id'], | |
1878 | )); | |
1879 | if ($lineItemArray['count'] != 0) { | |
1880 | // We only require first membership (signup) entry to make this work. | |
1881 | $firstLineItem = array_shift($lineItemArray['values']); | |
1882 | ||
1883 | // Membership signup line item entry. | |
1884 | // Line Item record is already present for membership by this stage. | |
1885 | // Just need to upgrade contribution_id column in the record. | |
1886 | if (!isset($firstLineItem['contribution_id'])) { | |
1887 | $item['id'] = $firstLineItem['id']; | |
1888 | } | |
1889 | } | |
1890 | } | |
1891 | ||
1892 | $line_result = wf_civicrm_api('line_item', 'create', $item); | |
1893 | $item['id'] = $line_result['id']; | |
1894 | ||
1895 | $lineItemRecord = json_decode(json_encode($item), FALSE); | |
1896 | // Add financial_item and entity_financial_trxn | |
1897 | $result = CRM_Financial_BAO_FinancialItem::add($lineItemRecord, $contributionResult); | |
1898 | if (!is_null($this->tax_rate)) { | |
1899 | $result = CRM_Financial_BAO_FinancialItem::add($lineItemRecord, $contributionResult, TRUE); | |
1900 | } | |
1901 | // Create participant/membership payment records | |
1902 | if (isset($item['membership_id']) || isset($item['participant_id'])) { | |
1903 | $type = isset($item['participant_id']) ? 'participant' : 'membership'; | |
1904 | wf_civicrm_api("{$type}_payment", 'create', array( | |
1905 | "{$type}_id" => $item["{$type}_id"], | |
1906 | 'contribution_id' => $id, | |
1907 | )); | |
1908 | } | |
1909 | } | |
1910 | } | |
1911 | ||
1912 | /** | |
1913 | * @param string $ent - entity type | |
1914 | * @param int $n - entity number | |
1915 | * @param int $id - entity id | |
1916 | * @param bool $new - is this a new object? (should we bother checking for existing data) | |
1917 | */ | |
1918 | private function processAttachments($ent, $n, $id, $new = FALSE) { | |
1919 | $attachments = $new ? array() : $this->getAttachments($ent, $id); | |
1920 | foreach ((array) wf_crm_aval($this->data[$ent], "$n:{$ent}upload:1") as $num => $file_id) { | |
1921 | if ($file_id) { | |
1922 | list(, $i) = explode('_', $num); | |
1923 | $dao = new CRM_Core_DAO_EntityFile(); | |
1924 | if (!empty($attachments[$i])) { | |
1925 | $dao->id = $attachments[$i]['id']; | |
1926 | } | |
1927 | $dao->file_id = $file_id; | |
1928 | $dao->entity_id = $id; | |
1929 | $dao->entity_table = "civicrm_$ent"; | |
1930 | $dao->save(); | |
1931 | } | |
1932 | } | |
1933 | } | |
1934 | ||
1935 | /** | |
1936 | * Recursive function to fill ContactRef fields with contact IDs | |
1937 | * | |
1938 | * @internal param $values null|array | |
1939 | * Leave blank - used internally to recurse through data | |
1940 | * @internal param $depth int | |
1941 | * Leave blank - used internally to track recursion level | |
1942 | */ | |
1943 | private function fillContactRefs($values = NULL, $depth = 0) { | |
1944 | $order = array('ent', 'c', 'table', 'n', 'name'); | |
1945 | static $ent = ''; | |
1946 | static $c = ''; | |
1947 | static $table = ''; | |
1948 | static $n = ''; | |
1949 | if ($values === NULL) { | |
1950 | $values = $this->data; | |
1951 | } | |
1952 | foreach ($values as $key => $val) { | |
1953 | ${$order[$depth]} = $key; | |
1954 | if ($depth < 4 && is_array($val)) { | |
1955 | $this->fillContactRefs($val, $depth + 1); | |
1956 | } | |
1957 | elseif ($depth == 4 && $val && wf_crm_aval($this->all_fields, "{$table}_$name:data_type") == 'ContactReference') { | |
1958 | if (is_array($val)) { | |
1959 | $this->data[$ent][$c][$table][$n][$name] = array(); | |
1960 | foreach ($val as $v) { | |
1961 | if (is_numeric($v) && !empty($this->ent['contact'][$v]['id'])) { | |
1962 | $this->data[$ent][$c][$table][$n][$name][] = $this->ent['contact'][$v]['id']; | |
1963 | } | |
1964 | } | |
1965 | } | |
1966 | else { | |
1967 | unset($this->data[$ent][$c][$table][$n][$name]); | |
1968 | if (!empty($this->ent['contact'][$val]['id'])) { | |
1969 | $this->data[$ent][$c][$table][$n][$name] = $this->ent['contact'][$val]['id']; | |
1970 | } | |
1971 | } | |
1972 | } | |
1973 | } | |
1974 | } | |
1975 | ||
1976 | /** | |
1977 | * Fill data array with submitted form values | |
1978 | */ | |
1979 | private function fillDataFromSubmission() { | |
1980 | foreach ($this->enabled as $field_key => $fid) { | |
1981 | $val = $this->submissionValue($fid); | |
1982 | // If value is null then it was hidden by a webform conditional rule - skip it | |
1983 | if ($val !== NULL && $val !== array(NULL)) { | |
1984 | list( , $c, $ent, $n, $table, $name) = explode('_', $field_key, 6); | |
1985 | // Fieldsets and existing contact fields are not strictly CiviCRM fields, so ignore | |
1986 | if ($name === 'existing' || $name === 'fieldset') { | |
1987 | continue; | |
1988 | } | |
1989 | // Ignore values from fields hidden by existing contact component | |
1990 | if ($this->isFieldHiddenByExistingContactSettings($ent, $c, $table, $n, $name)) { | |
1991 | // Also remove the value from the webform submission | |
1992 | $this->submissionValue($fid, array(NULL)); | |
1993 | continue; | |
1994 | } | |
1995 | $field = $this->all_fields[$table . '_' . $name]; | |
1996 | $component = $this->node->webform['components'][$this->enabled[$field_key]]; | |
1997 | // Ignore values from hidden fields | |
1998 | if ($field['type'] == 'hidden') { | |
1999 | continue; | |
2000 | } | |
2001 | // Translate privacy options into separate values | |
2002 | if ($name === 'privacy') { | |
2003 | foreach (array_keys($this->getExposedOptions($field_key)) as $key) { | |
2004 | $this->data[$ent][$c][$table][$n][$key] = in_array($key, $val); | |
2005 | } | |
2006 | continue; | |
2007 | } | |
2008 | $dataType = wf_crm_aval($field, 'data_type'); | |
2009 | if (!empty($field['extra']['multiple'])) { | |
2010 | if ($val === array('')) { | |
2011 | $val = array(); | |
2012 | } | |
2013 | // Merge with existing data | |
2014 | if (!empty($this->data[$ent][$c][$table][$n][$name]) && is_array($this->data[$ent][$c][$table][$n][$name])) { | |
2015 | $val = array_unique(array_merge($val, $this->data[$ent][$c][$table][$n][$name])); | |
2016 | } | |
2017 | // Implode data that will be stored as a string | |
2018 | if ($table !== 'other' && $name !== 'event_id' && $name !== 'relationship_type_id' && $table !== 'contact' && $dataType != 'ContactReference') { | |
2019 | $val = CRM_Utils_Array::implodePadded($val); | |
2020 | } | |
2021 | } | |
2022 | elseif ($name === 'image_URL') { | |
2023 | if (empty($val[0]) || !($val = $this->getDrupalFileUrl($val[0]))) { | |
2024 | // This field can't be emptied due to the nature of file uploads | |
2025 | continue; | |
2026 | } | |
2027 | } | |
2028 | elseif ($dataType == 'File') { | |
2029 | if (empty($val[0]) || !($val = $this->saveDrupalFileToCivi($val[0]))) { | |
2030 | // This field can't be emptied due to the nature of file uploads | |
2031 | continue; | |
2032 | } | |
2033 | } | |
2034 | elseif ($field['type'] === 'date') { | |
2035 | $val = empty($val[0]) ? '' : str_replace('-', '', $val[0]); | |
2036 | // Add time field value | |
2037 | $time = wf_crm_aval($this->data, "$ent:$c:$table:$n:$name", ''); | |
2038 | // Remove default date if it has been added | |
2039 | if (strlen($time) == 14) { | |
2040 | $time = substr($time, -6); | |
2041 | } | |
2042 | $val .= $time; | |
2043 | } | |
2044 | // The admin can change a number field to use checkbox/radio/select/grid widget and we'll sum the result | |
2045 | elseif ($field['type'] === 'number') { | |
2046 | $sum = 0; | |
2047 | foreach ((array) $val as $k => $v) { | |
2048 | // Perform multiplication across grid elements | |
2049 | if ($component['type'] == 'grid' && is_numeric($k)) { | |
2050 | $v = $v * $k; | |
2051 | } | |
2052 | if (is_numeric($v)) { | |
2053 | $sum += $v; | |
2054 | } | |
2055 | } | |
2056 | // We don't allow negative payments | |
2057 | $val = $sum < 0 ? 0 : $sum; | |
2058 | } | |
2059 | else { | |
2060 | $val = isset($val[0]) ? $val[0] : ''; | |
2061 | } | |
2062 | // Fudge together date and time fields | |
2063 | if ($field['type'] === 'time' && substr($name, -8) === 'timepart') { | |
2064 | $name = str_replace('_timepart', '', $name); | |
2065 | // Add date (default to today) | |
2066 | $date = wf_crm_aval($this->data, "$ent:$c:$table:$n:$name", date('Ymd')); | |
2067 | $val = $date . str_replace(':', '', $val); | |
2068 | } | |
2069 | // Only known contacts are allowed to empty a field | |
2070 | if (($val !== '' && $val !== NULL && $val !== array()) || !empty($this->existing_contacts[$c])) { | |
2071 | $this->data[$ent][$c][$table][$n][$name] = $val; | |
2072 | } | |
2073 | } | |
2074 | } | |
2075 | } | |
2076 | ||
2077 | /** | |
2078 | * Test whether a field has been hidden due to existing contact settings | |
2079 | * @param $ent | |
2080 | * @param $c | |
2081 | * @param $table | |
2082 | * @param $n | |
2083 | * @param $name | |
2084 | * @return bool | |
2085 | */ | |
2086 | private function isFieldHiddenByExistingContactSettings($ent, $c, $table, $n, $name) { | |
2087 | if ($ent == 'contact' && isset($this->enabled["civicrm_{$c}_contact_1_contact_existing"])) { | |
2088 | $component = $this->getComponent("civicrm_{$c}_contact_1_contact_existing"); | |
2089 | $existing_contact_val = $this->submissionValue($component['cid']); | |
2090 | // Fields are hidden if value is empty (no selection) or a numeric contact id | |
2091 | if (!$existing_contact_val[0] || is_numeric($existing_contact_val[0])) { | |
2092 | $type = ($table == 'contact' && strpos($name, 'name')) ? 'name' : $table; | |
2093 | if (in_array($type, $component['extra']['hide_fields'])) { | |
2094 | // With the no_hide_blank setting we must load the contact to determine if the field was hidden | |
2095 | if (wf_crm_aval($component['extra'], 'no_hide_blank')) { | |
2096 | $value = wf_crm_aval($this->loadContact($c), "$table:$n:$name"); | |
2097 | return !(!$value && !is_numeric($value)); | |
2098 | } | |
2099 | else { | |
2100 | return TRUE; | |
2101 | } | |
2102 | } | |
2103 | } | |
2104 | } | |
2105 | return FALSE; | |
2106 | } | |
2107 | ||
2108 | /** | |
2109 | * Test if any relevant data has been entered for a location | |
2110 | * @param string $location | |
2111 | * @param array $params | |
2112 | * @return bool | |
2113 | */ | |
2114 | private function locationIsEmpty($location, $params) { | |
2115 | switch ($location) { | |
2116 | case 'address': | |
2117 | return empty($params['street_address']) | |
2118 | && empty($params['city']) | |
2119 | && empty($params['state_province_id']) | |
2120 | && empty($params['country_id']) | |
2121 | && empty($params['postal_code']) | |
2122 | && (empty($params['master_id']) || $params['master_id'] == 'null'); | |
2123 | case 'website': | |
2124 | return empty($params['url']); | |
2125 | case 'im': | |
2126 | return empty($params['name']); | |
2127 | default: | |
2128 | return empty($params[$location]); | |
2129 | } | |
2130 | } | |
2131 | ||
2132 | /** | |
2133 | * Clears an error against a form element. | |
2134 | * Used to disable validation when this module hides a field | |
2135 | * @see https://api.drupal.org/comment/49163#comment-49163 | |
2136 | * | |
2137 | * @param $name string | |
2138 | */ | |
2139 | private function unsetError($name) { | |
2140 | $errors = &drupal_static('form_set_error', array()); | |
2141 | $removed_messages = array(); | |
2142 | if (isset($errors[$name])) { | |
2143 | $removed_messages[] = $errors[$name]; | |
2144 | unset($errors[$name]); | |
2145 | } | |
2146 | $_SESSION['messages']['error'] = array_diff($_SESSION['messages']['error'], $removed_messages); | |
2147 | if (empty($_SESSION['messages']['error'])) { | |
2148 | unset($_SESSION['messages']['error']); | |
2149 | } | |
2150 | } | |
2151 | ||
2152 | /** | |
2153 | * Get or set a value from a webform submission | |
2154 | * During validation phase we use $this->crmValues | |
2155 | * During submission processing we use $this->submission | |
2156 | * | |
2157 | * @param $fid | |
2158 | * Numeric webform component id | |
2159 | * @param $value | |
2160 | * Value to set - leave empty to get a value rather than setting it | |
2161 | * | |
2162 | * @return array|null field value if found | |
2163 | */ | |
2164 | protected function submissionValue($fid, $value = NULL) { | |
2165 | // In submission processing context | |
2166 | if ($this->submission) { | |
2167 | if (!isset($this->submission->data[$fid])) { | |
2168 | return NULL; | |
2169 | } | |
2170 | $field =& $this->submission->data[$fid]; | |
2171 | // During submission preprocessing this is used to alter the submission | |
2172 | if ($value !== NULL) { | |
2173 | $field = (array) $value; | |
2174 | } | |
2175 | return $field; | |
2176 | } | |
2177 | // In validation context - no need to ever change the value so just return it | |
2178 | else { | |
2179 | if (!isset($this->rawValues[$fid])) { | |
2180 | return NULL; | |
2181 | } | |
2182 | // rawValues is slightly different from submission->data in that empty values are present in arrays | |
2183 | if (is_array($this->rawValues[$fid])) { | |
2184 | $val = array_filter($this->rawValues[$fid]); | |
2185 | return $val; | |
2186 | } | |
2187 | else { | |
2188 | return (array) $this->rawValues[$fid]; | |
2189 | } | |
2190 | } | |
2191 | } | |
2192 | ||
2193 | /** | |
2194 | * Identifies contact 1 as acting user for CiviCRM's advanced logging | |
2195 | */ | |
2196 | public function setLoggingContact() { | |
2197 | if (!empty($this->ent['contact'][1]['id']) && user_is_anonymous()) { | |
2198 | CRM_Core_DAO::executeQuery('SET @civicrm_user_id = %1', array(1 => array($this->ent['contact'][1]['id'], 'Integer'))); | |
2199 | } | |
2200 | } | |
2201 | ||
2202 | /** | |
2203 | * reorder submitted location values according to existing location values | |
2204 | * | |
2205 | * @param array $submittedLocationValues | |
2206 | * @param array $existingLocationValues | |
2207 | * @param string $entity | |
2208 | * @return array | |
2209 | */ | |
2210 | protected function reorderLocationValues($submittedLocationValues, $existingLocationValues, $entity) { | |
2211 | $reorderedArray = array(); | |
2212 | $index = 1; | |
2213 | $entityTypeIdIndex = $entity.'_type_id'; | |
2214 | $entity = $entity == 'website' ? 'url' : $entity; // for website only | |
2215 | ||
2216 | foreach ($existingLocationValues as $eValue) { | |
2217 | $existingLocationTypeId = $entity != 'url' ? $eValue['location_type_id'] : NULL; | |
2218 | $existingEntityTypeId = isset($eValue[$entityTypeIdIndex]) ? $eValue[$entityTypeIdIndex] : NULL; | |
2219 | ||
2220 | if (!empty($existingEntityTypeId)) { | |
2221 | $reorderedArray[$index][$entityTypeIdIndex] = $existingEntityTypeId; | |
2222 | } | |
2223 | elseif (!empty($existingLocationTypeId)) { | |
2224 | $reorderedArray[$index]['location_type_id'] = $existingLocationTypeId; | |
2225 | } | |
2226 | ||
2227 | $reorderedArray[$index][$entity] = $eValue[$entity]; | |
2228 | ||
2229 | // address field contain many sub fields and should be handled differently | |
2230 | if ($entity != 'address') { | |
2231 | $submittedLocationValues = self::unsetEmptyValueIndexes($submittedLocationValues, $entity); | |
2232 | $reorderedArray[$index][$entity] = $eValue[$entity]; | |
2233 | } | |
2234 | else { | |
2235 | foreach (wf_crm_address_fields() as $field) { | |
2236 | $reorderedArray[$index][$field] = isset($eValue[$field]) ? $eValue[$field] : ''; | |
2237 | } | |
2238 | // handle supplemental addresses | |
2239 | $subAddressIndex = 1; | |
2240 | $subAddField = 'supplemental_address_' . $subAddressIndex; | |
2241 | while(isset($eValue[$subAddField])) { | |
2242 | $reorderedArray[$index][$subAddField] = $eValue[$subAddField]; | |
2243 | $subAddField = 'supplemental_address_' . ++$subAddressIndex; | |
2244 | } | |
2245 | } | |
2246 | ||
2247 | foreach ($submittedLocationValues as $key => $sValue) { | |
2248 | $sLocationTypeId = isset($sValue['location_type_id']) ? $sValue['location_type_id'] : NULL; | |
2249 | $sEntityTypeId = isset($sValue[$entityTypeIdIndex]) ? $sValue[$entityTypeIdIndex] : NULL; | |
2250 | ||
2251 | if (($existingLocationTypeId == $sLocationTypeId && empty($sEntityTypeId)) | |
2252 | || | |
2253 | ($existingEntityTypeId == $sEntityTypeId && empty($sLocationTypeId)) | |
2254 | || | |
2255 | ($existingLocationTypeId == $sLocationTypeId && $existingEntityTypeId == $sEntityTypeId)) { | |
2256 | ||
2257 | // address field contain many sub fields and should be handled differently | |
2258 | if ($entity != 'address') { | |
2259 | $reorderedArray[$index][$entity] = $sValue[$entity]; | |
2260 | } | |
2261 | else { | |
2262 | foreach (wf_crm_address_fields() as $field) { | |
2263 | if (isset($sValue[$field])) { | |
2264 | $reorderedArray[$index][$field] = $sValue[$field]; | |
2265 | } | |
2266 | } | |
2267 | // handle supplemental addresses | |
2268 | $subAddressIndex = 1; | |
2269 | $subAddField = 'supplemental_address_' . $subAddressIndex; | |
2270 | while(isset($sValue[$subAddField])) { | |
2271 | $reorderedArray[$index][$subAddField] = $sValue[$subAddField]; | |
2272 | $subAddField = 'supplemental_address_' . ++$subAddressIndex; | |
2273 | } | |
2274 | } | |
2275 | unset($submittedLocationValues[$key]); | |
2276 | } | |
2277 | } | |
2278 | $index++; | |
2279 | } | |
2280 | ||
2281 | // handle remaining values | |
2282 | if (!empty($submittedLocationValues)) { | |
2283 | // cannot use array_push, array_merge because preserving array keys is important | |
2284 | foreach ($submittedLocationValues as $sValue) { | |
2285 | $reorderedArray[] = $sValue; | |
2286 | } | |
2287 | } | |
2288 | ||
2289 | return $reorderedArray; | |
2290 | } | |
2291 | ||
2292 | private function unsetEmptyValueIndexes($values, $entity) { | |
2293 | foreach ($values as $k => $v) { | |
2294 | if (!isset($v[$entity])) { | |
2295 | unset($values[$k]); | |
2296 | } | |
2297 | } | |
2298 | ||
2299 | return $values; | |
2300 | } | |
2301 | } |