<?php
/**
 * Class represents records from table invoice_log
 * {autogenerated}
 * @property int $log_id PK
 * @property datetime $tm
 * @property int|null $invoice_id FK invoice
 * @property int|null $user_id FK user
 * @property string|null $paysys_id
 * @property string|null $type
 * @property string|null $title
 * @property string|null $details
 * @property string|null $remote_addr
 * @property int|null $is_processed
 * {/autogenerated}
 * @package Am_Invoice
 * @todo COMPRESS()/UNCOMPRESS() values to save storage
 */

class InvoiceLog extends Am_Record
{
    protected $_mask = [];
    protected $_disablePostbackLog = false;
    protected $_disableMask = false;

    function init()
    {
        if (Am_Di::getInstance()->config->get('disable_invoice_log')) {
            $this->toggleDisablePostbackLog(true);
        }
    }

    function toggleDisablePostbackLog($flag)
    {
        $this->_disablePostbackLog = (bool)$flag;
        return $this;
    }

    function setInvoice(Invoice $invoice)
    {
        $this->invoice_id = $invoice->invoice_id;
        $this->user_id = $invoice->user_id;
        return $this;
    }

    function setProcessed()
    {
        if (!$this->_disablePostbackLog)
            $this->updateQuick('is_processed', true);
    }

    function renderTable($kvRows)
    {
        $h = "<table class='logdetail'>\n";
        foreach ($kvRows as $k => $v)
            $h .= sprintf("<tr><th>%s</th><td>%s</td></tr>\n",
                Am_Html::escape($k), Am_Html::escape($v));
        $h .= "</table>\n";
        return $h;
    }

    function serializeArrayItem(XMLWriter $x, $item)
    {
        if (is_array($item)) {
            $x->startElement('params');
            foreach ($item as $k => $v) {
                $x->startElement('p');
                $x->writeAttribute('name', $k);
                if (is_array($v)) $x->writeAttribute('type', 'array');
                $this->serializeArrayItem($x, $v);
                $x->endElement();
            }
            $x->endElement();
        } else {
            $x->text(is_scalar($item) ? $item : serialize($item));
        }
    }

    function serializeItem($vars)
    {
        $x = new XMLWriter();
        $x->openMemory();
        $x->setIndent(true);
        $x->startElement('invoice-log-item');

        if (is_array($vars)) {
            $x->writeAttribute('type', 'array');
            $this->serializeArrayItem($x, $vars);
        } elseif ($vars instanceof Am_Mvc_Request) {
            $x->writeAttribute('type', 'incoming-request');
            $vars->toXml($x);
        } elseif ($vars instanceof Am_Paysystem_Action) {
            $x->writeAttribute('type', 'paysystem-action');
            $x->writeAttribute('class', get_class($vars));
            $vars->toXml($x);
        } elseif ($vars instanceof Am_HttpRequest) {
            $x->writeAttribute('type', 'outgoing-request');
            $vars->toXml($x, false);
        } elseif ($vars instanceof HTTP_Request2_Response) {
            $x->writeAttribute('type', 'outgoing-request-response');
            $x->startElement('status');
            $x->writeAttribute('code', $vars->getStatus());
            $x->writeAttribute('reason', $vars->getReasonPhrase());
            $x->endElement();
            $x->startElement('headers');
            foreach ($vars->getHeader(null) as $k => $v)
            {
                $x->startElement('header');
                $x->writeAttribute('name', $k);
                $x->text($v);
                $x->endElement();
            }
            $x->endElement();

            $x->startElement('body');
            $x->writeCdata($vars->getBody());
            $x->endElement();

            $x->endElement();
        }
        elseif ($vars instanceof Exception)
        {
            $x->writeAttribute('type', 'exception');
            $x->writeElement('type', get_class($vars));
            $x->writeElement('message', $vars->getMessage());
            $x->startElement('trace');
            foreach ($vars->getTrace() as $t)
            {
                $x->startElement('call');
                if (isset($t['class']))
                {
                    $x->writeElement('function', $t['class'] . $t['type'] . $t['function']);
                } else {
                    $x->writeElement('function', $t['function']);
                }
                $x->writeElement('file', @$t['file']);
                $x->writeElement('line', @$t['line']);
                $x->endElement();
            }
            $x->endElement();
        } elseif(is_object($vars) && method_exists($vars, 'toArray')) {
            $x->writeAttribute('type', 'array');
            $this->serializeArrayItem($x, $vars->toArray());
        } else {
            $x->writeAttribute('type', gettype($vars));
            $x->writeCdata((string)$vars);
        }
        $x->endElement(); // invoice-log-item
        return $x->flush();
    }

