Mercurial > hg > marc_php
view marccore.php @ 0:3ac7bd7495fd draft
Initial commit
author | Ivo Smits <Ivo@UCIS.nl> |
---|---|
date | Sat, 08 Nov 2014 22:22:42 +0100 |
parents | |
children | 5c8c4fa95803 |
line wrap: on
line source
<?php if (!function_exists('hex2bin')) { function hex2bin($h) { return pack("H*", $h); } } class MARCUpdate implements ArrayAccess { private $_version, $_key, $_serial, $_label, $_extensions, $_value = FALSE, $_valueoffset = -1, $_message, $_tags = array(); public function __get($name) { switch (strtolower($name)) { case 'version': return $this->_version; case 'key': return $this->_key; case 'serial': return $this->_serial; case 'label': return $this->_label; case 'expiration': return (isset($this->_extensions[4]) && strlen($this->_extensions[4]) >= 4) ? marc_decode_int32be($this->_extensions[4]) : NULL; case 'transfer': return isset($this->_extensions[1]) ? $this->_extensions[1] : NULL; case 'value': if ($this->_value === FALSE) $this->_value = self::DecodeValue($this->_message, $this->_valueoffset); return $this->_value; case 'updatemessage': return $this->_message; case 'tags': return $this->_tags; break; default: throw new Exception('Property '.$name.' does not exist'); } } public function __set($name, $value) { switch (strtolower($name)) { case 'version': case 'key': case 'serial': case 'label': case 'value': case 'expiration': case 'transfer': case 'updatemessage': case 'tags': throw new Exception('Property '.$name.' is read only'); default: throw new Exception('Property '.$name.' does not exist or is read-only'); } } public function __isset($name) { switch (strtolower($name)) { case 'version': case 'key': case 'serial': case 'label': case 'value': case 'updatemessage': case 'tags': return TRUE; case 'expiration': return isset($this->_extensions[4]); case 'transfer': return isset($this->_extensions[1]); default: return FALSE; } } public function __unset($name) { switch (strtolower($name)) { case 'version': case 'key': case 'serial': case 'label': case 'value': case 'expiration': case 'transfer': case 'updatemessage': case 'tags': throw new Exception('Property '.$name.' is read only'); default: throw new Exception('Property '.$name.' does not exist'); } } public function offsetSet($offset, $value) { $this->__set($offset, $value); } public function offsetExists($offset) { return $this->__isset($offset); } public function offsetUnset($offset) { $this->__unset($offset); } public function offsetGet($offset) { return $this->__get($offset); } private function __construct() { } public static function Decode($data) { $upd = new MARCUpdate(); $upd->_message = $data; if (strlen($data) < 1 + NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES) return FALSE; $upd->_version = ord($data[0]); if ($upd->_version != 2) return FALSE; $upd->_key = substr($data, 1, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES); $i = NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES + 64 + 1; $l = strlen($data); if (!$l || $l < $i+4+1+1) return FALSE; $upd->_serial = marc_decode_int32be($data, $i); $i += 4; $labellen = ord($data[$i++]); if ($l < $i+$labellen+4) return FALSE; $upd->_label = substr($data, $i, $labellen); $i += $labellen; $upd->_extensions = array(); for ($numext = ord($data[$i++]); $numext > 0; $numext--) { if ($l < $i+1+2) return FALSE; $extid = ord($data[$i++]); $extlen = marc_decode_int16be($data, $i); $i += 2; if ($l < $i + $extlen) return FALSE; $upd->_extensions[$extid] = substr($data, $i, $extlen); $i += $extlen; } $upd->_valueoffset = $i; return $upd; } public function Verify() { $data = $this->_message; if (strlen($data) < 1 + NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES) return FALSE; $version = ord($data[0]); if ($version != 2) return FALSE; $key = substr($data, 1, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES); $data = substr($data, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES + 1); $data = nacl_crypto_sign_ed25519_open($data, $key); return $data !== NULL && $data !== FALSE; } public function Create($upd, $seckey, $current = NULL) { if (strlen($seckey) == 32) nacl_crypto_sign_ed25519_keypair($seckey, $seckey); if (strlen($seckey) < 64) throw new Exception('Signing key is not valid'); if (!isset($upd['key'])) $upd['key'] = substr($seckey, 32, 32); if ($upd['key'] != substr($seckey, 32, 32)) throw new Exception('Resource key is not valid'); if (!isset($upd['label'])) throw new Exception('Resource label not set'); if (strlen($upd['label']) > 255) throw new Exception('Resource label too long'); if (!isset($upd['serial'])) $upd['serial'] = time(); if ($current) { if ($upd['serial'] <= $current['serial']) $upd['serial'] = $current['serial'] + 1; if (!self::CanImport($upd, $current)) throw new Exception('Can not update resource'); } if (isset($upd['transfer']) && (strlen($upd['transfer']) != 0 && strlen($upd['transfer']) != NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES)) throw new Exception('Transfer recipient key is not valid'); $data = marc_encode_int32be($upd['serial']); $data .= chr(strlen($upd['label'])).$upd['label']; $value = array(); if (isset($upd->_extensions)) foreach ($upd->_extensions as $identifier => $item) $value[$identifier] = $item; if (isset($upd['transfer'])) $value[1] = $upd['transfer']; if (isset($upd['expiration'])) $value[4] = marc_encode_int32be($upd['expiration']); if (count($value) > 0xff) throw new Exception('Too many extensions used'); $data .= chr(count($value)); foreach ($value as $identifier => $item) { $item = (string)$item; if (strlen($item) > 0xffff) throw new Exception('Extension data too big'); $data .= chr($identifier).marc_encode_int16be(strlen($item)).$item; } if (isset($upd['value'])) $data .= self::EncodeValue($upd['value']); $data = nacl_crypto_sign_ed25519($data, $seckey); if (!strlen($data)) throw new Exception('Failed to sign data'); if (!strlen(nacl_crypto_sign_ed25519_open($data, $upd['key']))) throw new Exception('Key pair is not valid'); $data = chr(2).$upd['key'].$data; return self::Decode($data); } public function ToArray() { $arr = array('version' => $this->version, 'key' => $this->key, 'serial' => $this->serial, 'label' => $this->label, 'value' => $this->value); if (isset($this->expiration)) $arr['expiration'] = $this->expiration; if (isset($this->transfer)) $arr['transfer'] = $this->transfer; return $arr; } private static function EncodeValue($data) { if (is_null($data)) { return chr(0); } else if (is_scalar($data)) { return chr(1).(string)$data; } else if (is_array($data)) { $iscollection = TRUE; foreach ($data as $key => $value) if (!is_int($key) || $key < 0) $iscollection = FALSE; $ret = $iscollection ? chr(2) : chr(3); foreach ($data as $key => $value) { if (!$iscollection) { if (strlen($key) > 0xff) throw new Exception('Dictionary key is too long'); $ret .= chr(strlen($key)).$key; } $bytes = self::EncodeValue($value); $ret .= marc_encode_int32be(strlen($bytes)).$bytes; } return $ret; } else { throw new Exception('Unable to encode type '.gettype($data)); } } private static function DecodeValue($data, $offset = 0, $length = -1) { if ($length == -1) $length = strlen($data) - $offset; if ($length < 0) throw new Exception('Truncated'); if ($length == 0) return NULL; $type = ord($data[$offset]); $offset++; $length--; switch ($type) { case 0: return NULL; case 1: return substr($data, $offset, $length); case 2: $value = array(); while ($length > 0) { if (4 > $length) throw new Exception('Truncated'); $len = marc_decode_int32be($data, $offset); $offset += 4; $length -= 4; if ($len > $length) throw new Exception('Truncated'); $value[] = self::DecodeValue($data, $offset, $len); $offset += $len; $length -= $len; } return $value; case 3: $value = array(); while ($length > 0) { if (1 > $length) throw new Exception('Truncated'); $len = ord($data[$offset]); $offset++; $length--; if ($len + 4 > $length) throw new Exception('Truncated'); $key = substr($data, $offset, $len); $offset += $len; $length -= $len; $len = marc_decode_int32be($data, $offset); $offset += 4; $length -= 4; if ($len > $length) throw new Exception('Truncated'); $value[$key] = self::DecodeValue($data, $offset, $len); $offset += $len; $length -= $len; } return $value; default: throw new Exception('Unsupported type code '.$type); } } public static function CanImport($nw, $cu = NULL) { if (!$nw || !isset($nw['label'])) return FALSE; if ($cu) { if ($nw['label'] != $cu['label']) return FALSE; if ($cu['serial'] >= $nw['serial']) return FALSE; if ($cu['key'] == $nw['key']) return TRUE; if (isset($cu['expiration']) && $cu['expiration'] < time()) return TRUE; if ($cu['serial'] < time() - 365*24*60*60) return TRUE; if (isset($cu['transfer']) && (!strlen($cu['transfer']) || $cu['transfer'] == $nw['key'])) return TRUE; return FALSE; } else { if ($nw['serial'] < time() - 365*24*60*60) return FALSE; if (isset($nw['expiration']) && $nw['expiration'] < time()) return FALSE; } return TRUE; } } abstract class MARCDatabase { private $_importResourceFilterCallbacks = array(), $_importResourceCallbacks = array(); public abstract function GetResource($label); protected abstract function ImportInternal($resource); public abstract function GetResources($labelprefix = NULL, $mints = NULL); public abstract function DeleteResource($label); public function Import($resource, $force = FALSE) { if (is_string($resource)) $resource = MARCUpdate::Decode($resource); if (!$resource) return FALSE; $current = $this->GetResource($resource['label']); if (!$force) foreach ($this->_importResourceFilterCallbacks as $callback) if (!call_user_func($callback, $this, $resource, $current)) return FALSE; if (!$force && !MARCUpdate::CanImport($resource, $current)) return FALSE; if (!$resource->Verify()) return FALSE; if (!$this->ImportInternal($resource)) return FALSE; $this->ResourceImported($resource); return $resource; } public function UpdateResource($resource, $seckey, $force = FALSE) { $res = MARCUpdate::Create($resource, $seckey, $force ? NULL : $this->GetResource($resource['label'])); if (!$res) return FALSE; return $this->Import($res, $force); } protected function CanImportResource(&$resource, $force = FALSE) { if (is_string($resource)) $resource = MARCUpdate::Decode($resource); if (!$resource) return FALSE; $current = $this->GetResource($resource['label']); if (!$force) foreach ($this->_importResourceFilterCallbacks as $callback) if (!call_user_func($callback, $this, $resource, $current)) return FALSE; if (!$force && !MARCUpdate::CanImport($resource, $current)) return FALSE; if (!$resource->Verify()) return FALSE; return TRUE; } protected function ResourceImported($resource) { foreach ($this->_importResourceCallbacks as $callback) call_user_func($callback, $this, $resource); } public function RegisterResourceFilterCallback($callback) { $this->_importResourceFilterCallbacks[] = $callback; } public function RegisterResourceImportCallback($callback) { $this->_importResourceCallbacks[] = $callback; } public function SyncHTTP($server, $options = array()) { $log = isset($options['log']) ? $options['log'] : TRUE; $result = array(); $method = isset($options['method']) ? $options['method'] : 'PUT'; if ($method != 'POST' && $method != 'PUT' && $method != 'GET') throw new Exception('Unsupported method '.$method); $result['exported'] = 0; $post = NULL; if ($method != 'GET' && (!isset($options['noexport']) || !$options['noexport'])) { $post = $method == 'POST' ? array() : ''; $updates = $this->GetResources(NULL, isset($options['exporttimestamp']) ? $options['exporttimestamp'] : NULL); foreach ($updates as $update) { if (!is_string($update)) $update = $update['updatemessage']; if ($method == 'PUT') { $post .= marc_encode_int32be(strlen($update)).$update; } else { $post[] = 'update[]='.urlencode($update); } $result['exported']++; } $updates = NULL; if ($method == 'POST') $post = implode('&', $post); } if ($log) echo "Sending ".strlen($post)." bytes... "; $result['bytessent'] = strlen($post); $ch = curl_init(); $timestamp = isset($options['importtimestamp']) ? intval($options['importtimestamp']) : 0; curl_setopt($ch, CURLOPT_URL, $server.'?version=3&get='.$timestamp); if ($method == 'PUT') curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); else if ($method == 'POST') curl_setopt($ch, CURLOPT_POST, TRUE); if ($post != NULL) curl_setopt($ch, CURLOPT_POSTFIELDS, $post); $post = NULL; curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 60); $response = curl_exec($ch); if ($response === FALSE) throw new Exception('HTTP request failed'); if ($log) echo "received ".strlen($response)." bytes.\n"; $result['bytesreceived'] = strlen($response); if (($httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE)) != 200) throw new Exception('Received unexpected HTTP code '.$httpcode.' - '.(strlen($response)?var_export($response,TRUE):'')); if (($contenttype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE)) != 'application/octet-stream') throw new Exception('Received unexpected content type '.$contenttype.' - '.(strlen($response)?var_export($response,TRUE):'')); curl_close($ch); $i = 0; if ($i + 2 > strlen($response)) throw new Exception('Data has been truncated'); $version = ord($response[$i++]); if ($version != 3) throw new Exception('Unsupported protocol version '.$version); for ($numext = ord($response[$i++]); $numext > 0; $numext--) { if ($i + 3 > strlen($response)) throw new Exception('Truncated'); $extid = ord($response[$i++]); $extlen = marc_decode_int16be($response, $i); $i += 2; if ($i + $extlen > strlen($response)) throw new Exception('Truncated'); switch ($extid) { case 2: if ($extlen >= 4 * 3) { $result['remotereceived'] = marc_decode_int32be($response, $i + 0); $result['remoteimported'] = marc_decode_int32be($response, $i + 4); $result['remoteexported'] = marc_decode_int32be($response, $i + 8); } break; case 3: if ($extlen >= 4) $result['importtimestamp'] = marc_decode_int32be($response, $i); break; } $i += $extlen; } if ($log) echo "Exported ".$result['exported']." updates of which ".(isset($result['remoteimported']) ? $result['remoteimported'] : 'none')." were imported.\n"; $result['updates'] = array(); $result['imported'] = 0; $result['updatesreceived'] = 0; while ($i < strlen($response)) { if ($i + 4 > strlen($response)) throw new Exception('Truncated'); $len = marc_decode_int32be($response, $i); $i += 4; if ($i + $len > strlen($response)) throw new Exception('Truncated'); $result['updatesreceived']++; $value = substr($response, $i, $len); $i += $len; $res = MARCUpdate::Decode($value); $result['updates'][] = $res; if (!$this->Import($value)) continue; $result['imported']++; } if ($log) echo "Imported ".$result['imported']." out of ".$result['updatesreceived']." updates.\n"; return $result; } } class MARCDatabaseFlatFile extends MARCDatabase { private $dbfile = NULL; private $resources = array(); private $changed = FALSE; public function GetResource($label) { if (!isset($this->resources[$label])) return NULL; return $this->resources[$label]['update']; } public function DeleteResource($label) { if (!isset($this->resources[$label])) return FALSE; unset($this->resources[$label]); $this->changed = TRUE; return TRUE; } protected function ImportInternal($update) { $this->resources[$update['label']] = array('timestamp' => time(), 'update' => $update); $this->changed = TRUE; return TRUE; } public function GetResources($labelprefix = NULL, $mints = NULL) { return new MARCDatabaseFlatFileFilteredResourceIterator(new ArrayIterator($this->resources), $labelprefix, $mints); } public function __construct($filename = FALSE) { if ($filename) $this->Open($filename); } private function OpenFile($filename) { $this->Close(); $this->dbfile = fopen($filename, 'c+'); if (!$this->dbfile) throw new Exception('Could not open database file'); if (!flock($this->dbfile, LOCK_EX)) throw new Exception('Could not lock database file'); } public function Open($filename) { $this->OpenFile($filename); $this->resources = array(); $this->changed = FALSE; rewind($this->dbfile); while (true) { $data = fread($this->dbfile, 8); if (strlen($data) == 0) break; if (strlen($data) != 8) throw new Exception('Database truncated'); $ts = marc_decode_int32be($data, 0); $len = marc_decode_int32be($data, 4); $data = fread($this->dbfile, $len); if (strlen($data) != $len) throw new Exception('Database truncated'); $res = MARCUpdate::Decode($data); if (!$res) continue; $this->resources[$res['label']] = array('timestamp' => $ts, 'update' => $res); } } public function Save() { if (!$this->dbfile) throw new Exception('No database file is open'); rewind($this->dbfile); ftruncate($this->dbfile, 0); foreach ($this->resources as $res) { fwrite($this->dbfile, marc_encode_int32be($res['timestamp'])); $u = (string)$res['update']['updatemessage']; fwrite($this->dbfile, marc_encode_int32be(strlen($u))); fwrite($this->dbfile, $u); } $this->changed = FALSE; } public function SaveAs($filename) { $this->OpenFile($filename); $this->Save(); } public function Close() { if ($this->dbfile) { flock($this->dbfile, LOCK_UN); fclose($this->dbfile); $this->dbfile = NULL; } } public function IsChanged() { return $this->changed; } } class MARCDatabaseFlatFileFilteredResourceIterator implements Iterator { private $source, $labelprefixlength, $labelprefix, $mints; public function __construct($source, $labelprefix, $mints) { $this->source = $source; $this->labelprefixlength = is_null($labelprefix) ? 0 : strlen($labelprefix); $this->labelprefix = $labelprefix; $this->mints = intval($mints); } public function current() { $r = $this->source->current(); return $r ? $r['update'] : NULL; } public function key() { return $this->source->key(); } public function valid() { return $this->source->valid(); } public function next() { $this->source->next(); $this->findnext(); } public function rewind() { $this->source->rewind(); $this->findnext(); } private function findnext() { while ($this->source->valid()) { $c = $this->source->current(); if ($c['timestamp'] >= $this->mints && (!$this->labelprefixlength || substr($c['update']['label'], 0, $this->labelprefixlength) == $this->labelprefix)) break; $this->source->next(); } } } class MARCDatabaseSQLite extends MARCDatabase { private $pdo = NULL; protected function DBPrepareStatement($query, $args = NULL) { $stmt = $this->pdo->prepare($query); if (!is_array($args) && !is_null($args)) $args = array($args); $stmt->execute($args); return $stmt; } private function DBUpdate($query, $args = NULL) { $stmt = $this->DBPrepareStatement($query, $args); $cnt = $stmt->rowCount(); $stmt->closeCursor(); return $cnt; } public function __construct($filename) { $this->pdo = new PDO('sqlite:'.$filename); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->DBUpdate('CREATE TABLE IF NOT EXISTS `resources` (`timestamp` INTEGER, `label` BLOB PRIMARY KEY, `update` BLOB)'); } public function GetResource($label) { $stmt = $this->DBPrepareStatement('SELECT `update` FROM `resources` WHERE `label` = ?', $label); $res = $stmt->fetchColumn(); $stmt->closeCursor(); if ($res === FALSE) return NULL; return MARCUpdate::Decode($res); } public function DeleteResource($label) { return $this->DBUpdate('DELETE FROM `resources` WHERE `label` = ?', $label) != 0; } protected function ImportInternal($update) { $args = array('d' => $update['updatemessage'], 't' => time(), 'l' => $update['label']); if ($this->DBUpdate('UPDATE `resources` SET `update` = :d, `timestamp` = :t WHERE `label` = :l', $args) == 0) $this->DBUpdate('INSERT OR IGNORE INTO `resources` (`label`, `update`, `timestamp`) VALUES (:l, :d, :t)', $args); return TRUE; } public function GetResources($labelprefix = NULL, $mints = NULL) { $labelprefix = is_null($labelprefix) ? '' : (string)$labelprefix; return $this->GetResourcesFromQuery('SELECT `update` FROM `resources` WHERE `timestamp` >= ? AND SUBSTR(`label`, 1, ?) = ? ORDER BY `label`', array(intval($mints), strlen($labelprefix), $labelprefix)); } public function GetResourcesFromQuery($query, $args = NULL) { return $this->GetResourcesFromStatement($this->DBPrepareStatement($query, $args)); } public function GetResourcesFromStatement($statement) { return new MARCDatabaseSQLiteResourceIterator($statement); } public function IsChanged() { return FALSE; } public function Close() { } } class MARCDatabaseSQLiteResourceIterator implements Iterator { private $source, $current = FALSE; public function __construct($source) { $this->source = $source; } public function current() { return $this->current === FALSE ? NULL : MARCUpdate::Decode($this->current); } public function key() { return NULL; } public function valid() { return $this->current !== FALSE; } public function next() { $this->current = $this->source->fetchColumn(); if ($this->current === FALSE) $this->source->closeCursor(); } public function rewind() { $this->next(); } } class MARCDatabaseDBA extends MARCDatabase { private $db = NULL; public function __construct($path, $mode = 'cd', $handler = 'qdbm') { $this->db = dba_open($path, $mode, $handler); if ($this->db === FALSE) throw new Exception('Could not open database'); } public function GetResource($label) { $r = dba_fetch($label, $this->db); if ($r === FALSE) return NULL; return MARCUpdate::Decode($r); } public function DeleteResource($label) { return dba_delete($label, $this->db); } protected function ImportInternal($update) { return dba_replace($update['label'], $update['updatemessage'], $this->db); } public function GetResources($labelprefix = NULL, $mints = NULL) { return new MARCDatabaseDBAFilteredResourceIterator($this->db, $labelprefix, $mints); } public function Save() { dba_sync($this->db); } public function Close() { dba_close($this->db); } public function IsChanged() { return FALSE; } } class MARCDatabaseDBAFilteredResourceIterator implements Iterator { private $db, $currentkey = FALSE, $labelprefixlength, $labelprefix, $mints; public function __construct($db, $labelprefix, $mints) { $this->db = $db; $this->labelprefixlength = is_null($labelprefix) ? 0 : strlen($labelprefix); $this->labelprefix = $labelprefix; $this->mints = intval($mints); } public function current() { return MARCUpdate::Decode(dba_fetch($this->currentkey, $this->db)); } public function key() { return $this->currentkey; } public function valid() { return strlen($this->currentkey); } public function next() { $this->currentkey = dba_nextkey($this->db); $this->findnext(); } public function rewind() { $this->currentkey = dba_firstkey($this->db); $this->findnext(); } private function findnext() { while ($this->currentkey !== FALSE && ($this->labelprefixlength && substr($this->currentkey, 0, $this->labelprefixlength) != $this->labelprefix)) $this->currentkey = dba_nextkey($this->db); } } function marc_decode_int32be($data, $i = 0) { return (ord($data[$i]) << 24) | (ord($data[$i+1]) << 16) | (ord($data[$i+2]) << 8) | ord($data[$i+3]); } function marc_decode_int16be($data, $i = 0) { return (ord($data[$i+0]) << 8) | ord($data[$i+1]); } function marc_encode_int32be($v) { return pack("N", $v); } function marc_encode_int16be($v) { return pack("n", $v); }