c87d52f02d3e3c027e54c2a19fa6496bc4033412
[civicrm-core.git] / CRM / Core / Payment / GoogleIPN.php
1 <?php
2
3 /**
4 * Copyright (C) 2006 Google Inc.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 /* This is the response handler code that will be invoked every time
20 * a notification or request is sent by the Google Server
21 *
22 * To allow this code to receive responses, the url for this file
23 * must be set on the seller page under Settings->Integration as the
24 * "API Callback URL'
25 * Order processing commands can be sent automatically by placing these
26 * commands appropriately
27 *
28 * To use this code for merchant-calculated feedback, this url must be
29 * set also as the merchant-calculations-url when the cart is posted
30 * Depending on your calculations for shipping, taxes, coupons and gift
31 * certificates update parts of the code as required
32 *
33 */
34
35
36
37 define('GOOGLE_DEBUG_PP', 0);
38 class CRM_Core_Payment_GoogleIPN extends CRM_Core_Payment_BaseIPN {
39
40 /**
41 * We only need one instance of this object. So we use the singleton
42 * pattern and cache the instance in this variable
43 *
44 * @var object
45 * @static
46 */
47 static private $_singleton = NULL;
48
49 /**
50 * mode of operation: live or test
51 *
52 * @var object
53 */
54 protected $_mode = NULL;
55
56 static function retrieve($name, $type, $object, $abort = TRUE) {
57 $value = CRM_Utils_Array::value($name, $object);
58 if ($abort && $value === NULL) {
59 CRM_Core_Error::debug_log_message("Could not find an entry for $name");
60 echo "Failure: Missing Parameter<p>";
61 exit();
62 }
63
64 if ($value) {
65 if (!CRM_Utils_Type::validate($value, $type)) {
66 CRM_Core_Error::debug_log_message("Could not find a valid entry for $name");
67 echo "Failure: Invalid Parameter<p>";
68 exit();
69 }
70 }
71
72 return $value;
73 }
74
75 /**
76 * Constructor
77 *
78 * @param string $mode the mode of operation: live or test
79 *
80 * @param $paymentProcessor
81 *
82 * @return \CRM_Core_Payment_GoogleIPN
83 */
84 function __construct($mode, &$paymentProcessor) {
85 parent::__construct();
86
87 $this->_mode = $mode;
88 $this->_paymentProcessor = $paymentProcessor;
89 }
90
91 /**
92 * The function gets called when a new order takes place.
93 *
94 * @param xml $dataRoot response send by google in xml format
95 * @param array $privateData contains the name value pair of <merchant-private-data>
96 *
97 * @param $component
98 *
99 * @return void
100 */
101 function newOrderNotify($dataRoot, $privateData, $component) {
102 $ids = $input = $params = array();
103
104 $input['component'] = strtolower($component);
105
106 $ids['contact'] = self::retrieve('contactID', 'Integer', $privateData, TRUE);
107 $ids['contribution'] = self::retrieve('contributionID', 'Integer', $privateData, TRUE);
108
109 $ids['contributionRecur'] = $ids['contributionPage'] = NULL;
110 if ($input['component'] == "event") {
111 $ids['event'] = self::retrieve('eventID', 'Integer', $privateData, TRUE);
112 $ids['participant'] = self::retrieve('participantID', 'Integer', $privateData, TRUE);
113 $ids['membership'] = NULL;
114 }
115 else {
116 $ids['membership'] = self::retrieve('membershipID', 'Integer', $privateData, FALSE);
117 $ids['related_contact'] = self::retrieve('relatedContactID', 'Integer', $privateData, FALSE);
118 $ids['onbehalf_dupe_alert'] = self::retrieve('onBehalfDupeAlert', 'Integer', $privateData, FALSE);
119 $ids['contributionRecur'] = self::retrieve('contributionRecurID', 'Integer', $privateData, FALSE);
120 }
121
122 $paymentProcessorID = CRM_Core_DAO::getFieldValue(
123 'CRM_Financial_DAO_PaymentProcessorType',
124 'Google_Checkout',
125 'id',
126 'payment_processor_type'
127 );
128
129 if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) {
130 return FALSE;
131 }
132
133 $input['invoice'] = $privateData['invoiceID'];
134 $input['newInvoice'] = $dataRoot['google-order-number']['VALUE'];
135
136 if ($ids['contributionRecur']) {
137 if ($objects['contributionRecur']->invoice_id == $dataRoot['serial-number']) {
138 CRM_Core_Error::debug_log_message("The new order notification already handled: {$dataRoot['serial-number']}.");
139 return;
140 }
141 else {
142 $transaction = new CRM_Core_Transaction();
143
144 CRM_Core_Error::debug_log_message("New order for an installment received.");
145 $recur = &$objects['contributionRecur'];
146
147 // fix dates that already exist
148 $dates = array('create', 'start', 'end', 'cancel', 'modified');
149 foreach ($dates as $date) {
150 $name = "{$date}_date";
151 if ($recur->$name) {
152 $recur->$name = CRM_Utils_Date::isoToMysql($recur->$name);
153 }
154 }
155 $recur->invoice_id = $dataRoot['serial-number'];
156 $recur->processor_id = $input['newInvoice'];
157 $recur->save();
158
159 if ($objects['contribution']->contribution_status_id == 1) {
160 // create a contribution and then get it processed
161 $contribution = new CRM_Contribute_DAO_Contribution();
162 $contribution->contact_id = $ids['contact'];
163 $contribution->financial_type_id = $objects['contributionType']->id;
164 $contribution->contribution_page_id = $objects['contribution']->contribution_page_id;
165 $contribution->contribution_recur_id = $ids['contributionRecur'];
166 $contribution->receive_date = date('YmdHis');
167 $contribution->currency = $objects['contribution']->currency;
168 $contribution->payment_instrument_id = $objects['contribution']->payment_instrument_id;
169 $contribution->amount_level = $objects['contribution']->amount_level;
170 $contribution->address_id = $objects['contribution']->address_id;
171 $contribution->invoice_id = $input['invoice'];
172 $contribution->total_amount = $dataRoot['order-total']['VALUE'];
173 $contribution->contribution_status_id = 2;
174 $contribution->campaign_id = $objects['contribution']->campaign_id;
175
176 $objects['contribution'] = $contribution;
177 }
178 $transaction->commit();
179 }
180 }
181
182 // make sure the invoice is valid and matches what we have in the contribution record
183 $contribution = &$objects['contribution'];
184
185 if ($contribution->invoice_id != $input['invoice']) {
186 CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request");
187 return;
188 }
189
190 // lets replace invoice-id with google-order-number because thats what is common and unique
191 // in subsequent calls or notifications sent by google.
192 $contribution->invoice_id = $input['newInvoice'];
193
194 $input['amount'] = $dataRoot['order-total']['VALUE'];
195
196 if ($contribution->total_amount != $input['amount']) {
197 CRM_Core_Error::debug_log_message("Amount values dont match between database and IPN request");
198 return;
199 }
200
201 if (!$this->getInput($input, $ids, $dataRoot)) {
202 return FALSE;
203 }
204
205 $transaction = new CRM_Core_Transaction();
206
207 // check if contribution is already completed, if so we ignore this ipn
208 if ($contribution->contribution_status_id == 1) {
209 CRM_Core_Error::debug_log_message("returning since contribution has already been handled");
210 return;
211 }
212 else {
213 /* Since trxn_id hasn't got any use here,
214 * lets make use of it by passing the eventID/membershipTypeID to next level.
215 * And change trxn_id to google-order-number before finishing db update */
216
217 if (!empty($ids['event'])) {
218 $contribution->trxn_id = $ids['event'] . CRM_Core_DAO::VALUE_SEPARATOR . $ids['participant'];
219 }
220 elseif (!empty($ids['membership'])) {
221 $contribution->trxn_id = $ids['membership'][0] . CRM_Core_DAO::VALUE_SEPARATOR . $ids['related_contact'] . CRM_Core_DAO::VALUE_SEPARATOR . $ids['onbehalf_dupe_alert'];
222 }
223 }
224
225 // CRM_Core_Error::debug_var( 'c', $contribution );
226 $contribution->save();
227 $transaction->commit();
228
229 return TRUE;
230 }
231
232 /**
233 * The function gets called when the state(CHARGED, CANCELLED..) changes for an order
234 *
235 * @param string $status status of the transaction send by google
236 * @param array $privateData contains the name value pair of <merchant-private-data>
237 *
238 * @return void
239 *
240 */
241 function orderStateChange($status, $dataRoot, $privateData, $component) {
242 $input = $objects = $ids = array();
243 $input['component'] = strtolower($component);
244
245 $ids['contributionRecur'] = self::retrieve('contributionRecurID', 'Integer', $privateData, FALSE);
246 $serial = $dataRoot['serial-number'];
247 $orderNo = $dataRoot['google-order-number']['VALUE'];
248
249 $contribution = new CRM_Contribute_BAO_Contribution();
250 $contribution->invoice_id = $orderNo;
251
252 if (!$contribution->find(TRUE)) {
253 CRM_Core_Error::debug_log_message("orderStateChange: Could not find contribution record with invoice id: $serial");
254 return;
255 }
256
257 // Google sends the charged notification twice.
258 // So to make sure, code is not executed again.
259 if ($contribution->contribution_status_id == 1) {
260 CRM_Core_Error::debug_log_message("Contribution already handled (ContributionID = {$contribution->id}).");
261 return;
262 }
263
264 // make sure invoice is set to serial no for recurring payments, to avoid violating uniqueness
265 $contribution->invoice_id = $ids['contributionRecur'] ? $serial : $orderNo;
266
267 $objects['contribution'] = &$contribution;
268 $ids['contribution'] = $contribution->id;
269 $ids['contact'] = $contribution->contact_id;
270
271 $ids['event'] = $ids['participant'] = $ids['membership'] = NULL;
272 $ids['contributionPage'] = NULL;
273
274 if ($input['component'] == "event") {
275 list($ids['event'], $ids['participant']) = explode(CRM_Core_DAO::VALUE_SEPARATOR,
276 $contribution->trxn_id
277 );
278 }
279 else {
280 $ids['related_contact'] = NULL;
281 $ids['onbehalf_dupe_alert'] = NULL;
282 if ($contribution->trxn_id) {
283 list($ids['membership'], $ids['related_contact'], $ids['onbehalf_dupe_alert']) = explode(CRM_Core_DAO::VALUE_SEPARATOR,
284 $contribution->trxn_id
285 );
286 }
287 foreach (array(
288 'membership', 'related_contact', 'onbehalf_dupe_alert') as $fld) {
289 if (!is_numeric($ids[$fld])) {
290 unset($ids[$fld]);
291 }
292 }
293 }
294
295 $paymentProcessorID = CRM_Core_DAO::getFieldValue(
296 'CRM_Financial_DAO_PaymentProcessorType',
297 'Google_Checkout',
298 'id',
299 'payment_processor_type'
300 );
301
302 $this->loadObjects($input, $ids, $objects, TRUE, $paymentProcessorID);
303
304 $transaction = new CRM_Core_Transaction();
305
306 // CRM_Core_Error::debug_var( 'c', $contribution );
307 if ($status == 'PAYMENT_DECLINED' ||
308 $status == 'CANCELLED_BY_GOOGLE' ||
309 $status == 'CANCELLED'
310 ) {
311 return $this->failed($objects, $transaction);
312 }
313
314 $input['amount'] = $contribution->total_amount;
315 $input['fee_amount'] = NULL;
316 $input['net_amount'] = NULL;
317 $input['trxn_id'] = $ids['contributionRecur'] ? $serial : $dataRoot['google-order-number']['VALUE'];
318 $input['is_test'] = $contribution->is_test;
319
320 $recur = NULL;
321 if ($ids['contributionRecur']) {
322 $recur = $objects['contributionRecur'];
323 }
324 $this->completeTransaction($input, $ids, $objects, $transaction, $recur);
325
326 $this->completeRecur($input, $ids, $objects);
327 }
328
329 function completeRecur($input, $ids, $objects) {
330 if ($ids['contributionRecur']) {
331 $recur = &$objects['contributionRecur'];
332 $contributionCount = CRM_Core_DAO::singleValueQuery("
333 SELECT count(*)
334 FROM civicrm_contribution
335 WHERE contribution_recur_id = {$ids['contributionRecur']}
336 ");
337 $autoRenewMembership = FALSE;
338 if ($recur->id &&
339 isset($ids['membership']) &&
340 $ids['membership']
341 ) {
342 $autoRenewMembership = TRUE;
343 }
344 if ($recur->installments && ($contributionCount >= $recur->installments)) {
345 $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
346
347 $recur->create_date = CRM_Utils_Date::isoToMysql($recur->create_date);
348 $recur->start_date = CRM_Utils_Date::isoToMysql($recur->start_date);
349 $recur->cancel_date = CRM_Utils_Date::isoToMysql($recur->cancel_date);
350 $recur->end_date = date('YmdHis');
351 $recur->modified_date = date('YmdHis');
352 $recur->contribution_status_id = array_search('Completed', $contributionStatus);
353 $recur->trnx_id = $dataRoot['google-order-number']['VALUE'];
354 $recur->save();
355
356 //send recurring Notification email for user
357 CRM_Contribute_BAO_ContributionPage::recurringNotify(
358 CRM_Core_Payment::RECURRING_PAYMENT_END,
359 $ids['contact'],
360 $ids['contributionPage'],
361 $recur,
362 $autoRenewMembership
363 );
364 }
365 elseif ($contributionCount == 1) {
366 CRM_Contribute_BAO_ContributionPage::recurringNotify(
367 CRM_Core_Payment::RECURRING_PAYMENT_START,
368 $ids['contact'],
369 $ids['contributionPage'],
370 $recur,
371 $autoRenewMembership
372 );
373 }
374 }
375 }
376
377 /**
378 * singleton function used to manage this object
379 *
380 * @param string $mode the mode of operation: live or test
381 *
382 * @return object
383 * @static
384 */
385 static function &singleton($mode, $component, &$paymentProcessor) {
386 if (self::$_singleton === NULL) {
387 self::$_singleton = new CRM_Core_Payment_GoogleIPN($mode, $paymentProcessor);
388 }
389 return self::$_singleton;
390 }
391
392 /**
393 * The function retrieves the amount the contribution is for, based on the order-no google sends
394 *
395 * @param int $orderNo <order-total> send by google
396 *
397 * @return amount
398 * @access public
399 */
400 function getAmount($orderNo) {
401 $contribution = new CRM_Contribute_DAO_Contribution();
402 $contribution->invoice_id = $orderNo;
403 if (!$contribution->find(TRUE)) {
404 CRM_Core_Error::debug_log_message("getAmount: Could not find contribution record with invoice id: $orderNo");
405 echo "Failure: Could not find contribution record with invoice id: $orderNo <p>";
406 exit();
407 }
408 return $contribution->total_amount;
409 }
410
411 /**
412 * The function returns the component(Event/Contribute..), given the google-order-no and merchant-private-data
413 *
414 * @param xml $xml_response response send by google in xml format
415 * @param array $privateData contains the name value pair of <merchant-private-data>
416 * @param int $orderNo <order-total> send by google
417 * @param string $root root of xml-response
418 *
419 * @return array context of this call (test, module, payment processor id)
420 * @static
421 */
422 function getContext($privateData, $orderNo, $root, $response, $serial) {
423 $contributionID = CRM_Utils_Array::value('contributionID', $privateData);
424 $contribution = new CRM_Contribute_DAO_Contribution();
425 if ($root == 'new-order-notification') {
426 $contribution->id = $contributionID;
427 }
428 else {
429 $contribution->invoice_id = $orderNo;
430 }
431 if (!$contribution->find(TRUE)) {
432 CRM_Core_Error::debug_log_message("getContext: Could not find contribution record with invoice id: $orderNo");
433 $response->SendAck($serial);
434 }
435
436 $module = 'Contribute';
437 if (stristr($contribution->source, ts('Online Contribution'))) {
438 $module = 'Contribute';
439 }
440 elseif (stristr($contribution->source, ts('Online Event Registration'))) {
441 $module = 'Event';
442 }
443 $isTest = $contribution->is_test;
444
445 $ids = $input = $objects = array();
446 $objects['contribution'] = &$contribution;
447 $ids['contributionRecur'] = self::retrieve('contributionRecurID', 'Integer', $privateData, FALSE);
448 $input['component'] = strtolower($module);
449
450 if (!$ids['contributionRecur'] && $contribution->contribution_status_id == 1) {
451 CRM_Core_Error::debug_log_message("Contribution already handled (ContributionID = {$contribution->id}).");
452 // There is no point in going further. Return ack so we don't receive the same ipn.
453 $response->SendAck($serial);
454 }
455
456 if ($input['component'] == 'event') {
457 if ($root == 'new-order-notification') {
458 $ids['event'] = $privateData['eventID'];
459 }
460 else {
461 list($ids['event'], $ids['participant']) =
462 explode(CRM_Core_DAO::VALUE_SEPARATOR, $contribution->trxn_id);
463 }
464 }
465
466 $paymentProcessorID = CRM_Core_DAO::getFieldValue(
467 'CRM_Financial_DAO_PaymentProcessor',
468 'Google_Checkout',
469 'id',
470 'payment_processor_type'
471 );
472
473 $this->loadObjects($input, $ids, $objects, FALSE, $paymentProcessorID);
474
475 if (!$ids['paymentProcessor']) {
476 CRM_Core_Error::debug_log_message("Payment processor could not be retrieved.");
477 // There is no point in going further. Return ack so we don't receive the same ipn.
478 $response->SendAck($serial);
479 }
480
481 return array($isTest, $input['component'], $ids['paymentProcessor']);
482 }
483
484 /**
485 * This method is handles the response that will be invoked (from extern/googleNotify) every time
486 * a notification or request is sent by the Google Server.
487 *
488 */
489 static function main($xml_response) {
490 require_once 'Google/library/googleresponse.php';
491 require_once 'Google/library/googlerequest.php';
492 require_once 'Google/library/googlemerchantcalculations.php';
493 require_once 'Google/library/googleresult.php';
494 require_once 'Google/library/xml-processing/gc_xmlparser.php';
495
496 $config = CRM_Core_Config::singleton();
497
498 // Retrieve the XML sent in the HTTP POST request to the ResponseHandler
499 if (get_magic_quotes_gpc()) {
500 $xml_response = stripslashes($xml_response);
501 }
502
503 $headers = CRM_Utils_System::getAllHeaders();
504
505 if (GOOGLE_DEBUG_PP) {
506 CRM_Core_Error::debug_var('RESPONSE', $xml_response, TRUE, TRUE, 'Google');
507 }
508
509 // Retrieve the root and data from the xml response
510 $response = new GoogleResponse();
511 list($root, $data) = $response->GetParsedXML($xml_response);
512 // lets retrieve the private-data & order-no
513 $privateData = NULL;
514 if (array_key_exists('shopping-cart', $data[$root])) {
515 $privateData = $data[$root]['shopping-cart']['merchant-private-data']['VALUE'];
516 }
517 if (empty($privateData) && array_key_exists('order-summary', $data[$root])
518 && array_key_exists('shopping-cart', $data[$root]['order-summary'])) {
519 $privateData = $data[$root]['order-summary']['shopping-cart']['merchant-private-data']['VALUE'];
520 }
521 $privateData = $privateData ? self::stringToArray($privateData) : '';
522 $orderNo = $data[$root]['google-order-number']['VALUE'];
523 $serial = $data[$root]['serial-number'];
524
525 // a dummy object to call get context and a parent function inside it.
526 $ipn = new CRM_Core_Payment_GoogleIPN('live', $dummyProcessor);
527 list($mode, $module, $paymentProcessorID) = $ipn->getContext($privateData, $orderNo, $root, $response, $serial);
528 $mode = $mode ? 'test' : 'live';
529
530 $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($paymentProcessorID, $mode);
531 $merchant_id = $paymentProcessor['user_name'];
532 $merchant_key = $paymentProcessor['password'];
533 $response->SetMerchantAuthentication($merchant_id, $merchant_key);
534
535 $server_type = ($mode == 'test') ? 'sandbox' : 'production';
536 $request = new GoogleRequest($merchant_id, $merchant_key, $server_type);
537
538 $ipn = self::singleton($mode, $module, $paymentProcessor);
539
540 if (GOOGLE_DEBUG_PP) {
541 CRM_Core_Error::debug_var('RESPONSE-ROOT', $response->root, TRUE, TRUE, 'Google');
542 }
543
544 //Check status and take appropriate action
545 $status = $response->HttpAuthentication($headers);
546
547 switch ($root) {
548 case "request-received":
549 case "error":
550 case "diagnosis":
551 case "checkout-redirect":
552 case "merchant-calculation-callback":
553 break;
554
555 case "new-order-notification": {
556 $response->SendAck($serial, FALSE);
557 $ipn->newOrderNotify($data[$root], $privateData, $module);
558 break;
559 }
560
561 case "order-state-change-notification": {
562 $response->SendAck($serial, FALSE);
563 $new_financial_state = $data[$root]['new-financial-order-state']['VALUE'];
564 $new_fulfillment_order = $data[$root]['new-fulfillment-order-state']['VALUE'];
565
566 switch ($new_financial_state) {
567 case 'CHARGEABLE':
568 break;
569
570 case 'CHARGED':
571 case 'PAYMENT_DECLINED':
572 case 'CANCELLED':
573 case 'CANCELLED_BY_GOOGLE':
574 $ipn->orderStateChange($new_financial_state, $data[$root], $privateData, $module);
575 break;
576
577 case 'REVIEWING':
578 case 'CHARGING':
579 break;
580
581 default:
582 break;
583 }
584 break;
585 }
586
587 case "authorization-amount-notification": {
588 $response->SendAck($serial, FALSE);
589 $new_financial_state = $data[$root]['order-summary']['financial-order-state']['VALUE'];
590 $new_fulfillment_order = $data[$root]['order-summary']['fulfillment-order-state']['VALUE'];
591
592 switch ($new_financial_state) {
593 case 'CHARGEABLE':
594 // For google-handled subscriptions chargeorder needn't be initiated,
595 // assuming auto-charging is turned on.
596 //$request->SendProcessOrder($data[$root]['google-order-number']['VALUE']);
597 //$request->SendChargeOrder($data[$root]['google-order-number']['VALUE'],'');
598 break;
599
600 case 'CHARGED':
601 case 'PAYMENT_DECLINED':
602 case 'CANCELLED':
603 break;
604
605 case 'REVIEWING':
606 case 'CHARGING':
607 case 'CANCELLED_BY_GOOGLE':
608 break;
609
610 default:
611 break;
612 }
613 break;
614 }
615
616 case "charge-amount-notification":
617 case "chargeback-amount-notification":
618 case "refund-amount-notification":
619 case "risk-information-notification":
620 $response->SendAck($serial);
621 break;
622
623 default:
624 break;
625 }
626 }
627
628 function getInput(&$input, &$ids, $dataRoot) {
629 if (!$this->getBillingID($ids)) {
630 return FALSE;
631 }
632
633 $billingID = $ids['billing'];
634 $lookup = array(
635 "first_name" => 'contact-name',
636 // "last-name" not available with google (every thing in contact-name)
637 "last_name" => 'last_name',
638 "street_address-{$billingID}" => 'address1',
639 "city-{$billingID}" => 'city',
640 "state-{$billingID}" => 'region',
641 "postal_code-{$billingID}" => 'postal-code',
642 "country-{$billingID}" => 'country-code',
643 );
644
645 foreach ($lookup as $name => $googleName) {
646 if (array_key_exists($googleName, $dataRoot['buyer-billing-address'])) {
647 $value = $dataRoot['buyer-billing-address'][$googleName]['VALUE'];
648 }
649 $input[$name] = $value ? $value : NULL;
650 }
651 return TRUE;
652 }
653
654 /**
655 * Converts the comma separated name-value pairs in <merchant-private-data>
656 * to an array of name-value pairs.
657 */
658 static function stringToArray($str) {
659 $vars = $labels = array();
660 $labels = explode(',', $str);
661 foreach ($labels as $label) {
662 $terms = explode('=', $label);
663 $vars[$terms[0]] = $terms[1];
664 }
665 return $vars;
666 }
667 }
668