    function add($vars, $commit = true)
    {
        if ($vars instanceof Am_Mvc_Request)
        {
            $vars = $this->serializeItem($vars);
        }
        else
        {
            $vars = $this->serializeItem($vars);
            if (!$this->_disableMask)
            {
                // credit card# has length from 13 to 16 as defined in ccvs
                $vars = preg_replace('/(?<!line)>\d{3}</', '>***<', $vars);
                $vars = preg_replace('/CDATA\[\d{3}\]\]/', 'CDATA[***]]', $vars);
                $vars = preg_replace('/(\&gt\;)(\d{3})(\&lt\;)/', '&gt;***&lt;', $vars);
                $vars = preg_replace_callback('/(\&gt\;)(\d{13,16})(\&lt\;)/', [$this, '_maskCc'], $vars);
                $vars = preg_replace_callback('/(>)(\d{13,16})(<)/', [$this, '_maskCc'], $vars);
                $vars = preg_replace_callback('/(\")(\d{13,16})(\")/', [$this, '_maskCc'], $vars);
                $vars = preg_replace_callback('/(\')(\d{13,16})(\')/', [$this, '_maskCc'], $vars);
                $vars = preg_replace_callback('/(CDATA\[)(\d{13,16})(\]\])/', [$this, '_maskCc'], $vars);
                $vars = str_replace(array_keys($this->_mask), array_values($this->_mask), $vars);
            }
        }
        if (!empty($this->details))
        {
            $this->details = str_replace('</invoice-log>', $vars."</invoice-log>", $this->details);
        } else {
            $this->details =
                '<?xml version="1.0"?>'.PHP_EOL
                .'<invoice-log>'.PHP_EOL
                .$vars
                .'</invoice-log>';
        }
        if ($commit && !$this->_disablePostbackLog) $this->save();
    }

    public function update()
    {
        if (!$this->_disablePostbackLog)
            parent::update();
        return $this;
    }

    public function insert($reload = true)
    {
        if (!$this->_disablePostbackLog)
            parent::insert($reload);
        return $this;
    }

    /**
     * Automatically replace occurences of $data in log records to $replacement (***)
     * @link add()
     */
    function mask($data, $replacement = null)
    {
        $this->_mask[(string)$data] = $replacement ?: str_repeat('*', strlen($data));
        return $this;
    }

    function _maskCc($regs)
    {
        $cc = $regs[2];
        return $regs[1].str_repeat('*', strlen($cc)-4) . substr($cc, -4).$regs[3];
    }

    function getRenderedDetails()
    {
        if (empty($this->details)) return [];
        $ret = [];
        foreach ($this->getXmlDetails() as $a)
        {
            $popup = null;
            list($type, $source) = $a;
            if (preg_match('|^\s*<.*>\s*$|ms', $source))
            {
                $x = new SimpleXMLElement($source);
                if (($body = (string)$x->body))
                {
                    list($t) = $x->xpath("headers/header[@name='content-type']");
                    if ($t) {
                        list($t) = explode(";", $t);
                    }
                    $popup = $this->renderVars($body, (string)$t);
                }
            }
            $source = highlight_string($source, true);
            $ret[] = [$type, $source, $popup];
        }
        return $ret;
    }

    /**
     * return @array details as array(type,xmlsource)
     */
    function getXmlDetails()
    {
        $ret = [];
        $x = simplexml_load_string($this->details);
        foreach($x->{'invoice-log-item'} as $i)
        {
            $ret[] = [(string)$i->attributes()->type, $i->asXML()];
        }
        return $ret;
    }

    function renderVars($body, $type = null)
    {
        switch ($type) {
            case 'application/x-www-form-urlencoded':
                $str = urldecode($body);
                parse_str($str, $arr);
                break;
            case 'application/json' :
                $arr = json_decode($body, true);
                break;
            default:
                $arr = [];
        }
        if (!count($arr)) return "";
        return print_r($arr, true);
    }

    /** @return Am_Mvc_Request|null */
    function getFirstRequest()
    {
        if (empty($this->details)) return;
        $x = new SimpleXMLElement($this->details);
        $k = 'invoice-log-item';
        foreach ($x->{'invoice-log-item'} as $item)
        {
            if ($item['type'] == 'incoming-request')
                return Am_Mvc_Request::fromXml($item);
        }
    }

    function toggleMask($flag = false)
    {
        $this->_disableMask = !$flag;
    }
}

/**
 * @extends Am_Table<InvoiceLog>
 * @method InvoiceLog[] findByInvoiceId($value, $start = null, $count = null, $orderBy = null)
 */
class InvoiceLogTable extends Am_Table
{
    protected $_key = 'log_id';
    protected $_table = '?_invoice_log';
    protected $_recordClass = 'InvoiceLog';

    public function insert(array $values, $returnInserted = false)
    {
        if (empty($values['tm']))
            $values['tm'] = $this->getDi()->sqlDateTime;
        if (empty($values['remote_addr']))
            $values['remote_addr'] = $_SERVER['REMOTE_ADDR'];
        return parent::insert($values, $returnInserted);
    }

    function log($invoice_id, $paysys_id, $title, $vars=null, $type='info', $remote_addr=null, $tm=null)
    {
        $r = $this->createRecord();
        if ($invoice = $this->getDi()->invoiceTable->load($invoice_id, false))
            $r->setInvoice($invoice);
        $r->paysys_id = $paysys_id;
        $r->title = $title;
        $r->remote_addr = get_first($remote_addr, $_SERVER['REMOTE_ADDR']);
        $r->add($vars, false);
        $r->insert();
        return $r;
    }

    function clearOld($date)
    {
        $this->_db->query("DELETE FROM ?_invoice_log WHERE `tm` < ?", $date . ' 00:00:00');
    }
}