<?php
/**
 * ARC2 base class
 *
 * @author Benjamin Nowack
 * @license W3C Software License and GPL
 * @homepage <https://github.com/semsol/arc2>
 * @package ARC2
 */

class ARC2_Class {
    protected $db_object;

  function __construct($a, &$caller) {
    $this->a = is_array($a) ? $a : array();
    $this->caller = $caller;
    $this->__init();
  }

  function __init() {/* base, time_limit */
    if (!$_POST && isset($GLOBALS['HTTP_RAW_POST_DATA'])) parse_str($GLOBALS['HTTP_RAW_POST_DATA'], $_POST); /* php5 bug */
    $this->inc_path = ARC2::getIncPath();
    $this->ns_count = 0;
    $rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
    $this->nsp = array($rdf => 'rdf');
    $this->used_ns = array($rdf);
    $this->ns = array_merge(array('rdf' => $rdf), $this->v('ns', array(), $this->a));

    $this->base = $this->v('base', ARC2::getRequestURI(), $this->a);
    $this->errors = array();
    $this->warnings = array();
    $this->adjust_utf8 = $this->v('adjust_utf8', 0, $this->a);
    $this->max_errors = $this->v('max_errors', 25, $this->a);
    $this->has_pcre_unicode = @preg_match('/\pL/u', 'test');/* \pL = block/point which is a Letter */
  }

  /*  */

  function v($name, $default = false, $o = false) {/* value if set */
    if ($o === false) $o = $this;
    if (is_array($o)) {
      return isset($o[$name]) ? $o[$name] : $default;
    }
    return isset($o->$name) ? $o->$name : $default;
  }

  function v1($name, $default = false, $o = false) {/* value if 1 (= not empty) */
    if ($o === false) $o = $this;
    if (is_array($o)) {
      return (isset($o[$name]) && $o[$name]) ? $o[$name] : $default;
    }
    return (isset($o->$name) && $o->$name) ? $o->$name : $default;
  }

  function m($name, $a = false, $default = false, $o = false) {/* call method */
    if ($o === false) $o = $this;
    return method_exists($o, $name) ? $o->$name($a) : $default;
  }

  /*  */

  function camelCase($v, $lc_first = 0, $keep_boundaries = 0) {
    $r = ucfirst($v);
    while (preg_match('/^(.*)[^a-z0-9](.*)$/si', $r, $m)) {
      /* don't fuse 2 upper-case chars */
      if ($keep_boundaries && $m[1]) {
        $boundary = substr($m[1], -1);
        if (strtoupper($boundary) == $boundary) $m[1] .= 'CAMELCASEBOUNDARY';
      }
      $r = $m[1] . ucfirst($m[2]);
    }
    $r = str_replace('CAMELCASEBOUNDARY', '_', $r);
    if ((strlen($r) > 1) && $lc_first && !preg_match('/[A-Z]/', $r[1])) $r = strtolower($r[0]) . substr($r, 1);
    return $r;
  }

  function deCamelCase($v, $uc_first = 0) {
    $r = str_replace('_', ' ', $v);
    $r = preg_replace_callback('/([a-z0-9])([A-Z])/', function($matches) {
      return $matches[1] . ' ' . strtolower($matches[2]);
    }, $r);
    return $uc_first ? ucfirst($r) : $r;
  }

  /**
   * Tries to extract a somewhat human-readable label from a URI.
   */

  function extractTermLabel($uri, $loops = 0) {
    list($ns, $r) = $this->splitURI($uri);
    /* encode apostrophe + s */
    $r = str_replace('%27s', '_apostrophes_', $r);
    /* normalize */
    $r = $this->deCamelCase($this->camelCase($r, 1, 1));
    /* decode apostrophe + s */
    $r = str_replace(' apostrophes ', "'s ", $r);
    /* typical RDF non-info URI */
    if (($loops < 1) && preg_match('/^(self|it|this|me|id)$/i', $r)) {
      return $this->extractTermLabel(preg_replace('/\#.+$/', '', $uri), $loops + 1);
    }
    /* trailing hash or slash */
    if ($uri && !$r && ($loops < 2)) {
      return $this->extractTermLabel(preg_replace('/[\#\/]$/', '', $uri), $loops + 1);
    }
    /* a de-camel-cased URL (will look like "www example com") */
    if (preg_match('/^www (.+ [a-z]{2,4})$/', $r, $m)) {
      return $this->getPrettyURL($uri);
    }
    return $r;
  }

