1<?php 2/** 3 * DTA 4 * 5 * DTA is a class that provides functions to create DTA files used in 6 * Germany to exchange informations about money transactions with banks 7 * or online banking programs. 8 * 9 * PHP version 5 10 * 11 * This LICENSE is in the BSD license style. 12 * 13 * Copyright (c) 2003-2005 Hermann Stainer, Web-Gear 14 * http://www.web-gear.com/ 15 * Copyright (c) 2008-2010 Martin Schütte 16 * All rights reserved. 17 * 18 * Redistribution and use in source and binary forms, with or without 19 * modification, are permitted provided that the following conditions 20 * are met: 21 * 22 * Redistributions of source code must retain the above copyright 23 * notice, this list of conditions and the following disclaimer. 24 * 25 * Redistributions in binary form must reproduce the above copyright 26 * notice, this list of conditions and the following disclaimer in the 27 * documentation and/or other materials provided with the distribution. 28 * 29 * Neither the name of Hermann Stainer, Web-Gear nor the names of his 30 * contributors may be used to endorse or promote products derived from 31 * this software without specific prior written permission. 32 * 33 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 34 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 35 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 36 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 37 * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 38 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 39 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 40 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 41 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 42 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 43 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 44 * POSSIBILITY OF SUCH DAMAGE. 45 * 46 * @category Payment 47 * @package Payment_DTA 48 * @author Hermann Stainer <hs@web-gear.com> 49 * @author Martin Schütte <info@mschuette.name> 50 * @copyright 2003-2005 Hermann Stainer, Web-Gear 51 * @copyright 2008-2010 Martin Schütte 52 * @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) 53 * @version SVN: $Id$ 54 * @link http://pear.php.net/package/Payment_DTA 55 */ 56 57/** 58 * needs base class 59 */ 60require_once 'DTABase.php'; 61 62/** 63* Determines the type of the DTA file: 64* DTA file contains credit payments. 65* 66* @const DTA_CREDIT 67*/ 68define("DTA_CREDIT", 0); 69 70/** 71* Determines the type of the DTA file: 72* DTA file contains debit payments (default). 73* 74* @const DTA_DEBIT 75*/ 76define("DTA_DEBIT", 1); 77 78 79/** 80* Dta class provides functions to create and handle with DTA files 81* used in Germany to exchange informations about money transactions with 82* banks or online banking programs. 83* 84* Specifications: 85* - http://www.ebics-zka.de/dokument/pdf/Anlage%203-Spezifikation%20der%20Datenformate%20-%20Version%202.3%20Endfassung%20vom%2005.11.2008.pdf, 86* part 1.1 DTAUS0, p. 4ff 87* - http://www.bundesbank.de/download/zahlungsverkehr/zv_spezifikationen_v1_5.pdf 88* - http://www.hbci-zka.de/dokumente/aenderungen/DTAUS_2002.pdf 89* 90* @category Payment 91* @package Payment_DTA 92* @author Hermann Stainer <hs@web-gear.com> 93* @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) 94* @version Release: 1.4.3 95* @link http://pear.php.net/package/Payment_DTA 96*/ 97class DTA extends DTABase 98{ 99 /** 100 * Type of DTA file, DTA_CREDIT or DTA_DEBIT. 101 * 102 * @var integer $type 103 */ 104 protected $type; 105 106 /** 107 * Sum of bank codes in exchanges; used for control fields. 108 * 109 * @var integer $sum_bankcodes 110 */ 111 protected $sum_bankcodes; 112 113 /** 114 * Sum of account numbers in exchanges; used for control fields. 115 * 116 * @var integer $sum_accounts 117 */ 118 protected $sum_accounts; 119 120 /** 121 * Constructor. Creates an empty DTA object or imports one. 122 * 123 * If the parameter is a string, then it is expected to be in DTA format 124 * an its content (sender and transactions) is imported. If the string cannot 125 * be parsed at all then an empty DTA object with type DTA_CREDIT is returned. 126 * If only parts of the string can be parsed, then all transactions before the 127 * error are included into the object. 128 * The user should use getParsingError() to check whether a parsing error occured. 129 * 130 * Otherwise the parameter has to be the type of the new DTA object, 131 * either DTA_CREDIT or DTA_DEBIT. In this case exceptions are never 132 * thrown to ensure compatibility. 133 * 134 * @param integer|string $type Either a string with DTA data or the type of the 135 * new DTA file (DTA_CREDIT or DTA_DEBIT). Must be set. 136 * 137 * @access public 138 */ 139 function __construct($type) 140 { 141 parent::__construct(); 142 $this->sum_bankcodes = 0; 143 $this->sum_accounts = 0; 144 145 if (is_int($type)) { 146 $this->type = $type; 147 } else { 148 try { 149 $this->parse($type); 150 } catch (Payment_DTA_FatalParseException $e) { 151 // cannot construct this object, reset everything 152 parent::__construct(); 153 $this->sum_bankcodes = 0; 154 $this->sum_accounts = 0; 155 $this->type = DTA_CREDIT; 156 $this->allerrors[] = $e; 157 } catch (Payment_DTA_Exception $e) { 158 // object is valid, but save the error 159 $this->allerrors[] = $e; 160 } 161 } 162 } 163 164 /** 165 * Set the sender of the DTA file. Must be set for valid DTA file. 166 * The given account data is also used as default sender's account. 167 * Account data contains 168 * name Sender's name. Maximally 27 chars are allowed. 169 * bank_code Sender's bank code. 170 * account_number Sender's account number. 171 * additional_name If necessary, additional line for sender's name 172 * (maximally 27 chars). 173 * exec_date Optional execution date for the DTA file in format DDMMYYYY. 174 * 175 * @param array $account Account data for file sender. 176 * 177 * @access public 178 * @return boolean 179 */ 180 function setAccountFileSender($account) 181 { 182 $account['account_number'] 183 = strval($account['account_number']); 184 $account['bank_code'] 185 = strval($account['bank_code']); 186 187 if (strlen($account['name']) > 0 188 && strlen($account['bank_code']) > 0 189 && strlen($account['bank_code']) <= 8 190 && ctype_digit($account['bank_code']) 191 && strlen($account['account_number']) > 0 192 && strlen($account['account_number']) <= 10 193 && ctype_digit($account['account_number']) 194 ) { 195 196 if (empty($account['additional_name'])) { 197 $account['additional_name'] = ""; 198 } 199 200 if (empty($account['exec_date']) 201 || !ctype_digit($account['exec_date']) 202 ) { 203 $account['exec_date'] = str_repeat(" ", 8); 204 } 205 206 $this->account_file_sender = array( 207 "name" => $this->filter($account['name'], 27), 208 "bank_code" => $account['bank_code'], 209 "account_number" => $account['account_number'], 210 "additional_name" => $this->filter($account['additional_name'], 27), 211 "exec_date" => $account['exec_date'] 212 ); 213 214 $result = true; 215 } else { 216 $result = false; 217 } 218 219 return $result; 220 } 221 222 /** 223 * Auxillary method to fill and normalize the receiver and sender arrays. 224 * 225 * @param array $account_receiver Receiver's account data. 226 * @param array $account_sender Sender's account data. 227 * 228 * @access private 229 * @return array 230 */ 231 private function _exchangeFillArrays($account_receiver, $account_sender) 232 { 233 if (empty($account_receiver['additional_name'])) { 234 $account_receiver['additional_name'] = ""; 235 } 236 if (empty($account_sender['name'])) { 237 $account_sender['name'] = $this->account_file_sender['name']; 238 } 239 if (empty($account_sender['bank_code'])) { 240 $account_sender['bank_code'] = $this->account_file_sender['bank_code']; 241 } 242 if (empty($account_sender['account_number'])) { 243 $account_sender['account_number'] 244 = $this->account_file_sender['account_number']; 245 } 246 if (empty($account_sender['additional_name'])) { 247 $account_sender['additional_name'] 248 = $this->account_file_sender['additional_name']; 249 } 250 251 $account_receiver['account_number'] 252 = strval($account_receiver['account_number']); 253 $account_receiver['bank_code'] 254 = strval($account_receiver['bank_code']); 255 $account_sender['account_number'] 256 = strval($account_sender['account_number']); 257 $account_sender['bank_code'] 258 = strval($account_sender['bank_code']); 259 260 return array($account_receiver, $account_sender); 261 } 262 263 /** 264 * Adds an exchange. First the account data for the receiver of the exchange is 265 * set. In the case the DTA file contains credits, this is the payment receiver. 266 * In the other case (the DTA file contains debits), this is the account, from 267 * which money is taken away. If the sender is not specified, values of the 268 * file sender are used by default. 269 * 270 * Account data for receiver and sender contain 271 * name Name. Maximally 27 chars are allowed. 272 * bank_code Bank code. 273 * account_number Account number. 274 * additional_name If necessary, additional line for name (maximally 27 chars). 275 * 276 * @param array $account_receiver Receiver's account data. 277 * @param double $amount Amount of money in this exchange. 278 * Currency: EURO 279 * @param array $purposes Array of up to 14 lines 280 * (maximally 27 chars each) for 281 * description of the exchange. 282 * A string is accepted as well. 283 * @param array $account_sender Sender's account data. 284 * 285 * @access public 286 * @return boolean 287 */ 288 function addExchange( 289 $account_receiver, 290 $amount, 291 $purposes, 292 $account_sender = array() 293 ) { 294 list($account_receiver, $account_sender) 295 = $this->_exchangeFillArrays($account_receiver, $account_sender); 296 297 $cents = (int)(round($amount * 100)); 298 if (strlen($account_sender['name']) > 0 299 && strlen($account_sender['bank_code']) > 0 300 && strlen($account_sender['bank_code']) <= 8 301 && ctype_digit($account_sender['bank_code']) 302 && strlen($account_sender['account_number']) > 0 303 && strlen($account_sender['account_number']) <= 10 304 && ctype_digit($account_sender['account_number']) 305 && strlen($account_receiver['name']) > 0 306 && strlen($account_receiver['bank_code']) <= 8 307 && ctype_digit($account_receiver['bank_code']) 308 && strlen($account_receiver['account_number']) <= 10 309 && ctype_digit($account_receiver['account_number']) 310 && is_numeric($amount) 311 && $cents > 0 312 && $cents <= PHP_INT_MAX 313 && $this->sum_amounts <= (PHP_INT_MAX - $cents) 314 && ( (is_string($purposes) 315 && strlen($purposes) > 0) 316 || (is_array($purposes) 317 && count($purposes) >= 1 318 && count($purposes) <= 14)) 319 ) { 320 $this->sum_amounts += $cents; 321 $this->sum_bankcodes += $account_receiver['bank_code']; 322 $this->sum_accounts += $account_receiver['account_number']; 323 324 if (is_string($purposes)) { 325 $filtered_purposes = str_split( 326 $this->makeValidString($purposes), 27 327 ); 328 $filtered_purposes = array_slice($filtered_purposes, 0, 14); 329 } else { 330 $filtered_purposes = array(); 331 foreach ($purposes as $purposeline) { 332 $filtered_purposes[] = $this->filter($purposeline, 27); 333 } 334 } 335 336 $this->exchanges[] = array( 337 "sender_name" => $this->filter( 338 $account_sender['name'], 27 339 ), 340 "sender_bank_code" => $account_sender['bank_code'], 341 "sender_account_number" => $account_sender['account_number'], 342 "sender_additional_name" => $this->filter( 343 $account_sender['additional_name'], 27 344 ), 345 "receiver_name" => $this->filter( 346 $account_receiver['name'], 27 347 ), 348 "receiver_bank_code" => $account_receiver['bank_code'], 349 "receiver_account_number" => $account_receiver['account_number'], 350 "receiver_additional_name" => $this->filter( 351 $account_receiver['additional_name'], 27 352 ), 353 "amount" => $cents, 354 "purposes" => $filtered_purposes 355 ); 356 357 $result = true; 358 } else { 359 $result = false; 360 } 361 362 return $result; 363 } 364 365 /** 366 * Auxillary method to write the A record. 367 * 368 * @access private 369 * @return string 370 */ 371 private function _generateArecord() 372 { 373 $content = ""; 374 375 // (field numbers according to ebics-zka.de specification) 376 // A1 record length (128 Bytes) 377 $content .= str_pad("128", 4, "0", STR_PAD_LEFT); 378 // A2 record type 379 $content .= "A"; 380 // A3 file mode (credit or debit) 381 // and Customer File ("K") / Bank File ("B") 382 $content .= ($this->type == DTA_CREDIT) ? "G" : "L"; 383 $content .= "K"; 384 // A4 sender's bank code 385 $content .= str_pad( 386 $this->account_file_sender['bank_code'], 8, "0", STR_PAD_LEFT 387 ); 388 // A5 only used if Bank File, otherwise NULL 389 $content .= str_repeat("0", 8); 390 // A6 sender's name 391 $content .= str_pad( 392 $this->account_file_sender['name'], 27, " ", STR_PAD_RIGHT 393 ); 394 // A7 date of file creation 395 $content .= strftime("%d%m%y", $this->timestamp); 396 // A8 free (bank internal) 397 $content .= str_repeat(" ", 4); 398 // A9 sender's account number 399 $content .= str_pad( 400 $this->account_file_sender['account_number'], 10, "0", STR_PAD_LEFT 401 ); 402 // A10 sender's reference number (optional) 403 $content .= str_repeat("0", 10); 404 // A11a free (reserve) 405 $content .= str_repeat(" ", 15); 406 // A11b execution date ("DDMMYYYY", optional) 407 $content .= $this->account_file_sender['exec_date']; 408 // A11c free (reserve) 409 $content .= str_repeat(" ", 24); 410 // A12 currency (1 = Euro) 411 $content .= "1"; 412 413 assert(strlen($content) == 128); 414 return $content; 415 } 416 417 /** 418 * Auxillary method to write C records. 419 * 420 * @param array $exchange The transaction to serialize. 421 * 422 * @access private 423 * @return string 424 */ 425 private function _generateCrecord($exchange) 426 { 427 // preparation of additional parts for record extensions 428 $additional_parts = array(); 429 $additional_purposes = $exchange['purposes']; 430 $first_purpose = array_shift($additional_purposes); 431 432 if (strlen($exchange['receiver_additional_name']) > 0) { 433 $additional_parts[] = array("type" => "01", 434 "content" => $exchange['receiver_additional_name'] 435 ); 436 } 437 438 foreach ($additional_purposes as $additional_purpose) { 439 $additional_parts[] = array("type" => "02", 440 "content" => $additional_purpose 441 ); 442 } 443 444 if (strlen($exchange['sender_additional_name']) > 0) { 445 $additional_parts[] = array("type" => "03", 446 "content" => $exchange['sender_additional_name'] 447 ); 448 } 449 assert(count($additional_parts) <= 15); 450 451 $content = ""; 452 453 // (field numbers according to ebics-zka.de specification) 454 // C1 record length (187 Bytes + 29 Bytes for each additional part) 455 $content .= str_pad( 456 187 + count($additional_parts) * 29, 4, "0", STR_PAD_LEFT 457 ); 458 // C2 record type 459 $content .= "C"; 460 // C3 first involved bank 461 $content .= str_pad( 462 $exchange['sender_bank_code'], 8, "0", STR_PAD_LEFT 463 ); 464 // C4 receiver's bank code 465 $content .= str_pad( 466 $exchange['receiver_bank_code'], 8, "0", STR_PAD_LEFT 467 ); 468 // C5 receiver's account number 469 $content .= str_pad( 470 $exchange['receiver_account_number'], 10, "0", STR_PAD_LEFT 471 ); 472 // C6 internal customer number (11 chars) or NULL 473 $content .= "0" . str_repeat("0", 11) . "0"; 474 // C7a payment mode (text key) 475 $content .= ($this->type == DTA_CREDIT) ? "51" : "05"; 476 // C7b additional text key 477 $content .= "000"; 478 // C8 bank internal 479 $content .= " "; 480 // C9 free (reserve) 481 $content .= str_repeat("0", 11); 482 // C10 sender's bank code 483 $content .= str_pad( 484 $exchange['sender_bank_code'], 8, "0", STR_PAD_LEFT 485 ); 486 // C11 sender's account number 487 $content .= str_pad( 488 $exchange['sender_account_number'], 10, "0", STR_PAD_LEFT 489 ); 490 // C12 amount 491 $content .= str_pad( 492 $exchange['amount'], 11, "0", STR_PAD_LEFT 493 ); 494 // C13 free (reserve) 495 $content .= str_repeat(" ", 3); 496 // C14a receiver's name 497 $content .= str_pad( 498 $exchange['receiver_name'], 27, " ", STR_PAD_RIGHT 499 ); 500 // C14b delimitation 501 $content .= str_repeat(" ", 8); 502 /* first part/128 chars full */ 503 // C15 sender's name 504 $content .= str_pad( 505 $exchange['sender_name'], 27, " ", STR_PAD_RIGHT 506 ); 507 // C16 first line of purposes 508 $content .= str_pad($first_purpose, 27, " ", STR_PAD_RIGHT); 509 // C17a currency (1 = Euro) 510 $content .= "1"; 511 // C17b free (reserve) 512 $content .= str_repeat(" ", 2); 513 // C18 number of additional parts (00-15) 514 $content .= str_pad(count($additional_parts), 2, "0", STR_PAD_LEFT); 515 516 /* 517 * End of the constant part (187 chars), 518 * now up to 15 extensions with 29 chars each might follow. 519 */ 520 521 if (count($additional_parts) == 0) { 522 // no extension, pad to fill the part to 2*128 chars 523 $content .= str_repeat(" ", 256-187); 524 } else { 525 // The first two extensions fit into the current part: 526 for ($index = 1;$index <= 2;$index++) { 527 if (count($additional_parts) > 0) { 528 $additional_part = array_shift($additional_parts); 529 } else { 530 $additional_part = array("type" => " ", 531 "content" => "" 532 ); 533 } 534 // C19/21 type of addional part 535 $content .= $additional_part['type']; 536 // C20/22 additional part content 537 $content .= str_pad( 538 $additional_part['content'], 27, " ", STR_PAD_RIGHT 539 ); 540 } 541 // delimitation 542 $content .= str_repeat(" ", 11); 543 } 544 545 // For more extensions add up to 4 more parts: 546 for ($part = 3;$part <= 5;$part++) { 547 if (count($additional_parts) > 0) { 548 for ($index = 1;$index <= 4;$index++) { 549 if (count($additional_parts) > 0) { 550 $additional_part = array_shift($additional_parts); 551 } else { 552 $additional_part = array("type" => " ", 553 "content" => "" 554 ); 555 } 556 // C24/26/28/30 type of addional part 557 $content .= $additional_part['type']; 558 // C25/27/29/31 additional part content 559 $content .= str_pad( 560 $additional_part['content'], 27, " ", STR_PAD_RIGHT 561 ); 562 } 563 // C32 delimitation 564 $content .= str_repeat(" ", 12); 565 } 566 } 567 // with 15 extensions there may be a 6th part 568 if (count($additional_parts) > 0) { 569 $additional_part = array_shift($additional_parts); 570 // C24 type of addional part 571 $content .= $additional_part['type']; 572 // C25 additional part content 573 $content .= str_pad( 574 $additional_part['content'], 27, " ", STR_PAD_RIGHT 575 ); 576 // padding to fill the part 577 $content .= str_repeat(" ", 128-27-2); 578 } 579 assert(count($additional_parts) == 0); 580 assert(strlen($content) % 128 == 0); 581 return $content; 582 } 583 584 /** 585 * Auxillary method to write the E record. 586 * 587 * @access private 588 * @return string 589 */ 590 private function _generateErecord() 591 { 592 $content = ""; 593 594 // (field numbers according to ebics-zka.de specification) 595 // E1 record length (128 bytes) 596 $content .= str_pad("128", 4, "0", STR_PAD_LEFT); 597 // E2 record type 598 $content .= "E"; 599 // E3 free (reserve) 600 $content .= str_repeat(" ", 5); 601 // E4 number of records type C 602 $content .= str_pad(count($this->exchanges), 7, "0", STR_PAD_LEFT); 603 // E5 free (reserve) 604 $content .= str_repeat("0", 13); 605 // use number_format() to ensure proper integer formatting 606 // E6 sum of account numbers 607 $content .= str_pad( 608 number_format($this->sum_accounts, 0, "", ""), 17, "0", STR_PAD_LEFT 609 ); 610 // E7 sum of bank codes 611 $content .= str_pad( 612 number_format($this->sum_bankcodes, 0, "", ""), 17, "0", STR_PAD_LEFT 613 ); 614 // E8 sum of amounts 615 $content .= str_pad( 616 number_format($this->sum_amounts, 0, "", ""), 13, "0", STR_PAD_LEFT 617 ); 618 // E9 delimitation 619 $content .= str_repeat(" ", 51); 620 621 assert(strlen($content) % 128 == 0); 622 return $content; 623 } 624 625 /** 626 * Returns the full content of the generated DTA file. 627 * All added exchanges are processed. 628 * 629 * @access public 630 * @return string 631 */ 632 function getFileContent() 633 { 634 $content = ""; 635 636 /** 637 * data record A 638 */ 639 $content .= $this->_generateArecord(); 640 641 /** 642 * data record(s) C 643 */ 644 $sum_account_numbers = 0; 645 $sum_bank_codes = 0; 646 $sum_amounts = 0; 647 648 foreach ($this->exchanges as $exchange) { 649 $sum_account_numbers += $exchange['receiver_account_number']; 650 $sum_bank_codes += (int) $exchange['receiver_bank_code']; 651 $sum_amounts += (int) $exchange['amount']; 652 653 $content .= $this->_generateCrecord($exchange); 654 assert(strlen($content) % 128 == 0); 655 } 656 657 assert($this->sum_amounts === $sum_amounts); 658 assert($this->sum_bankcodes === $sum_bank_codes); 659 assert($this->sum_accounts === $sum_account_numbers); 660 661 /** 662 * data record E 663 */ 664 $content .= $this->_generateErecord(); 665 666 return $content; 667 } 668 669 /** 670 * Returns an array with information about the transactions. 671 * Can be used to print an accompanying document (Begleitzettel) for disks. 672 * 673 * @access public 674 * @return array Returns an array with keys: "sender_name", 675 * "sender_bank_code", "sender_account", "sum_amounts", 676 * "type", "sum_bankcodes", "sum_accounts", "count", "date", "exec_date" 677 */ 678 function getMetaData() 679 { 680 $meta = parent::getMetaData(); 681 682 $meta["sum_bankcodes"] = floatval($this->sum_bankcodes); 683 $meta["sum_accounts"] = floatval($this->sum_accounts); 684 $meta["type"] = strval(($this->type == DTA_CREDIT) ? "CREDIT" : "DEBIT"); 685 686 $meta["exec_date"] = $meta["date"]; 687 // use timestamp to be consistent with $meta["date"] 688 if (trim($this->account_file_sender["exec_date"]) !== "") { 689 $ftime = strptime($this->account_file_sender["exec_date"], '%d%m%Y'); 690 if ($ftime) { 691 $meta["exec_date"] = mktime( 692 0, 0, 0, 693 $ftime['tm_mon'] + 1, 694 $ftime['tm_mday'], 695 $ftime['tm_year'] + 1900 696 ); 697 } 698 } 699 return $meta; 700 } 701 702 /** 703 * Auxillary parser to consume A records. 704 * 705 * @param string $input content of DTA file 706 * @param integer &$offset read offset into $input 707 * 708 * @throws Payment_DTA_Exception on unrecognized input 709 * @access private 710 * @return void 711 */ 712 private function _parseArecord($input, &$offset) 713 { 714 /* field 1+2 */ 715 $this->checkStr($input, $offset, "0128A"); 716 /* field 3 */ 717 $type = $this->getStr($input, $offset, 2); 718 /* field 4 */ 719 $Asender_blz = $this->getNum($input, $offset, 8); 720 /* field 5 */ 721 $this->checkStr($input, $offset, "00000000"); 722 /* field 6 */ 723 $Asender_name = rtrim($this->getStr($input, $offset, 27, true)); 724 /* field 7 */ 725 $Adate_day = $this->getNum($input, $offset, 2); 726 $Adate_month = $this->getNum($input, $offset, 2); 727 $Adate_year = $this->getNum($input, $offset, 2); 728 $this->timestamp = mktime( 729 0, 0, 0, 730 intval($Adate_month), intval($Adate_day), intval($Adate_year) 731 ); 732 /* field 8 */ 733 $this->checkStr($input, $offset, " "); 734 /* field 9 */ 735 $Asender_account = $this->getNum($input, $offset, 10); 736 /* field 10 */ 737 $this->checkStr($input, $offset, "0000000000"); 738 /* field 11a */ 739 $this->checkStr($input, $offset, str_repeat(" ", 15)); 740 /* field 11b */ 741 $Aexec_date = $this->getStr($input, $offset, 8); 742 /* field 11c */ 743 $this->checkStr($input, $offset, str_repeat(" ", 24)); 744 /* field 12 */ 745 $this->checkStr($input, $offset, "1"); 746 747 /* the first char G/L indicates credit and debit exchanges 748 * the second char K/B indicates a customer or bank file 749 * (I do not know if bank files should be treated different) 750 */ 751 if ($type === "GK" || $type === "GB") { 752 $this->type = DTA_CREDIT; 753 } elseif ($type === "LK" || $type === "LB") { 754 $this->type = DTA_DEBIT; 755 } else { 756 throw new Payment_DTA_FatalParseException( 757 "Invalid type indicator: '$type', expected ". 758 "either 'GK'/'GB' or 'LK'/'LB' (@offset 6)." 759 ); 760 } 761 762 /* 763 * additional_name is problematic and cannot be parsed & reproduced. 764 * it is set as part of the AccountFileSender, but appears as part 765 * of every transaction. 766 */ 767 $rc = $this->setAccountFileSender( 768 array( 769 "name" => $Asender_name, 770 "bank_code" => $Asender_blz, 771 "account_number" => $Asender_account, 772 "additional_name" => '', 773 "exec_date" => $Aexec_date 774 ) 775 ); 776 if (!$rc) { 777 // should never happen 778 throw new Payment_DTA_FatalParseException( 779 "Cannot setAccountFileSender(), please file a bug report" 780 ); 781 } 782 // currently not a TODO: 783 // does anyone have to preserve the creation date or execution date? 784 } 785 786 /** 787 * Auxillary method to parse C record extensions. 788 * 789 * Reads the variable number of extensions at the end of a C record. 790 * 791 * @param string $input content of DTA file 792 * @param integer &$offset read offset into $input 793 * @param integer $extensions expected number of extensions 794 * @param integer $c_start C record offset (for exceptions) 795 * 796 * @throws Payment_DTA_ParseException on invalid extensions 797 * @access private 798 * @return array of $Cpurpose, 2nd sender line, 2nd receiver line 799 */ 800 private function _parseCextension($input, &$offset, $extensions, $c_start) 801 { 802 $extensions_read = array(); 803 804 // first handle the up to 2 extensions inside the 2nd part 805 if ($extensions == 0) { // only padding 806 $this->checkStr($input, $offset, str_repeat(" ", 69)); 807 } elseif ($extensions == 1) { 808 /* field 19 */ 809 $ext_type = $this->getNum($input, $offset, 2); 810 /* field 20 */ 811 $ext_content = $this->getStr($input, $offset, 27, true); 812 array_push($extensions_read, array($ext_type, $ext_content)); 813 /* fields 21,22,23 */ 814 $this->checkStr($input, $offset, str_repeat(" ", 2+27+11)); 815 } else { 816 /* field 19 */ 817 $ext_type = $this->getNum($input, $offset, 2); 818 /* field 20 */ 819 $ext_content = $this->getStr($input, $offset, 27, true); 820 array_push($extensions_read, array($ext_type, $ext_content)); 821 /* field 21 */ 822 $ext_type = $this->getNum($input, $offset, 2); 823 /* field 22 */ 824 $ext_content = $this->getStr($input, $offset, 27, true); 825 array_push($extensions_read, array($ext_type, $ext_content)); 826 /* fields 23 */ 827 $this->checkStr($input, $offset, str_repeat(" ", 11)); 828 } 829 // end 2nd part of C record 830 assert($offset % 128 === 0); 831 832 // up to 4 more parts, each with 128 bytes & up to 4 extensions 833 while (count($extensions_read) < $extensions) { 834 $ext_in_part = $extensions - count($extensions_read); 835 // one switch to read the content 836 switch($ext_in_part) { 837 default: // =4 838 case 4: /* fallthrough */ 839 $ext_type = $this->getNum($input, $offset, 2); 840 $ext_content = $this->getStr($input, $offset, 27, true); 841 array_push($extensions_read, array($ext_type, $ext_content)); 842 case 3: /* fallthrough */ 843 $ext_type = $this->getNum($input, $offset, 2); 844 $ext_content = $this->getStr($input, $offset, 27, true); 845 array_push($extensions_read, array($ext_type, $ext_content)); 846 case 2: /* fallthrough */ 847 $ext_type = $this->getNum($input, $offset, 2); 848 $ext_content = $this->getStr($input, $offset, 27, true); 849 array_push($extensions_read, array($ext_type, $ext_content)); 850 case 1: /* fallthrough */ 851 $ext_type = $this->getNum($input, $offset, 2); 852 $ext_content = $this->getStr($input, $offset, 27, true); 853 array_push($extensions_read, array($ext_type, $ext_content)); 854 break; 855 case 0: 856 // should never happen 857 throw new Payment_DTA_ParseException( 858 'confused about number of extensions in transaction number '. 859 strval($this->count()+1) .' @ offset '. strval($c_start) . 860 ', please file a bug report' 861 ); 862 } 863 864 // and one switch for the padding 865 switch($ext_in_part) { 866 case 1: 867 $this->checkStr($input, $offset, str_repeat(" ", 29)); 868 case 2: /* fallthrough */ 869 $this->checkStr($input, $offset, str_repeat(" ", 29)); 870 case 3: /* fallthrough */ 871 $this->checkStr($input, $offset, str_repeat(" ", 29)); 872 case 4: /* fallthrough */ 873 default: /* fallthrough */ 874 $this->checkStr($input, $offset, str_repeat(" ", 12)); 875 break; 876 } 877 // end n-th part of C record 878 assert($offset % 128 === 0); 879 } 880 return $extensions_read; 881 } 882 883 /** 884 * Auxillary method to combine C record extensions. 885 * 886 * Takes the parsed extensions to check the allowed number of them per type 887 * and to collect all purpose lines into one array. 888 * 889 * @param array $extensions_read read extensions as arrays 890 * @param array $Cpurpose existing array of purpose lines 891 * @param integer $c_start C record offset (for exceptions) 892 * 893 * @throws Payment_DTA_ParseException on invalid extensions 894 * @access private 895 * @return array of $Cpurpose, 2nd sender line, 2nd receiver line 896 */ 897 private function _processCextension($extensions_read, $Cpurpose, $c_start) 898 { 899 $Csender_name2 = ""; 900 $Creceiver_name2 = ""; 901 902 foreach ($extensions_read as $ext) { 903 $ext_type = $ext[0]; 904 $ext_content = $ext[1]; 905 906 switch($ext_type) { 907 case 1: 908 if (!empty($Creceiver_name2)) { 909 throw new Payment_DTA_ParseException( 910 'multiple receiver name extensions in transaction number '. 911 strval($this->count()+1) .' @ offset '. strval($c_start) 912 ); 913 } else { 914 $Creceiver_name2 = $ext_content; 915 } 916 break; 917 case 2: 918 if (count($Cpurpose) >= 14) { 919 // allowed: 1 line in fixed part + 13 in extensions 920 throw new Payment_DTA_ParseException( 921 'too many purpose extensions in transaction number '. 922 strval($this->count()+1) .' @ offset '. strval($c_start) 923 ); 924 } else { 925 array_push($Cpurpose, $ext_content); 926 } 927 break; 928 case 3: 929 if (!empty($Csender_name2)) { 930 throw new Payment_DTA_ParseException( 931 'multiple receiver name extensions in transaction number '. 932 strval($this->count()+1) .' @ offset '. strval($c_start) 933 ); 934 } else { 935 $Csender_name2 = $ext_content; 936 } 937 break; 938 default: 939 throw new Payment_DTA_ParseException( 940 'invalid extension type in transaction number '. 941 strval($this->count()+1) .' @ offset '. strval($c_start) 942 ); 943 } 944 } 945 946 return array($Cpurpose, $Csender_name2, $Creceiver_name2); 947 } 948 949 /** 950 * Auxillary parser to consume C records. 951 * 952 * @param string $input content of DTA file 953 * @param integer &$offset read offset into $input 954 * @param array &$checks holds checksums for validation in E record 955 * 956 * @throws Payment_DTA_Exception on unrecognized input 957 * @access private 958 * @return void 959 */ 960 private function _parseCrecord($input, &$offset, &$checks) 961 { 962 // save for possible exceptions 963 $c_start = $offset; 964 965 /* field 1 */ 966 $record_length = $this->getNum($input, $offset, 4); 967 /* field 2 */ 968 $this->checkStr($input, $offset, "C"); 969 970 // check the record length 971 if (($record_length-187)%29) { 972 throw new Payment_DTA_ParseException('invalid C record length'); 973 } 974 $extensions_length = ($record_length-187)/29; 975 976 /* field 3 */ 977 $Cbank_blz = $this->getNum($input, $offset, 8); // usually 0, ignored 978 /* field 4 */ 979 $Creceiver_blz = $this->getNum($input, $offset, 8); 980 /* field 5 */ 981 $Creceiver_account = $this->getNum($input, $offset, 10); 982 /* field 6 */ 983 $this->checkStr($input, $offset, "0"); 984 // either 0s or aninternal customer number: 985 $this->getNum($input, $offset, 11); 986 $this->checkStr($input, $offset, "0"); 987 /* field 7 */ 988 // may hold about a dozen values with details about the type of transaction 989 $Ctype = $this->getStr($input, $offset, 5); 990 if ( (($this->type == DTA_DEBIT) && (!preg_match('/^0[45]\d{3}$/', $Ctype))) 991 || (($this->type == DTA_CREDIT) && (!preg_match('/^5\d{4}$/', $Ctype))) 992 ) { 993 throw new Payment_DTA_ParseException( 994 'C record type of payment (' . $Ctype . ') '. 995 'does not match A record type indicator '. 996 '(' . (($this->type == DTA_CREDIT) ? "CREDIT" : "DEBIT") . ') '. 997 'in transaction number '. strval($this->count()+1) . 998 ' @ offset '. strval($c_start) 999 ); 1000 } 1001 /* field 8 */ 1002 $this->checkStr($input, $offset, " "); 1003 /* field 9 */ 1004 $this->checkStr($input, $offset, "00000000000"); 1005 /* field 10 */ 1006 $Csender_blz = $this->getNum($input, $offset, 8); 1007 /* field 11 */ 1008 $Csender_account = $this->getNum($input, $offset, 10); 1009 /* field 12 */ 1010 $Camount = $this->getNum($input, $offset, 11); 1011 /* field 13 */ 1012 $this->checkStr($input, $offset, " "); 1013 /* field 14a */ 1014 $Creceiver_name = rtrim($this->getStr($input, $offset, 27, true)); 1015 /* field 14b */ 1016 $this->checkStr($input, $offset, " "); 1017 // end 1st part of C record 1018 assert($offset % 128 === 0); 1019 /* field 15 */ 1020 $Csender_name = rtrim($this->getStr($input, $offset, 27, true)); 1021 /* field 16 */ 1022 $Cpurpose = array(rtrim($this->getStr($input, $offset, 27, true))); 1023 /* field 17a */ 1024 $this->checkStr($input, $offset, "1"); 1025 /* field 17b */ 1026 $this->checkStr($input, $offset, " "); 1027 /* field 18 */ 1028 $extensions = $this->getNum($input, $offset, 2); 1029 if ($extensions != $extensions_length) { 1030 throw new Payment_DTA_ParseException( 1031 'number of extensions '. 1032 'does not match record length in transaction number '. 1033 strval($this->count()+1) .' @ offset '. strval($c_start) 1034 ); 1035 } 1036 1037 // extensions to C record, read into array & processed later 1038 $extensions_read 1039 = $this->_parseCextension($input, $offset, $extensions, $c_start); 1040 1041 // process read extension content 1042 list($Cpurpose, $Csender_name2, $Creceiver_name2) 1043 = $this->_processCextension($extensions_read, $Cpurpose, $c_start); 1044 1045 /* we read the fields, now add an exchange */ 1046 $rc = $this->addExchange( 1047 array( 1048 'name' => $Creceiver_name, 1049 'bank_code' => $Creceiver_blz, 1050 'account_number' => $Creceiver_account, 1051 'additional_name' => $Creceiver_name2 1052 ), 1053 $Camount/100.0, 1054 $Cpurpose, 1055 array( 1056 'name' => $Csender_name, 1057 'bank_code' => $Csender_blz, 1058 'account_number' => $Csender_account, 1059 'additional_name' => $Csender_name2 1060 ) 1061 ); 1062 if (!$rc) { 1063 // should never happen 1064 throw new Payment_DTA_ParseException( 1065 'Cannot addExchange() for transaction number '. 1066 strval($this->count()+1) . 1067 ' @ offset '. strval($c_start). ', please file a bug report' 1068 ); 1069 } 1070 $checks['account'] += $Creceiver_account; 1071 $checks['blz'] += $Creceiver_blz; 1072 $checks['amount'] += $Camount; 1073 } 1074 1075 /** 1076 * Auxillary parser to consume E records. 1077 * 1078 * @param string $input content of DTA file 1079 * @param integer &$offset read offset into $input 1080 * @param array $checks holds checksums for validation 1081 * 1082 * @throws Payment_DTA_Exception on unrecognized input 1083 * @access private 1084 * @return void 1085 */ 1086 private function _parseErecord($input, &$offset, $checks) 1087 { 1088 /* field 1+2 */ 1089 $this->checkStr($input, $offset, "0128E"); 1090 /* field 3 */ 1091 $this->checkStr($input, $offset, " "); 1092 /* field 4 */ 1093 $E_check_count = $this->getNum($input, $offset, 7); 1094 /* field 5 */ 1095 $this->checkStr($input, $offset, str_repeat("0", 13)); 1096 /* field 6 */ 1097 $E_check_account = $this->getNum($input, $offset, 17); 1098 /* field 7 */ 1099 $E_check_blz = $this->getNum($input, $offset, 17); 1100 /* field 8 */ 1101 $E_check_amount = $this->getNum($input, $offset, 13); 1102 /* field 9 */ 1103 $this->checkStr($input, $offset, str_repeat(" ", 51)); 1104 // end of E record 1105 assert($offset % 128 === 0); 1106 1107 // check checksums 1108 1109 /* 1110 * NB: because errors are indicated by exceptions, the user/caller never 1111 * sees more than one checksum error. Only the first mismatch is reported, 1112 * the other checks are skipped by throwing the exception. 1113 */ 1114 if ($E_check_count != $this->count()) { 1115 throw new Payment_DTA_ChecksumException( 1116 "E record checksum mismatch for transaction count: ". 1117 "reads $E_check_count, expected ".$this->count() 1118 ); 1119 } 1120 if ($E_check_account != $checks['account']) { 1121 throw new Payment_DTA_ChecksumException( 1122 "E record checksum mismatch for account numbers: ". 1123 "reads $E_check_account, expected ".$checks['account'] 1124 ); 1125 } 1126 if ($E_check_blz != $checks['blz']) { 1127 throw new Payment_DTA_ChecksumException( 1128 "E record checksum mismatch for bank codes: ". 1129 "reads $E_check_blz, expected ".$checks['blz'] 1130 ); 1131 } 1132 if ($E_check_amount != $checks['amount']) { 1133 throw new Payment_DTA_ChecksumException( 1134 "E record checksum mismatch for transfer amount: ". 1135 "reads $E_check_amount, expected ".$checks['amount'] 1136 ); 1137 } 1138 } 1139 1140 /** 1141 * Parser. Read data from an existing DTA file content. 1142 * 1143 * Parsing can leave us with four situations: 1144 * - the input is parsed correctly => valid DTA object. 1145 * - the input is parsed but a checksum does not match the data read 1146 * => valid DTA object. 1147 * throws a Payment_DTA_ChecksumException. 1148 * - the n-th transaction cannot be parsed => parsing stops there, yielding 1149 * a valid DTA object, but with only the first n-1 transactions 1150 * and without checksum verification. 1151 * throws a Payment_DTA_ParseException. 1152 * - a parsing error occurs in the A record => the DTA object is invalid 1153 * throws a Payment_DTA_FatalParseException. 1154 * 1155 * @param string $input content of DTA file 1156 * 1157 * @throws Payment_DTA_Exception on unrecognized input 1158 * @access protected 1159 * @return void 1160 */ 1161 protected function parse($input) 1162 { 1163 /* 1164 * Open Questions/TODOs for the parsing code: 1165 * - Are the provided exceptions adequate? (Or are they too verbose for 1166 * practical use or OTOH not detailed enough to really handle errors?) 1167 * - Should we try to parse truncated files, i.e. ones with a wrong length? 1168 * - Should we try to find records with a wrong offset, e.g. when an 1169 * encoding error shifts all following records 4 bytes backwards? 1170 * - Should we abort on any error or rather skip the exchange and continue? 1171 * In the later case we need a way to preserve/indicate the problem 1172 * because any simple ParseException in a C record will be masked by 1173 * a resulting ChecksumException in the E record. 1174 * - TODO: We should read non-ASCII chars in A/C records. Some programs 1175 * write 8-bit chars into the fields. 1176 */ 1177 if (strlen($input) % 128) { 1178 throw new Payment_DTA_FatalParseException("invalid length"); 1179 } 1180 1181 $checks = array( 1182 'account' => 0, 1183 'blz' => 0, 1184 'amount' => 0); 1185 $offset = 0; 1186 1187 /* A record */ 1188 try { 1189 $this->_parseArecord($input, $offset); 1190 } catch (Payment_DTA_Exception $e) { 1191 throw new Payment_DTA_FatalParseException("Exception in A record", $e); 1192 } 1193 1194 //do not consume input by using getStr()/getNum() here 1195 while ($input[$offset + 4] == 'C') { 1196 /* C record */ 1197 $c_start = $offset; 1198 $c_length = intval(substr($input, $offset, 4)); 1199 try { 1200 $this->_parseCrecord($input, $offset, $checks); 1201 } catch (Payment_DTA_Exception $e) { 1202 // preserve error 1203 $this->allerrors[] = new Payment_DTA_ParseException( 1204 "Error in C record, in transaction number ". 1205 strval($this->count()+1) ." @ offset ". strval($c_start), $e 1206 ); 1207 // skip to next 128-byte aligned record 1208 $offset = $c_start + 128 * (1 + intval($c_length/128)); 1209 } 1210 } // while 1211 1212 /* E record */ 1213 try { 1214 $this->_parseErecord($input, $offset, $checks); 1215 } catch (Payment_DTA_ChecksumException $e) { 1216 throw $e; 1217 } catch (Payment_DTA_Exception $e) { 1218 throw new Payment_DTA_ParseException("Error in E record", $e); 1219 } 1220 } 1221} 1222