<?php

/*
@homepage <https://github.com/semsol/arc2>
@license W3C Software License and GPL

class:    ARC2 Legaxy XML Parser
author:   Benjamin Nowack
version:  2010-11-16
*/

ARC2::inc('Class');

class ARC2_LegacyXMLParser extends ARC2_Class
{
    public function __construct($a, &$caller)
    {
        parent::__construct($a, $caller);
    }

    public function __init()
    {/* reader */
        parent::__init();
        $this->encoding = $this->v('encoding', false, $this->a);
        $this->state = 0;
        $this->x_base = $this->base;
        $this->xml = 'http://www.w3.org/XML/1998/namespace';
        $this->rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
        $this->nsp = [$this->xml => 'xml', $this->rdf => 'rdf'];
        $this->allowCDataNodes = 1;
        $this->target_encoding = '';
        $this->keep_cdata_ws = $this->v('keep_cdata_whitespace', 0, $this->a);
    }

    public function setReader(&$reader)
    {
        $this->reader = $reader;
    }

    public function parse($path, $data = '', $iso_fallback = false)
    {
        $this->nodes = [];
        $this->node_count = 0;
        $this->level = 0;
        /* reader */
        if (!$this->v('reader')) {
            ARC2::inc('Reader');
            $this->reader = new ARC2_Reader($this->a, $this);
        }
        $this->reader->setAcceptHeader('Accept: application/xml; q=0.9, */*; q=0.1');
        $this->reader->activate($path, $data);
        $this->x_base = isset($this->a['base']) && $this->a['base'] ? $this->a['base'] : $this->reader->base;
        $this->base = $this->x_base;
        $this->doc_url = $this->reader->base;
        /* xml parser */
        $this->initXMLParser();
        /* parse */
        $first = true;
        while ($d = $this->reader->readStream(1)) {
            if ($iso_fallback && $first) {
                $d = '<?xml version="1.0" encoding="ISO-8859-1"?>'."\n".preg_replace('/^\<\?xml [^\>]+\?\>\s*/s', '', $d);
            }
            if (!xml_parse($this->xml_parser, $d, false)) {
                $error_str = xml_error_string(xml_get_error_code($this->xml_parser));
                $line = xml_get_current_line_number($this->xml_parser);
                if (!$iso_fallback && preg_match('/Invalid character/i', $error_str)) {
                    xml_parser_free($this->xml_parser);
                    unset($this->xml_parser);
                    $this->reader->closeStream();
                    unset($this->reader);
                    $this->__init();
                    $this->encoding = 'ISO-8859-1';
                    $this->initXMLParser();

                    return $this->parse($path, $data, true);
                } else {
                    return $this->addError('XML error: "'.$error_str.'" at line '.$line.' (parsing as '.$this->getEncoding().')');
                }
            }
            $first = false;
        }
        $this->target_encoding = xml_parser_get_option($this->xml_parser, \XML_OPTION_TARGET_ENCODING);
        xml_parser_free($this->xml_parser);
        $this->reader->closeStream();
        unset($this->reader);

        return $this->done();
    }

    public function getEncoding($src = 'config')
    {
        if ('parser' == $src) {
            return $this->target_encoding;
        } elseif (('config' == $src) && $this->encoding) {
            return $this->encoding;
        }

        return $this->reader->getEncoding();
    }

    public function done()
    {
    }

    public function getStructure()
    {
        return ['nodes' => $this->v('nodes', [])];
    }

    public function getNodeIndex()
    {
        if (!isset($this->node_index)) {
            /* index by parent */
            $index = [];
            for ($i = 0, $i_max = count($this->nodes); $i < $i_max; ++$i) {
                $node = $this->nodes[$i];
                $node['id'] = $i;
                $node['doc_base'] = $this->base;
                if (isset($this->doc_url)) {
                    $node['doc_url'] = $this->doc_url;
                }
                $this->updateNode($node);
                $p_id = $node['p_id'];
                if (!isset($index[$p_id])) {
                    $index[$p_id] = [];
                }
                $index[$p_id][$node['pos']] = $node;
            }
            $this->node_index = $index;
        }

        return $this->node_index;
    }

    public function getNodes()
    {
        return $this->nodes;
    }

    public function getSubNodes($n)
    {
        return $this->v($n['id'], [], $this->getNodeIndex());
    }

    public function getNodeContent($n, $outer = 0, $trim = 1)
    {
        // echo '<pre>' . htmlspecialchars(print_r($n, 1)) . '</pre>';
        if ('cdata' == $n['tag']) {
            $r = $n['a']['value'];
        } else {
            $r = '';
            if ($outer) {
                $r .= '<'.$n['tag'];
                asort($n['a']);
                if (isset($n['a']['xmlns']) && $n['a']['xmlns']['']) {
                    $r .= ' xmlns="'.$n['a']['xmlns'][''].'"';
                }
                foreach ($n['a'] as $a => $val) {
                    $r .= preg_match('/^[^\s]+$/', $a) && !is_array($val) ? ' '.$a.'="'.addslashes($val).'"' : '';
                }
                $r .= $n['empty'] ? '/>' : '>';
            }
            if (!$n['empty']) {
                $r .= $this->v('cdata', '', $n);
                $sub_nodes = $this->getSubNodes($n);
                foreach ($sub_nodes as $sub_n) {
                    $r .= $this->getNodeContent($sub_n, 1, 0);
                }
                if ($outer) {
                    $r .= '</'.$n['tag'].'>';
                }
            }
        }

        return ($trim && !$this->keep_cdata_ws) ? trim($r) : $r;
    }