  /**
   * Generates a less ugly in-your-face URL.
   */

  function getPrettyURL($r) {
    $r = rtrim($r, '/');
    $r = preg_replace('/^https?\:\/\/(www\.)?/', '', $r);
    return $r;
  }

  /*  */

  function addError($v) {
    if (!in_array($v, $this->errors)) {
      $this->errors[] = $v;
    }
    if ($this->caller && method_exists($this->caller, 'addError')) {
      $glue = strpos($v, ' in ') ? ' via ' : ' in ';
      $this->caller->addError($v . $glue . get_class($this));
    }
    if (count($this->errors) > $this->max_errors) {
      die('Too many errors (limit: ' . $this->max_errors . '): ' . print_r($this->errors, 1));
    }
    return false;
  }

  function getErrors() {
    return $this->errors;
  }

  function getWarnings() {
    return $this->warnings;
  }

  function resetErrors() {
    $this->errors = array();
    if ($this->caller && method_exists($this->caller, 'resetErrors')) {
      $this->caller->resetErrors();
    }
  }

  /*  */

  function splitURI($v) {
    return ARC2::splitURI($v);
  }

  /*  */

  function getPName($v, $connector = ':') {
    /* is already a pname */
    $ns = $this->getPNameNamespace($v, $connector);
    if ($ns) {
      if (!in_array($ns, $this->used_ns)) $this->used_ns[] = $ns;
      return $v;
    }
    /* new pname */
    $parts = $this->splitURI($v);
    if ($parts) {
      /* known prefix */
      foreach ($this->ns as $prefix => $ns) {
        if ($parts[0] == $ns) {
          if (!in_array($ns, $this->used_ns)) $this->used_ns[] = $ns;
          return $prefix . $connector . $parts[1];
        }
      }
      /* new prefix */
      $prefix = $this->getPrefix($parts[0]);
      return $prefix . $connector . $parts[1];
    }
    return $v;
  }

  function getPNameNamespace($v, $connector = ':') {
    $re = '/^([a-z0-9\_\-]+)\:([a-z0-9\_\-\.\%]+)$/i';
    if ($connector != ':') {
      $connectors = array('\:', '\-', '\_', '\.');
      $chars = join('', array_diff($connectors, array($connector)));
      $re = '/^([a-z0-9' . $chars . ']+)\\' . $connector . '([a-z0-9\_\-\.\%]+)$/i';
    }
    if (!preg_match($re, $v, $m)) return 0;
    if (!isset($this->ns[$m[1]])) return 0;
    return $this->ns[$m[1]];
  }

  function setPrefix($prefix, $ns) {
	 $this->ns[$prefix] = $ns;
	 $this->nsp[$ns] = $prefix;
	 return $this;
  }

  function getPrefix($ns) {
    if (!isset($this->nsp[$ns])) {
      $this->ns['ns' . $this->ns_count] = $ns;
      $this->nsp[$ns] = 'ns' . $this->ns_count;
      $this->ns_count++;
    }
    if (!in_array($ns, $this->used_ns)) $this->used_ns[] = $ns;
    return $this->nsp[$ns];
  }

  function expandPName($v, $connector = ':') {
    $re = '/^([a-z0-9\_\-]+)\:([a-z0-9\_\-\.\%]+)$/i';
    if ($connector != ':') {
      $connectors = array(':', '-', '_', '.');
      $chars = '\\' . join('\\', array_diff($connectors, array($connector)));
      $re = '/^([a-z0-9' . $chars . ']+)\\' . $connector . '([a-z0-9\_\-\.\%]+)$/Ui';
    }
    if (preg_match($re, $v, $m) && isset($this->ns[$m[1]])) {
      return $this->ns[$m[1]] . $m[2];
    }
    return $v;
  }

