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