    public function pushNode($n)
    {
        $n['id'] = $this->node_count;
        $this->nodes[$this->node_count] = $n;
        ++$this->node_count;
    }

    public function getCurNode($t = '')
    {
        $i = 1;
        do {
            $r = $this->node_count ? $this->nodes[$this->node_count - $i] : 0;
            $found = (!$t || ($r['tag'] == $t)) ? 1 : 0;
            ++$i;
        } while (!$found && isset($this->nodes[$this->node_count - $i]));

        return $r;
    }

    public function updateNode($node)
    {/* php4-save */
        $this->nodes[$node['id']] = $node;
    }

    public function initXMLParser()
    {
        if (!isset($this->xml_parser)) {
            $enc = preg_match('/^(utf\-8|iso\-8859\-1|us\-ascii)$/i', $this->getEncoding(), $m) ? $m[1] : 'UTF-8';
            $parser = xml_parser_create_ns($enc, '');
            xml_parser_set_option($parser, \XML_OPTION_SKIP_WHITE, 0);
            xml_parser_set_option($parser, \XML_OPTION_CASE_FOLDING, 0);
            xml_set_element_handler($parser, [$this, 'open'], [$this, 'close']);
            xml_set_character_data_handler($parser, [$this, 'cData']);
            xml_set_start_namespace_decl_handler($parser, [$this, 'nsDecl']);
            $this->xml_parser = $parser;
        }
    }

    public function open($p, $t, $a)
    {
        $t_exact = $t;
        // echo "<br />\n".'opening '.$t . ' ' . print_r($a, 1); flush();
        // echo "<br />\n".'opening '.$t; flush();
        $t = strpos($t, ':') ? $t : strtolower($t);
        /* base check */
        $base = '';
        if (('base' == $t) && isset($a['href'])) {
            $this->base = $a['href'];
            $base = $a['href'];
        }
        /* URIs */
        foreach (['href', 'src', 'id'] as $uri_a) {
            if (isset($a[$uri_a])) {
                $a[$uri_a.' uri'] = ('id' == $uri_a) ? $this->calcURI('#'.$a[$uri_a]) : $this->calcURI($a[$uri_a]);
            }
        }
        /* ns */
        if ($a) {
            foreach ($a as $k => $v) {
                if (str_starts_with($k, 'xmlns')) {
                    $this->nsDecl($p, trim(substr($k, 5), ':'), $v);
                }
            }
        }
        /* node */
        $node = [
            'tag' => $t,
            'tag_exact' => $t_exact,
            'a' => $a,
            'level' => $this->level,
            'pos' => 0,
            'p_id' => $this->node_count - 1,
            'state' => 'open',
            'empty' => 0,
            'cdata' => '',
        ];
        if ($base) {
            $node['base'] = $base;
        }
        /* parent/sibling */
        if ($this->node_count) {
            $l = $this->level;
            $prev_node = $this->getCurNode();
            if ($prev_node['level'] == $l) {
                $node['p_id'] = $prev_node['p_id'];
                $node['pos'] = $prev_node['pos'] + 1;
            } elseif ($prev_node['level'] > $l) {
                while ($prev_node['level'] > $l) {
                    if (!isset($this->nodes[$prev_node['p_id']])) {
                        // $this->addError('nesting mismatch: tag is ' . $t . ', level is ' . $l . ', prev_level is ' . $prev_node['level'] . ', prev_node p_id is ' . $prev_node['p_id']);
                        break;
                    }
                    $prev_node = $this->nodes[$prev_node['p_id']];
                }
                $node['p_id'] = $prev_node['p_id'];
                $node['pos'] = $prev_node['pos'] + 1;
            }
        }
        $this->pushNode($node);
        ++$this->level;
        /* cdata */
        $this->cur_cdata = '';
    }

    public function close($p, $t, $empty = 0)
    {
        // echo "<br />\n".'closing '.$t; flush();
        $node = $this->getCurNode($t);
        $node['state'] = 'closed';
        $node['empty'] = $empty;
        $this->updateNode($node);
        --$this->level;
    }

    public function cData($p, $d)
    {
        // echo trim($d) ? "<br />\n".'cdata: ' . $d : ''; flush();
        $node = $this->getCurNode();
        if ('open' == $node['state']) {
            $node['cdata'] .= $d;
            $this->updateNode($node);
        } else {/* cdata is sibling of node */
            if ($this->allowCDataNodes) {
                $this->open($p, 'cdata', ['value' => $d]);
                $this->close($p, 'cdata');
            }
        }
    }

    public function nsDecl($p, $prf, $uri)
    {
        if (is_array($uri)) {
            return 1;
        }
        $this->ns[$prf] = $uri;
        $this->nsp[$uri] = isset($this->nsp[$uri]) ? $this->nsp[$uri] : $prf;
    }
}
