芝麻web文件管理V1.00
编辑当前文件:/home/strato/chroot/opt/RZphp81/includes/Payment/DTA.php
* @author Martin Schütte
* @copyright 2003-2005 Hermann Stainer, Web-Gear * @copyright 2008-2010 Martin Schütte * @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) * @version SVN: $Id$ * @link http://pear.php.net/package/Payment_DTA */ /** * needs base class */ require_once 'DTABase.php'; /** * Determines the type of the DTA file: * DTA file contains credit payments. * * @const DTA_CREDIT */ define("DTA_CREDIT", 0); /** * Determines the type of the DTA file: * DTA file contains debit payments (default). * * @const DTA_DEBIT */ define("DTA_DEBIT", 1); /** * Dta class provides functions to create and handle with DTA files * used in Germany to exchange informations about money transactions with * banks or online banking programs. * * Specifications: * - http://www.ebics-zka.de/dokument/pdf/Anlage%203-Spezifikation%20der%20Datenformate%20-%20Version%202.3%20Endfassung%20vom%2005.11.2008.pdf, * part 1.1 DTAUS0, p. 4ff * - http://www.bundesbank.de/download/zahlungsverkehr/zv_spezifikationen_v1_5.pdf * - http://www.hbci-zka.de/dokumente/aenderungen/DTAUS_2002.pdf * * @category Payment * @package Payment_DTA * @author Hermann Stainer
* @license http://www.debian.org/misc/bsd.license BSD License (3 Clause) * @version Release: 1.4.3 * @link http://pear.php.net/package/Payment_DTA */ class DTA extends DTABase { /** * Type of DTA file, DTA_CREDIT or DTA_DEBIT. * * @var integer $type */ protected $type; /** * Sum of bank codes in exchanges; used for control fields. * * @var integer $sum_bankcodes */ protected $sum_bankcodes; /** * Sum of account numbers in exchanges; used for control fields. * * @var integer $sum_accounts */ protected $sum_accounts; /** * Constructor. Creates an empty DTA object or imports one. * * If the parameter is a string, then it is expected to be in DTA format * an its content (sender and transactions) is imported. If the string cannot * be parsed at all then an empty DTA object with type DTA_CREDIT is returned. * If only parts of the string can be parsed, then all transactions before the * error are included into the object. * The user should use getParsingError() to check whether a parsing error occured. * * Otherwise the parameter has to be the type of the new DTA object, * either DTA_CREDIT or DTA_DEBIT. In this case exceptions are never * thrown to ensure compatibility. * * @param integer|string $type Either a string with DTA data or the type of the * new DTA file (DTA_CREDIT or DTA_DEBIT). Must be set. * * @access public */ function __construct($type) { parent::__construct(); $this->sum_bankcodes = 0; $this->sum_accounts = 0; if (is_int($type)) { $this->type = $type; } else { try { $this->parse($type); } catch (Payment_DTA_FatalParseException $e) { // cannot construct this object, reset everything parent::__construct(); $this->sum_bankcodes = 0; $this->sum_accounts = 0; $this->type = DTA_CREDIT; $this->allerrors[] = $e; } catch (Payment_DTA_Exception $e) { // object is valid, but save the error $this->allerrors[] = $e; } } } /** * Set the sender of the DTA file. Must be set for valid DTA file. * The given account data is also used as default sender's account. * Account data contains * name Sender's name. Maximally 27 chars are allowed. * bank_code Sender's bank code. * account_number Sender's account number. * additional_name If necessary, additional line for sender's name * (maximally 27 chars). * exec_date Optional execution date for the DTA file in format DDMMYYYY. * * @param array $account Account data for file sender. * * @access public * @return boolean */ function setAccountFileSender($account) { $account['account_number'] = strval($account['account_number']); $account['bank_code'] = strval($account['bank_code']); if (strlen($account['name']) > 0 && strlen($account['bank_code']) > 0 && strlen($account['bank_code']) <= 8 && ctype_digit($account['bank_code']) && strlen($account['account_number']) > 0 && strlen($account['account_number']) <= 10 && ctype_digit($account['account_number']) ) { if (empty($account['additional_name'])) { $account['additional_name'] = ""; } if (empty($account['exec_date']) || !ctype_digit($account['exec_date']) ) { $account['exec_date'] = str_repeat(" ", 8); } $this->account_file_sender = array( "name" => $this->filter($account['name'], 27), "bank_code" => $account['bank_code'], "account_number" => $account['account_number'], "additional_name" => $this->filter($account['additional_name'], 27), "exec_date" => $account['exec_date'] ); $result = true; } else { $result = false; } return $result; } /** * Auxillary method to fill and normalize the receiver and sender arrays. * * @param array $account_receiver Receiver's account data. * @param array $account_sender Sender's account data. * * @access private * @return array */ private function _exchangeFillArrays($account_receiver, $account_sender) { if (empty($account_receiver['additional_name'])) { $account_receiver['additional_name'] = ""; } if (empty($account_sender['name'])) { $account_sender['name'] = $this->account_file_sender['name']; } if (empty($account_sender['bank_code'])) { $account_sender['bank_code'] = $this->account_file_sender['bank_code']; } if (empty($account_sender['account_number'])) { $account_sender['account_number'] = $this->account_file_sender['account_number']; } if (empty($account_sender['additional_name'])) { $account_sender['additional_name'] = $this->account_file_sender['additional_name']; } $account_receiver['account_number'] = strval($account_receiver['account_number']); $account_receiver['bank_code'] = strval($account_receiver['bank_code']); $account_sender['account_number'] = strval($account_sender['account_number']); $account_sender['bank_code'] = strval($account_sender['bank_code']); return array($account_receiver, $account_sender); } /** * Adds an exchange. First the account data for the receiver of the exchange is * set. In the case the DTA file contains credits, this is the payment receiver. * In the other case (the DTA file contains debits), this is the account, from * which money is taken away. If the sender is not specified, values of the * file sender are used by default. * * Account data for receiver and sender contain * name Name. Maximally 27 chars are allowed. * bank_code Bank code. * account_number Account number. * additional_name If necessary, additional line for name (maximally 27 chars). * * @param array $account_receiver Receiver's account data. * @param double $amount Amount of money in this exchange. * Currency: EURO * @param array $purposes Array of up to 14 lines * (maximally 27 chars each) for * description of the exchange. * A string is accepted as well. * @param array $account_sender Sender's account data. * * @access public * @return boolean */ function addExchange( $account_receiver, $amount, $purposes, $account_sender = array() ) { list($account_receiver, $account_sender) = $this->_exchangeFillArrays($account_receiver, $account_sender); $cents = (int)(round($amount * 100)); if (strlen($account_sender['name']) > 0 && strlen($account_sender['bank_code']) > 0 && strlen($account_sender['bank_code']) <= 8 && ctype_digit($account_sender['bank_code']) && strlen($account_sender['account_number']) > 0 && strlen($account_sender['account_number']) <= 10 && ctype_digit($account_sender['account_number']) && strlen($account_receiver['name']) > 0 && strlen($account_receiver['bank_code']) <= 8 && ctype_digit($account_receiver['bank_code']) && strlen($account_receiver['account_number']) <= 10 && ctype_digit($account_receiver['account_number']) && is_numeric($amount) && $cents > 0 && $cents <= PHP_INT_MAX && $this->sum_amounts <= (PHP_INT_MAX - $cents) && ( (is_string($purposes) && strlen($purposes) > 0) || (is_array($purposes) && count($purposes) >= 1 && count($purposes) <= 14)) ) { $this->sum_amounts += $cents; $this->sum_bankcodes += $account_receiver['bank_code']; $this->sum_accounts += $account_receiver['account_number']; if (is_string($purposes)) { $filtered_purposes = str_split( $this->makeValidString($purposes), 27 ); $filtered_purposes = array_slice($filtered_purposes, 0, 14); } else { $filtered_purposes = array(); foreach ($purposes as $purposeline) { $filtered_purposes[] = $this->filter($purposeline, 27); } } $this->exchanges[] = array( "sender_name" => $this->filter( $account_sender['name'], 27 ), "sender_bank_code" => $account_sender['bank_code'], "sender_account_number" => $account_sender['account_number'], "sender_additional_name" => $this->filter( $account_sender['additional_name'], 27 ), "receiver_name" => $this->filter( $account_receiver['name'], 27 ), "receiver_bank_code" => $account_receiver['bank_code'], "receiver_account_number" => $account_receiver['account_number'], "receiver_additional_name" => $this->filter( $account_receiver['additional_name'], 27 ), "amount" => $cents, "purposes" => $filtered_purposes ); $result = true; } else { $result = false; } return $result; } /** * Auxillary method to write the A record. * * @access private * @return string */ private function _generateArecord() { $content = ""; // (field numbers according to ebics-zka.de specification) // A1 record length (128 Bytes) $content .= str_pad("128", 4, "0", STR_PAD_LEFT); // A2 record type $content .= "A"; // A3 file mode (credit or debit) // and Customer File ("K") / Bank File ("B") $content .= ($this->type == DTA_CREDIT) ? "G" : "L"; $content .= "K"; // A4 sender's bank code $content .= str_pad( $this->account_file_sender['bank_code'], 8, "0", STR_PAD_LEFT ); // A5 only used if Bank File, otherwise NULL $content .= str_repeat("0", 8); // A6 sender's name $content .= str_pad( $this->account_file_sender['name'], 27, " ", STR_PAD_RIGHT ); // A7 date of file creation $content .= strftime("%d%m%y", $this->timestamp); // A8 free (bank internal) $content .= str_repeat(" ", 4); // A9 sender's account number $content .= str_pad( $this->account_file_sender['account_number'], 10, "0", STR_PAD_LEFT ); // A10 sender's reference number (optional) $content .= str_repeat("0", 10); // A11a free (reserve) $content .= str_repeat(" ", 15); // A11b execution date ("DDMMYYYY", optional) $content .= $this->account_file_sender['exec_date']; // A11c free (reserve) $content .= str_repeat(" ", 24); // A12 currency (1 = Euro) $content .= "1"; assert(strlen($content) == 128); return $content; } /** * Auxillary method to write C records. * * @param array $exchange The transaction to serialize. * * @access private * @return string */ private function _generateCrecord($exchange) { // preparation of additional parts for record extensions $additional_parts = array(); $additional_purposes = $exchange['purposes']; $first_purpose = array_shift($additional_purposes); if (strlen($exchange['receiver_additional_name']) > 0) { $additional_parts[] = array("type" => "01", "content" => $exchange['receiver_additional_name'] ); } foreach ($additional_purposes as $additional_purpose) { $additional_parts[] = array("type" => "02", "content" => $additional_purpose ); } if (strlen($exchange['sender_additional_name']) > 0) { $additional_parts[] = array("type" => "03", "content" => $exchange['sender_additional_name'] ); } assert(count($additional_parts) <= 15); $content = ""; // (field numbers according to ebics-zka.de specification) // C1 record length (187 Bytes + 29 Bytes for each additional part) $content .= str_pad( 187 + count($additional_parts) * 29, 4, "0", STR_PAD_LEFT ); // C2 record type $content .= "C"; // C3 first involved bank $content .= str_pad( $exchange['sender_bank_code'], 8, "0", STR_PAD_LEFT ); // C4 receiver's bank code $content .= str_pad( $exchange['receiver_bank_code'], 8, "0", STR_PAD_LEFT ); // C5 receiver's account number $content .= str_pad( $exchange['receiver_account_number'], 10, "0", STR_PAD_LEFT ); // C6 internal customer number (11 chars) or NULL $content .= "0" . str_repeat("0", 11) . "0"; // C7a payment mode (text key) $content .= ($this->type == DTA_CREDIT) ? "51" : "05"; // C7b additional text key $content .= "000"; // C8 bank internal $content .= " "; // C9 free (reserve) $content .= str_repeat("0", 11); // C10 sender's bank code $content .= str_pad( $exchange['sender_bank_code'], 8, "0", STR_PAD_LEFT ); // C11 sender's account number $content .= str_pad( $exchange['sender_account_number'], 10, "0", STR_PAD_LEFT ); // C12 amount $content .= str_pad( $exchange['amount'], 11, "0", STR_PAD_LEFT ); // C13 free (reserve) $content .= str_repeat(" ", 3); // C14a receiver's name $content .= str_pad( $exchange['receiver_name'], 27, " ", STR_PAD_RIGHT ); // C14b delimitation $content .= str_repeat(" ", 8); /* first part/128 chars full */ // C15 sender's name $content .= str_pad( $exchange['sender_name'], 27, " ", STR_PAD_RIGHT ); // C16 first line of purposes $content .= str_pad($first_purpose, 27, " ", STR_PAD_RIGHT); // C17a currency (1 = Euro) $content .= "1"; // C17b free (reserve) $content .= str_repeat(" ", 2); // C18 number of additional parts (00-15) $content .= str_pad(count($additional_parts), 2, "0", STR_PAD_LEFT); /* * End of the constant part (187 chars), * now up to 15 extensions with 29 chars each might follow. */ if (count($additional_parts) == 0) { // no extension, pad to fill the part to 2*128 chars $content .= str_repeat(" ", 256-187); } else { // The first two extensions fit into the current part: for ($index = 1;$index <= 2;$index++) { if (count($additional_parts) > 0) { $additional_part = array_shift($additional_parts); } else { $additional_part = array("type" => " ", "content" => "" ); } // C19/21 type of addional part $content .= $additional_part['type']; // C20/22 additional part content $content .= str_pad( $additional_part['content'], 27, " ", STR_PAD_RIGHT ); } // delimitation $content .= str_repeat(" ", 11); } // For more extensions add up to 4 more parts: for ($part = 3;$part <= 5;$part++) { if (count($additional_parts) > 0) { for ($index = 1;$index <= 4;$index++) { if (count($additional_parts) > 0) { $additional_part = array_shift($additional_parts); } else { $additional_part = array("type" => " ", "content" => "" ); } // C24/26/28/30 type of addional part $content .= $additional_part['type']; // C25/27/29/31 additional part content $content .= str_pad( $additional_part['content'], 27, " ", STR_PAD_RIGHT ); } // C32 delimitation $content .= str_repeat(" ", 12); } } // with 15 extensions there may be a 6th part if (count($additional_parts) > 0) { $additional_part = array_shift($additional_parts); // C24 type of addional part $content .= $additional_part['type']; // C25 additional part content $content .= str_pad( $additional_part['content'], 27, " ", STR_PAD_RIGHT ); // padding to fill the part $content .= str_repeat(" ", 128-27-2); } assert(count($additional_parts) == 0); assert(strlen($content) % 128 == 0); return $content; } /** * Auxillary method to write the E record. * * @access private * @return string */ private function _generateErecord() { $content = ""; // (field numbers according to ebics-zka.de specification) // E1 record length (128 bytes) $content .= str_pad("128", 4, "0", STR_PAD_LEFT); // E2 record type $content .= "E"; // E3 free (reserve) $content .= str_repeat(" ", 5); // E4 number of records type C $content .= str_pad(count($this->exchanges), 7, "0", STR_PAD_LEFT); // E5 free (reserve) $content .= str_repeat("0", 13); // use number_format() to ensure proper integer formatting // E6 sum of account numbers $content .= str_pad( number_format($this->sum_accounts, 0, "", ""), 17, "0", STR_PAD_LEFT ); // E7 sum of bank codes $content .= str_pad( number_format($this->sum_bankcodes, 0, "", ""), 17, "0", STR_PAD_LEFT ); // E8 sum of amounts $content .= str_pad( number_format($this->sum_amounts, 0, "", ""), 13, "0", STR_PAD_LEFT ); // E9 delimitation $content .= str_repeat(" ", 51); assert(strlen($content) % 128 == 0); return $content; } /** * Returns the full content of the generated DTA file. * All added exchanges are processed. * * @access public * @return string */ function getFileContent() { $content = ""; /** * data record A */ $content .= $this->_generateArecord(); /** * data record(s) C */ $sum_account_numbers = 0; $sum_bank_codes = 0; $sum_amounts = 0; foreach ($this->exchanges as $exchange) { $sum_account_numbers += $exchange['receiver_account_number']; $sum_bank_codes += (int) $exchange['receiver_bank_code']; $sum_amounts += (int) $exchange['amount']; $content .= $this->_generateCrecord($exchange); assert(strlen($content) % 128 == 0); } assert($this->sum_amounts === $sum_amounts); assert($this->sum_bankcodes === $sum_bank_codes); assert($this->sum_accounts === $sum_account_numbers); /** * data record E */ $content .= $this->_generateErecord(); return $content; } /** * Returns an array with information about the transactions. * Can be used to print an accompanying document (Begleitzettel) for disks. * * @access public * @return array Returns an array with keys: "sender_name", * "sender_bank_code", "sender_account", "sum_amounts", * "type", "sum_bankcodes", "sum_accounts", "count", "date", "exec_date" */ function getMetaData() { $meta = parent::getMetaData(); $meta["sum_bankcodes"] = floatval($this->sum_bankcodes); $meta["sum_accounts"] = floatval($this->sum_accounts); $meta["type"] = strval(($this->type == DTA_CREDIT) ? "CREDIT" : "DEBIT"); $meta["exec_date"] = $meta["date"]; // use timestamp to be consistent with $meta["date"] if (trim($this->account_file_sender["exec_date"]) !== "") { $ftime = strptime($this->account_file_sender["exec_date"], '%d%m%Y'); if ($ftime) { $meta["exec_date"] = mktime( 0, 0, 0, $ftime['tm_mon'] + 1, $ftime['tm_mday'], $ftime['tm_year'] + 1900 ); } } return $meta; } /** * Auxillary parser to consume A records. * * @param string $input content of DTA file * @param integer &$offset read offset into $input * * @throws Payment_DTA_Exception on unrecognized input * @access private * @return void */ private function _parseArecord($input, &$offset) { /* field 1+2 */ $this->checkStr($input, $offset, "0128A"); /* field 3 */ $type = $this->getStr($input, $offset, 2); /* field 4 */ $Asender_blz = $this->getNum($input, $offset, 8); /* field 5 */ $this->checkStr($input, $offset, "00000000"); /* field 6 */ $Asender_name = rtrim($this->getStr($input, $offset, 27, true)); /* field 7 */ $Adate_day = $this->getNum($input, $offset, 2); $Adate_month = $this->getNum($input, $offset, 2); $Adate_year = $this->getNum($input, $offset, 2); $this->timestamp = mktime( 0, 0, 0, intval($Adate_month), intval($Adate_day), intval($Adate_year) ); /* field 8 */ $this->checkStr($input, $offset, " "); /* field 9 */ $Asender_account = $this->getNum($input, $offset, 10); /* field 10 */ $this->checkStr($input, $offset, "0000000000"); /* field 11a */ $this->checkStr($input, $offset, str_repeat(" ", 15)); /* field 11b */ $Aexec_date = $this->getStr($input, $offset, 8); /* field 11c */ $this->checkStr($input, $offset, str_repeat(" ", 24)); /* field 12 */ $this->checkStr($input, $offset, "1"); /* the first char G/L indicates credit and debit exchanges * the second char K/B indicates a customer or bank file * (I do not know if bank files should be treated different) */ if ($type === "GK" || $type === "GB") { $this->type = DTA_CREDIT; } elseif ($type === "LK" || $type === "LB") { $this->type = DTA_DEBIT; } else { throw new Payment_DTA_FatalParseException( "Invalid type indicator: '$type', expected ". "either 'GK'/'GB' or 'LK'/'LB' (@offset 6)." ); } /* * additional_name is problematic and cannot be parsed & reproduced. * it is set as part of the AccountFileSender, but appears as part * of every transaction. */ $rc = $this->setAccountFileSender( array( "name" => $Asender_name, "bank_code" => $Asender_blz, "account_number" => $Asender_account, "additional_name" => '', "exec_date" => $Aexec_date ) ); if (!$rc) { // should never happen throw new Payment_DTA_FatalParseException( "Cannot setAccountFileSender(), please file a bug report" ); } // currently not a TODO: // does anyone have to preserve the creation date or execution date? } /** * Auxillary method to parse C record extensions. * * Reads the variable number of extensions at the end of a C record. * * @param string $input content of DTA file * @param integer &$offset read offset into $input * @param integer $extensions expected number of extensions * @param integer $c_start C record offset (for exceptions) * * @throws Payment_DTA_ParseException on invalid extensions * @access private * @return array of $Cpurpose, 2nd sender line, 2nd receiver line */ private function _parseCextension($input, &$offset, $extensions, $c_start) { $extensions_read = array(); // first handle the up to 2 extensions inside the 2nd part if ($extensions == 0) { // only padding $this->checkStr($input, $offset, str_repeat(" ", 69)); } elseif ($extensions == 1) { /* field 19 */ $ext_type = $this->getNum($input, $offset, 2); /* field 20 */ $ext_content = $this->getStr($input, $offset, 27, true); array_push($extensions_read, array($ext_type, $ext_content)); /* fields 21,22,23 */ $this->checkStr($input, $offset, str_repeat(" ", 2+27+11)); } else { /* field 19 */ $ext_type = $this->getNum($input, $offset, 2); /* field 20 */ $ext_content = $this->getStr($input, $offset, 27, true); array_push($extensions_read, array($ext_type, $ext_content)); /* field 21 */ $ext_type = $this->getNum($input, $offset, 2); /* field 22 */ $ext_content = $this->getStr($input, $offset, 27, true); array_push($extensions_read, array($ext_type, $ext_content)); /* fields 23 */ $this->checkStr($input, $offset, str_repeat(" ", 11)); } // end 2nd part of C record assert($offset % 128 === 0); // up to 4 more parts, each with 128 bytes & up to 4 extensions while (count($extensions_read) < $extensions) { $ext_in_part = $extensions - count($extensions_read); // one switch to read the content switch($ext_in_part) { default: // =4 case 4: /* fallthrough */ $ext_type = $this->getNum($input, $offset, 2); $ext_content = $this->getStr($input, $offset, 27, true); array_push($extensions_read, array($ext_type, $ext_content)); case 3: /* fallthrough */ $ext_type = $this->getNum($input, $offset, 2); $ext_content = $this->getStr($input, $offset, 27, true); array_push($extensions_read, array($ext_type, $ext_content)); case 2: /* fallthrough */ $ext_type = $this->getNum($input, $offset, 2); $ext_content = $this->getStr($input, $offset, 27, true); array_push($extensions_read, array($ext_type, $ext_content)); case 1: /* fallthrough */ $ext_type = $this->getNum($input, $offset, 2); $ext_content = $this->getStr($input, $offset, 27, true); array_push($extensions_read, array($ext_type, $ext_content)); break; case 0: // should never happen throw new Payment_DTA_ParseException( 'confused about number of extensions in transaction number '. strval($this->count()+1) .' @ offset '. strval($c_start) . ', please file a bug report' ); } // and one switch for the padding switch($ext_in_part) { case 1: $this->checkStr($input, $offset, str_repeat(" ", 29)); case 2: /* fallthrough */ $this->checkStr($input, $offset, str_repeat(" ", 29)); case 3: /* fallthrough */ $this->checkStr($input, $offset, str_repeat(" ", 29)); case 4: /* fallthrough */ default: /* fallthrough */ $this->checkStr($input, $offset, str_repeat(" ", 12)); break; } // end n-th part of C record assert($offset % 128 === 0); } return $extensions_read; } /** * Auxillary method to combine C record extensions. * * Takes the parsed extensions to check the allowed number of them per type * and to collect all purpose lines into one array. * * @param array $extensions_read read extensions as arrays * @param array $Cpurpose existing array of purpose lines * @param integer $c_start C record offset (for exceptions) * * @throws Payment_DTA_ParseException on invalid extensions * @access private * @return array of $Cpurpose, 2nd sender line, 2nd receiver line */ private function _processCextension($extensions_read, $Cpurpose, $c_start) { $Csender_name2 = ""; $Creceiver_name2 = ""; foreach ($extensions_read as $ext) { $ext_type = $ext[0]; $ext_content = $ext[1]; switch($ext_type) { case 1: if (!empty($Creceiver_name2)) { throw new Payment_DTA_ParseException( 'multiple receiver name extensions in transaction number '. strval($this->count()+1) .' @ offset '. strval($c_start) ); } else { $Creceiver_name2 = $ext_content; } break; case 2: if (count($Cpurpose) >= 14) { // allowed: 1 line in fixed part + 13 in extensions throw new Payment_DTA_ParseException( 'too many purpose extensions in transaction number '. strval($this->count()+1) .' @ offset '. strval($c_start) ); } else { array_push($Cpurpose, $ext_content); } break; case 3: if (!empty($Csender_name2)) { throw new Payment_DTA_ParseException( 'multiple receiver name extensions in transaction number '. strval($this->count()+1) .' @ offset '. strval($c_start) ); } else { $Csender_name2 = $ext_content; } break; default: throw new Payment_DTA_ParseException( 'invalid extension type in transaction number '. strval($this->count()+1) .' @ offset '. strval($c_start) ); } } return array($Cpurpose, $Csender_name2, $Creceiver_name2); } /** * Auxillary parser to consume C records. * * @param string $input content of DTA file * @param integer &$offset read offset into $input * @param array &$checks holds checksums for validation in E record * * @throws Payment_DTA_Exception on unrecognized input * @access private * @return void */ private function _parseCrecord($input, &$offset, &$checks) { // save for possible exceptions $c_start = $offset; /* field 1 */ $record_length = $this->getNum($input, $offset, 4); /* field 2 */ $this->checkStr($input, $offset, "C"); // check the record length if (($record_length-187)%29) { throw new Payment_DTA_ParseException('invalid C record length'); } $extensions_length = ($record_length-187)/29; /* field 3 */ $Cbank_blz = $this->getNum($input, $offset, 8); // usually 0, ignored /* field 4 */ $Creceiver_blz = $this->getNum($input, $offset, 8); /* field 5 */ $Creceiver_account = $this->getNum($input, $offset, 10); /* field 6 */ $this->checkStr($input, $offset, "0"); // either 0s or aninternal customer number: $this->getNum($input, $offset, 11); $this->checkStr($input, $offset, "0"); /* field 7 */ // may hold about a dozen values with details about the type of transaction $Ctype = $this->getStr($input, $offset, 5); if ( (($this->type == DTA_DEBIT) && (!preg_match('/^0[45]\d{3}$/', $Ctype))) || (($this->type == DTA_CREDIT) && (!preg_match('/^5\d{4}$/', $Ctype))) ) { throw new Payment_DTA_ParseException( 'C record type of payment (' . $Ctype . ') '. 'does not match A record type indicator '. '(' . (($this->type == DTA_CREDIT) ? "CREDIT" : "DEBIT") . ') '. 'in transaction number '. strval($this->count()+1) . ' @ offset '. strval($c_start) ); } /* field 8 */ $this->checkStr($input, $offset, " "); /* field 9 */ $this->checkStr($input, $offset, "00000000000"); /* field 10 */ $Csender_blz = $this->getNum($input, $offset, 8); /* field 11 */ $Csender_account = $this->getNum($input, $offset, 10); /* field 12 */ $Camount = $this->getNum($input, $offset, 11); /* field 13 */ $this->checkStr($input, $offset, " "); /* field 14a */ $Creceiver_name = rtrim($this->getStr($input, $offset, 27, true)); /* field 14b */ $this->checkStr($input, $offset, " "); // end 1st part of C record assert($offset % 128 === 0); /* field 15 */ $Csender_name = rtrim($this->getStr($input, $offset, 27, true)); /* field 16 */ $Cpurpose = array(rtrim($this->getStr($input, $offset, 27, true))); /* field 17a */ $this->checkStr($input, $offset, "1"); /* field 17b */ $this->checkStr($input, $offset, " "); /* field 18 */ $extensions = $this->getNum($input, $offset, 2); if ($extensions != $extensions_length) { throw new Payment_DTA_ParseException( 'number of extensions '. 'does not match record length in transaction number '. strval($this->count()+1) .' @ offset '. strval($c_start) ); } // extensions to C record, read into array & processed later $extensions_read = $this->_parseCextension($input, $offset, $extensions, $c_start); // process read extension content list($Cpurpose, $Csender_name2, $Creceiver_name2) = $this->_processCextension($extensions_read, $Cpurpose, $c_start); /* we read the fields, now add an exchange */ $rc = $this->addExchange( array( 'name' => $Creceiver_name, 'bank_code' => $Creceiver_blz, 'account_number' => $Creceiver_account, 'additional_name' => $Creceiver_name2 ), $Camount/100.0, $Cpurpose, array( 'name' => $Csender_name, 'bank_code' => $Csender_blz, 'account_number' => $Csender_account, 'additional_name' => $Csender_name2 ) ); if (!$rc) { // should never happen throw new Payment_DTA_ParseException( 'Cannot addExchange() for transaction number '. strval($this->count()+1) . ' @ offset '. strval($c_start). ', please file a bug report' ); } $checks['account'] += $Creceiver_account; $checks['blz'] += $Creceiver_blz; $checks['amount'] += $Camount; } /** * Auxillary parser to consume E records. * * @param string $input content of DTA file * @param integer &$offset read offset into $input * @param array $checks holds checksums for validation * * @throws Payment_DTA_Exception on unrecognized input * @access private * @return void */ private function _parseErecord($input, &$offset, $checks) { /* field 1+2 */ $this->checkStr($input, $offset, "0128E"); /* field 3 */ $this->checkStr($input, $offset, " "); /* field 4 */ $E_check_count = $this->getNum($input, $offset, 7); /* field 5 */ $this->checkStr($input, $offset, str_repeat("0", 13)); /* field 6 */ $E_check_account = $this->getNum($input, $offset, 17); /* field 7 */ $E_check_blz = $this->getNum($input, $offset, 17); /* field 8 */ $E_check_amount = $this->getNum($input, $offset, 13); /* field 9 */ $this->checkStr($input, $offset, str_repeat(" ", 51)); // end of E record assert($offset % 128 === 0); // check checksums /* * NB: because errors are indicated by exceptions, the user/caller never * sees more than one checksum error. Only the first mismatch is reported, * the other checks are skipped by throwing the exception. */ if ($E_check_count != $this->count()) { throw new Payment_DTA_ChecksumException( "E record checksum mismatch for transaction count: ". "reads $E_check_count, expected ".$this->count() ); } if ($E_check_account != $checks['account']) { throw new Payment_DTA_ChecksumException( "E record checksum mismatch for account numbers: ". "reads $E_check_account, expected ".$checks['account'] ); } if ($E_check_blz != $checks['blz']) { throw new Payment_DTA_ChecksumException( "E record checksum mismatch for bank codes: ". "reads $E_check_blz, expected ".$checks['blz'] ); } if ($E_check_amount != $checks['amount']) { throw new Payment_DTA_ChecksumException( "E record checksum mismatch for transfer amount: ". "reads $E_check_amount, expected ".$checks['amount'] ); } } /** * Parser. Read data from an existing DTA file content. * * Parsing can leave us with four situations: * - the input is parsed correctly => valid DTA object. * - the input is parsed but a checksum does not match the data read * => valid DTA object. * throws a Payment_DTA_ChecksumException. * - the n-th transaction cannot be parsed => parsing stops there, yielding * a valid DTA object, but with only the first n-1 transactions * and without checksum verification. * throws a Payment_DTA_ParseException. * - a parsing error occurs in the A record => the DTA object is invalid * throws a Payment_DTA_FatalParseException. * * @param string $input content of DTA file * * @throws Payment_DTA_Exception on unrecognized input * @access protected * @return void */ protected function parse($input) { /* * Open Questions/TODOs for the parsing code: * - Are the provided exceptions adequate? (Or are they too verbose for * practical use or OTOH not detailed enough to really handle errors?) * - Should we try to parse truncated files, i.e. ones with a wrong length? * - Should we try to find records with a wrong offset, e.g. when an * encoding error shifts all following records 4 bytes backwards? * - Should we abort on any error or rather skip the exchange and continue? * In the later case we need a way to preserve/indicate the problem * because any simple ParseException in a C record will be masked by * a resulting ChecksumException in the E record. * - TODO: We should read non-ASCII chars in A/C records. Some programs * write 8-bit chars into the fields. */ if (strlen($input) % 128) { throw new Payment_DTA_FatalParseException("invalid length"); } $checks = array( 'account' => 0, 'blz' => 0, 'amount' => 0); $offset = 0; /* A record */ try { $this->_parseArecord($input, $offset); } catch (Payment_DTA_Exception $e) { throw new Payment_DTA_FatalParseException("Exception in A record", $e); } //do not consume input by using getStr()/getNum() here while ($input[$offset + 4] == 'C') { /* C record */ $c_start = $offset; $c_length = intval(substr($input, $offset, 4)); try { $this->_parseCrecord($input, $offset, $checks); } catch (Payment_DTA_Exception $e) { // preserve error $this->allerrors[] = new Payment_DTA_ParseException( "Error in C record, in transaction number ". strval($this->count()+1) ." @ offset ". strval($c_start), $e ); // skip to next 128-byte aligned record $offset = $c_start + 128 * (1 + intval($c_length/128)); } } // while /* E record */ try { $this->_parseErecord($input, $offset, $checks); } catch (Payment_DTA_ChecksumException $e) { throw $e; } catch (Payment_DTA_Exception $e) { throw new Payment_DTA_ParseException("Error in E record", $e); } } }