commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-old / webform_civicrm / includes / wf_crm_webform_postprocess.inc
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 // Email
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 }