  function expandPNames($index) {
    $r = array();
    foreach ($index as $s => $ps) {
      $s = $this->expandPName($s);
      $r[$s] = array();
      foreach ($ps as $p => $os) {
        $p = $this->expandPName($p);
        if (!is_array($os)) $os = array($os);
        foreach ($os as $i => $o) {
          if (!is_array($o)) {
            $o_val = $this->expandPName($o);
            $o_type = preg_match('/^[a-z]+\:[^\s\<\>]+$/si', $o_val) ? 'uri' : 'literal';
            $o = array('value' => $o_val, 'type' => $o_type);
          }
          $os[$i] = $o;
        }
        $r[$s][$p] = $os;
      }
    }
    return $r;
  }

  /*  */

  function calcURI($path, $base = "") {
    /* quick check */
    if (preg_match("/^[a-z0-9\_]+\:/i", $path)) {/* abs path or bnode */
      return $path;
    }
    if (preg_match('/^\$\{.*\}/', $path)) {/* placeholder, assume abs URI */
      return $path;
    }
    if (preg_match("/^\/\//", $path)) {/* net path, assume http */
      return 'http:' . $path;
    }
    /* other URIs */
    $base = $base ? $base : $this->base;
    $base = preg_replace('/\#.*$/', '', $base);
    if ($path === true) {/* empty (but valid) URIref via turtle parser: <> */
      return $base;
    }
    $path = preg_replace("/^\.\//", '', $path);
    $root = preg_match('/(^[a-z0-9]+\:[\/]{1,3}[^\/]+)[\/|$]/i', $base, $m) ? $m[1] : $base; /* w/o trailing slash */
    $base .= ($base == $root) ? '/' : '';
    if (preg_match('/^\//', $path)) {/* leading slash */
      return $root . $path;
    }
    if (!$path) {
      return $base;
    }
    if (preg_match('/^([\#\?])/', $path, $m)) {
      return preg_replace('/\\' .$m[1]. '.*$/', '', $base) . $path;
    }
    if (preg_match('/^(\&)(.*)$/', $path, $m)) {/* not perfect yet */
      return preg_match('/\?/', $base) ? $base . $m[1] . $m[2] : $base . '?' . $m[2];
    }
    if (preg_match("/^[a-z0-9]+\:/i", $path)) {/* abs path */
      return $path;
    }
    /* rel path: remove stuff after last slash */
    $base = substr($base, 0, strrpos($base, '/')+1);
    /* resolve ../ */
    while (preg_match('/^(\.\.\/)(.*)$/', $path, $m)) {
      $path = $m[2];
      $base = ($base == $root.'/') ? $base : preg_replace('/^(.*\/)[^\/]+\/$/', '\\1', $base);
    }
    return $base . $path;
  }

  /*  */

  function calcBase($path) {
    $r = $path;
    $r = preg_replace('/\#.*$/', '', $r);/* remove hash */
    $r = preg_replace('/^\/\//', 'http://', $r);/* net path (//), assume http */
    if (preg_match('/^[a-z0-9]+\:/', $r)) {/* scheme, abs path */
      while (preg_match('/^(.+\/)(\.\.\/.*)$/U', $r, $m)) {
        $r = $this->calcURI($m[1], $m[2]);
      }
      return $r;
    }
    return 'file://' . realpath($r);/* real path */
  }

  /*  */

  function getResource($uri, $store_or_props = '') {
    $res = ARC2::getResource($this->a);
    $res->setURI($uri);
    if (is_array($store_or_props)) {
      $res->setProps($store_or_props);
    }
    else {
      $res->setStore($store_or_props);
    }
    return $res;
  }

  function toIndex($v) {
    if (is_array($v)) {
      if (isset($v[0]) && isset($v[0]['s'])) return ARC2::getSimpleIndex($v, 0);
      return $v;
    }
    $parser = ARC2::getRDFParser($this->a);
    if ($v && !preg_match('/\s/', $v)) {/* assume graph URI */
      $parser->parse($v);
    }
    else {
      $parser->parse('', $v);
    }
    return $parser->getSimpleIndex(0);
  }

  function toTriples($v) {
    if (is_array($v)) {
      if (isset($v[0]) && isset($v[0]['s'])) return $v;
      return ARC2::getTriplesFromIndex($v);
    }
    $parser = ARC2::getRDFParser($this->a);
    if ($v && !preg_match('/\s/', $v)) {/* assume graph URI */
      $parser->parse($v);
    }
    else {
      $parser->parse('', $v);
    }
    return $parser->getTriples();
  }

  /*  */

  function toNTriples($v, $ns = '', $raw = 0) {
    ARC2::inc('NTriplesSerializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $ser = new ARC2_NTriplesSerializer(array_merge($this->a, array('ns' => $ns)), $this);
    return (isset($v[0]) && isset($v[0]['s'])) ? $ser->getSerializedTriples($v, $raw) : $ser->getSerializedIndex($v, $raw);
  }

  function toTurtle($v, $ns = '', $raw = 0) {
    ARC2::inc('TurtleSerializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $ser = new ARC2_TurtleSerializer(array_merge($this->a, array('ns' => $ns)), $this);
    return (isset($v[0]) && isset($v[0]['s'])) ? $ser->getSerializedTriples($v, $raw) : $ser->getSerializedIndex($v, $raw);
  }

  function toRDFXML($v, $ns = '', $raw = 0) {
    ARC2::inc('RDFXMLSerializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $ser = new ARC2_RDFXMLSerializer(array_merge($this->a, array('ns' => $ns)), $this);
    return (isset($v[0]) && isset($v[0]['s'])) ? $ser->getSerializedTriples($v, $raw) : $ser->getSerializedIndex($v, $raw);
  }

  function toRDFJSON($v, $ns = '') {
    ARC2::inc('RDFJSONSerializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $ser = new ARC2_RDFJSONSerializer(array_merge($this->a, array('ns' => $ns)), $this);
    return (isset($v[0]) && isset($v[0]['s'])) ? $ser->getSerializedTriples($v) : $ser->getSerializedIndex($v);
  }

  function toRSS10($v, $ns = '') {
    ARC2::inc('RSS10Serializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $ser = new ARC2_RSS10Serializer(array_merge($this->a, array('ns' => $ns)), $this);
    return (isset($v[0]) && isset($v[0]['s'])) ? $ser->getSerializedTriples($v) : $ser->getSerializedIndex($v);
  }

  function toLegacyXML($v, $ns = '') {
    ARC2::inc('LegacyXMLSerializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $ser = new ARC2_LegacyXMLSerializer(array_merge($this->a, array('ns' => $ns)), $this);
    return $ser->getSerializedArray($v);
  }

  function toLegacyJSON($v, $ns = '') {
    ARC2::inc('LegacyJSONSerializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $ser = new ARC2_LegacyJSONSerializer(array_merge($this->a, array('ns' => $ns)), $this);
    return $ser->getSerializedArray($v);
  }

  function toLegacyHTML($v, $ns = '') {
    ARC2::inc('LegacyHTMLSerializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $ser = new ARC2_LegacyHTMLSerializer(array_merge($this->a, array('ns' => $ns)), $this);
    return $ser->getSerializedArray($v);
  }

  function toHTML($v, $ns = '', $label_store = '') {
    ARC2::inc('MicroRDFSerializer');
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $conf = array_merge($this->a, array('ns' => $ns));
    if ($label_store) $conf['label_store'] = $label_store;
    $ser = new ARC2_MicroRDFSerializer($conf, $this);
    return (isset($v[0]) && isset($v[0]['s'])) ? $ser->getSerializedTriples($v) : $ser->getSerializedIndex($v);
  }

  /*  */

  function getFilledTemplate($t, $vals, $g = '') {
    $parser = ARC2::getTurtleParser();
    $parser->parse($g, $this->getTurtleHead() . $t);
    return $parser->getSimpleIndex(0, $vals);
  }

  function getTurtleHead() {
    $r = '';
    $ns = $this->v('ns', array(), $this->a);
    foreach ($ns as $k => $v) {
      $r .= "@prefix " . $k . ": <" .$v. "> .\n";
    }
    return $r;
  }

  function completeQuery($q, $ns = '') {
    if (!$ns) $ns = isset($this->a['ns']) ? $this->a['ns'] : array();
    $added_prefixes = array();
    $prologue = '';
    foreach ($ns as $k => $v) {
      $k = rtrim($k, ':');
      if (in_array($k, $added_prefixes)) continue;
      if (preg_match('/(^|\s)' . $k . ':/s', $q) && !preg_match('/PREFIX\s+' . $k . '\:/is', $q)) {
        $prologue .= "\n" . 'PREFIX ' . $k . ': <' . $v . '>';
      }
      $added_prefixes[] = $k;
    }
    return $prologue . "\n" . $q;
  }

  /*  */

  function toUTF8($str) {
    return $this->adjust_utf8 ? ARC2::toUTF8($str) : $str;
  }

  function toDataURI($str) {
    return 'data:text/plain;charset=utf-8,' . rawurlencode($str);
  }

  function fromDataURI($str) {
    return str_replace('data:text/plain;charset=utf-8,', '', rawurldecode($str));
  }

  /* prevent SQL injections via SPARQL REGEX */

  function checkRegex($str) {
    return addslashes($str); // @@todo extend
  }

  /* Microdata methods */

  function getMicrodataAttrs($id, $type = '') {
    $type = $type ? $this->expandPName($type) : $this->expandPName('owl:Thing');
    return 'itemscope="" itemtype="' . htmlspecialchars($type) . '" itemid="' . htmlspecialchars($id) . '"';
  }

  function mdAttrs($id, $type = '') {
    return $this->getMicrodataAttrs($id, $type);
  }

    /* central DB query hook */

    public function getDBObjectFromARC2Class($con = null)
    {
        if (null == $this->db_object) {
            if (false === class_exists('\\ARC2\\Store\\Adapter\\AdapterFactory')) {
                require __DIR__.'/src/ARC2/Store/Adapter/AdapterFactory.php';
            }
            if (false == isset($this->a['db_adapter'])) {
                $this->a['db_adapter'] = 'mysqli';
            }
            $factory = new \ARC2\Store\Adapter\AdapterFactory();
            $this->db_object = $factory->getInstanceFor($this->a['db_adapter'], $this->a);
            if ($con) {
                $this->db_object->connect($con);
            } else {
                $this->db_object->connect();
            }
        }
        return $this->db_object;
    }

    /**
     * Dont use this function to directly query the database. It currently works only with mysqli DB adapter.
     *
     * @param string $sql SQL query
     * @param mysqli $con Connection
     * @param int    $log_errors 1 if you want to log errors. Default is 0
     *
     * @return mysqli Result
     *
     * @deprecated since 2.4.0
     */
    public function queryDB($sql, $con, $log_errors = 0)
    {
        $t1 = ARC2::mtime();

        // create connection using an adapter, if not available yet
        $this->getDBObjectFromARC2Class($con);

        $r = $this->db_object->mysqliQuery($sql);

        // TODO check if this is ever called. it seems not and therefore could be removed.
        if (0) {
            $t2 = ARC2::mtime() - $t1;
            $call_obj = $this;
            $call_path = '';
            while ($call_obj) {
                $call_path = get_class($call_obj) . ' / ' . $call_path;
                $call_obj = isset($call_obj->caller) ? $call_obj->caller : false;
            }
            echo "\n" . $call_path . " needed " . $t2 . ' secs for ' . str_replace("\n" , ' ', $sql);;
        }

        if ($log_errors && !empty($this->db_object->getErrorMessage())) {
            $this->addError($this->db_object->getErrorMessage());
        }
        return $r;
    }

  /**
   * Shortcut method to create an RDF/XML backup dump from an RDF Store object.
   */
  function backupStoreData($store, $target_path, $offset = 0) {
    $limit = 10;
    $q = '
      SELECT DISTINCT ?s WHERE {
        ?s ?p ?o .
      }
      ORDER BY ?s
      LIMIT ' . $limit . '
      ' . ($offset ? 'OFFSET ' . $offset : '') . '
    ';
    $rows = $store->query($q, 'rows');
    $tc = count($rows);
    $full_tc = $tc + $offset;
    $mode = $offset ? 'ab' : 'wb';
    $fp = fopen($target_path, $mode);
    foreach ($rows as $row) {
      $index = $store->query('DESCRIBE <' . $row['s'] . '>', 'raw');
      if ($index) {
        $doc = $this->toRDFXML($index);
        fwrite($fp, $doc . "\n\n");
      }
    }
    fclose($fp);
    if ($tc == 10) {
      set_time_limit(0);
      $this->backupStoreData($store, $target_path, $offset + $limit);
    }
    return $full_tc;
  }

}
