comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:3ac7bd7495fd
1 <?php
2 if (!function_exists('hex2bin')) { function hex2bin($h) { return pack("H*", $h); } }
3
4 class MARCUpdate implements ArrayAccess {
5 private $_version, $_key, $_serial, $_label, $_extensions, $_value = FALSE, $_valueoffset = -1, $_message, $_tags = array();
6 public function __get($name) {
7 switch (strtolower($name)) {
8 case 'version': return $this->_version;
9 case 'key': return $this->_key;
10 case 'serial': return $this->_serial;
11 case 'label': return $this->_label;
12 case 'expiration': return (isset($this->_extensions[4]) && strlen($this->_extensions[4]) >= 4) ? marc_decode_int32be($this->_extensions[4]) : NULL;
13 case 'transfer': return isset($this->_extensions[1]) ? $this->_extensions[1] : NULL;
14 case 'value':
15 if ($this->_value === FALSE) $this->_value = self::DecodeValue($this->_message, $this->_valueoffset);
16 return $this->_value;
17 case 'updatemessage': return $this->_message;
18 case 'tags': return $this->_tags; break;
19 default: throw new Exception('Property '.$name.' does not exist');
20 }
21 }
22 public function __set($name, $value) {
23 switch (strtolower($name)) {
24 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');
25 default: throw new Exception('Property '.$name.' does not exist or is read-only');
26 }
27 }
28 public function __isset($name) {
29 switch (strtolower($name)) {
30 case 'version': case 'key': case 'serial': case 'label': case 'value': case 'updatemessage': case 'tags': return TRUE;
31 case 'expiration': return isset($this->_extensions[4]);
32 case 'transfer': return isset($this->_extensions[1]);
33 default: return FALSE;
34 }
35 }
36 public function __unset($name) {
37 switch (strtolower($name)) {
38 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');
39 default: throw new Exception('Property '.$name.' does not exist');
40 }
41 }
42 public function offsetSet($offset, $value) { $this->__set($offset, $value); }
43 public function offsetExists($offset) { return $this->__isset($offset); }
44 public function offsetUnset($offset) { $this->__unset($offset); }
45 public function offsetGet($offset) { return $this->__get($offset); }
46
47 private function __construct() { }
48 public static function Decode($data) {
49 $upd = new MARCUpdate();
50 $upd->_message = $data;
51 if (strlen($data) < 1 + NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES) return FALSE;
52 $upd->_version = ord($data[0]);
53 if ($upd->_version != 2) return FALSE;
54 $upd->_key = substr($data, 1, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES);
55 $i = NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES + 64 + 1;
56 $l = strlen($data);
57 if (!$l || $l < $i+4+1+1) return FALSE;
58 $upd->_serial = marc_decode_int32be($data, $i); $i += 4;
59 $labellen = ord($data[$i++]);
60 if ($l < $i+$labellen+4) return FALSE;
61 $upd->_label = substr($data, $i, $labellen); $i += $labellen;
62 $upd->_extensions = array();
63 for ($numext = ord($data[$i++]); $numext > 0; $numext--) {
64 if ($l < $i+1+2) return FALSE;
65 $extid = ord($data[$i++]);
66 $extlen = marc_decode_int16be($data, $i); $i += 2;
67 if ($l < $i + $extlen) return FALSE;
68 $upd->_extensions[$extid] = substr($data, $i, $extlen);
69 $i += $extlen;
70 }
71 $upd->_valueoffset = $i;
72 return $upd;
73 }
74 public function Verify() {
75 $data = $this->_message;
76 if (strlen($data) < 1 + NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES) return FALSE;
77 $version = ord($data[0]);
78 if ($version != 2) return FALSE;
79 $key = substr($data, 1, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES);
80 $data = substr($data, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES + 1);
81 $data = nacl_crypto_sign_ed25519_open($data, $key);
82 return $data !== NULL && $data !== FALSE;
83 }
84 public function Create($upd, $seckey, $current = NULL) {
85 if (strlen($seckey) == 32) nacl_crypto_sign_ed25519_keypair($seckey, $seckey);
86 if (strlen($seckey) < 64) throw new Exception('Signing key is not valid');
87 if (!isset($upd['key'])) $upd['key'] = substr($seckey, 32, 32);
88 if ($upd['key'] != substr($seckey, 32, 32)) throw new Exception('Resource key is not valid');
89 if (!isset($upd['label'])) throw new Exception('Resource label not set');
90 if (strlen($upd['label']) > 255) throw new Exception('Resource label too long');
91 if (!isset($upd['serial'])) $upd['serial'] = time();
92 if ($current) {
93 if ($upd['serial'] <= $current['serial']) $upd['serial'] = $current['serial'] + 1;
94 if (!self::CanImport($upd, $current)) throw new Exception('Can not update resource');
95 }
96 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');
97 $data = marc_encode_int32be($upd['serial']);
98 $data .= chr(strlen($upd['label'])).$upd['label'];
99 $value = array();
100 if (isset($upd->_extensions)) foreach ($upd->_extensions as $identifier => $item) $value[$identifier] = $item;
101 if (isset($upd['transfer'])) $value[1] = $upd['transfer'];
102 if (isset($upd['expiration'])) $value[4] = marc_encode_int32be($upd['expiration']);
103 if (count($value) > 0xff) throw new Exception('Too many extensions used');
104 $data .= chr(count($value));
105 foreach ($value as $identifier => $item) {
106 $item = (string)$item;
107 if (strlen($item) > 0xffff) throw new Exception('Extension data too big');
108 $data .= chr($identifier).marc_encode_int16be(strlen($item)).$item;
109 }
110 if (isset($upd['value'])) $data .= self::EncodeValue($upd['value']);
111 $data = nacl_crypto_sign_ed25519($data, $seckey);
112 if (!strlen($data)) throw new Exception('Failed to sign data');
113 if (!strlen(nacl_crypto_sign_ed25519_open($data, $upd['key']))) throw new Exception('Key pair is not valid');
114 $data = chr(2).$upd['key'].$data;
115 return self::Decode($data);
116 }
117 public function ToArray() {
118 $arr = array('version' => $this->version, 'key' => $this->key, 'serial' => $this->serial, 'label' => $this->label, 'value' => $this->value);
119 if (isset($this->expiration)) $arr['expiration'] = $this->expiration;
120 if (isset($this->transfer)) $arr['transfer'] = $this->transfer;
121 return $arr;
122 }
123
124 private static function EncodeValue($data) {
125 if (is_null($data)) {
126 return chr(0);
127 } else if (is_scalar($data)) {
128 return chr(1).(string)$data;
129 } else if (is_array($data)) {
130 $iscollection = TRUE;
131 foreach ($data as $key => $value) if (!is_int($key) || $key < 0) $iscollection = FALSE;
132 $ret = $iscollection ? chr(2) : chr(3);
133 foreach ($data as $key => $value) {
134 if (!$iscollection) {
135 if (strlen($key) > 0xff) throw new Exception('Dictionary key is too long');
136 $ret .= chr(strlen($key)).$key;
137 }
138 $bytes = self::EncodeValue($value);
139 $ret .= marc_encode_int32be(strlen($bytes)).$bytes;
140 }
141 return $ret;
142 } else {
143 throw new Exception('Unable to encode type '.gettype($data));
144 }
145 }
146 private static function DecodeValue($data, $offset = 0, $length = -1) {
147 if ($length == -1) $length = strlen($data) - $offset;
148 if ($length < 0) throw new Exception('Truncated');
149 if ($length == 0) return NULL;
150 $type = ord($data[$offset]); $offset++; $length--;
151 switch ($type) {
152 case 0: return NULL;
153 case 1: return substr($data, $offset, $length);
154 case 2:
155 $value = array();
156 while ($length > 0) {
157 if (4 > $length) throw new Exception('Truncated');
158 $len = marc_decode_int32be($data, $offset); $offset += 4; $length -= 4;
159 if ($len > $length) throw new Exception('Truncated');
160 $value[] = self::DecodeValue($data, $offset, $len); $offset += $len; $length -= $len;
161 }
162 return $value;
163 case 3:
164 $value = array();
165 while ($length > 0) {
166 if (1 > $length) throw new Exception('Truncated');
167 $len = ord($data[$offset]); $offset++; $length--;
168 if ($len + 4 > $length) throw new Exception('Truncated');
169 $key = substr($data, $offset, $len); $offset += $len; $length -= $len;
170 $len = marc_decode_int32be($data, $offset); $offset += 4; $length -= 4;
171 if ($len > $length) throw new Exception('Truncated');
172 $value[$key] = self::DecodeValue($data, $offset, $len); $offset += $len; $length -= $len;
173 }
174 return $value;
175 default: throw new Exception('Unsupported type code '.$type);
176 }
177 }
178 public static function CanImport($nw, $cu = NULL) {
179 if (!$nw || !isset($nw['label'])) return FALSE;
180 if ($cu) {
181 if ($nw['label'] != $cu['label']) return FALSE;
182 if ($cu['serial'] >= $nw['serial']) return FALSE;
183 if ($cu['key'] == $nw['key']) return TRUE;
184 if (isset($cu['expiration']) && $cu['expiration'] < time()) return TRUE;
185 if ($cu['serial'] < time() - 365*24*60*60) return TRUE;
186 if (isset($cu['transfer']) && (!strlen($cu['transfer']) || $cu['transfer'] == $nw['key'])) return TRUE;
187 return FALSE;
188 } else {
189 if ($nw['serial'] < time() - 365*24*60*60) return FALSE;
190 if (isset($nw['expiration']) && $nw['expiration'] < time()) return FALSE;
191 }
192 return TRUE;
193 }
194 }
195 abstract class MARCDatabase {
196 private $_importResourceFilterCallbacks = array(), $_importResourceCallbacks = array();
197 public abstract function GetResource($label);
198 protected abstract function ImportInternal($resource);
199 public abstract function GetResources($labelprefix = NULL, $mints = NULL);
200 public abstract function DeleteResource($label);
201 public function Import($resource, $force = FALSE) {
202 if (is_string($resource)) $resource = MARCUpdate::Decode($resource);
203 if (!$resource) return FALSE;
204 $current = $this->GetResource($resource['label']);
205 if (!$force) foreach ($this->_importResourceFilterCallbacks as $callback) if (!call_user_func($callback, $this, $resource, $current)) return FALSE;
206 if (!$force && !MARCUpdate::CanImport($resource, $current)) return FALSE;
207 if (!$resource->Verify()) return FALSE;
208 if (!$this->ImportInternal($resource)) return FALSE;
209 $this->ResourceImported($resource);
210 return $resource;
211 }
212 public function UpdateResource($resource, $seckey, $force = FALSE) {
213 $res = MARCUpdate::Create($resource, $seckey, $force ? NULL : $this->GetResource($resource['label']));
214 if (!$res) return FALSE;
215 return $this->Import($res, $force);
216 }
217 protected function CanImportResource(&$resource, $force = FALSE) {
218 if (is_string($resource)) $resource = MARCUpdate::Decode($resource);
219 if (!$resource) return FALSE;
220 $current = $this->GetResource($resource['label']);
221 if (!$force) foreach ($this->_importResourceFilterCallbacks as $callback) if (!call_user_func($callback, $this, $resource, $current)) return FALSE;
222 if (!$force && !MARCUpdate::CanImport($resource, $current)) return FALSE;
223 if (!$resource->Verify()) return FALSE;
224 return TRUE;
225 }
226 protected function ResourceImported($resource) {
227 foreach ($this->_importResourceCallbacks as $callback) call_user_func($callback, $this, $resource);
228 }
229 public function RegisterResourceFilterCallback($callback) {
230 $this->_importResourceFilterCallbacks[] = $callback;
231 }
232 public function RegisterResourceImportCallback($callback) {
233 $this->_importResourceCallbacks[] = $callback;
234 }
235 public function SyncHTTP($server, $options = array()) {
236 $log = isset($options['log']) ? $options['log'] : TRUE;
237 $result = array();
238 $method = isset($options['method']) ? $options['method'] : 'PUT';
239 if ($method != 'POST' && $method != 'PUT' && $method != 'GET') throw new Exception('Unsupported method '.$method);
240 $result['exported'] = 0;
241 $post = NULL;
242 if ($method != 'GET' && (!isset($options['noexport']) || !$options['noexport'])) {
243 $post = $method == 'POST' ? array() : '';
244 $updates = $this->GetResources(NULL, isset($options['exporttimestamp']) ? $options['exporttimestamp'] : NULL);
245 foreach ($updates as $update) {
246 if (!is_string($update)) $update = $update['updatemessage'];
247 if ($method == 'PUT') {
248 $post .= marc_encode_int32be(strlen($update)).$update;
249 } else {
250 $post[] = 'update[]='.urlencode($update);
251 }
252 $result['exported']++;
253 }
254 $updates = NULL;
255 if ($method == 'POST') $post = implode('&', $post);
256 }
257 if ($log) echo "Sending ".strlen($post)." bytes... ";
258 $result['bytessent'] = strlen($post);
259 $ch = curl_init();
260 $timestamp = isset($options['importtimestamp']) ? intval($options['importtimestamp']) : 0;
261 curl_setopt($ch, CURLOPT_URL, $server.'?version=3&get='.$timestamp);
262 if ($method == 'PUT') curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
263 else if ($method == 'POST') curl_setopt($ch, CURLOPT_POST, TRUE);
264 if ($post != NULL) curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
265 $post = NULL;
266 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
267 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
268 curl_setopt($ch, CURLOPT_TIMEOUT, 60);
269 $response = curl_exec($ch);
270 if ($response === FALSE) throw new Exception('HTTP request failed');
271 if ($log) echo "received ".strlen($response)." bytes.\n";
272 $result['bytesreceived'] = strlen($response);
273 if (($httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE)) != 200) throw new Exception('Received unexpected HTTP code '.$httpcode.' - '.(strlen($response)?var_export($response,TRUE):''));
274 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):''));
275 curl_close($ch);
276 $i = 0;
277 if ($i + 2 > strlen($response)) throw new Exception('Data has been truncated');
278 $version = ord($response[$i++]);
279 if ($version != 3) throw new Exception('Unsupported protocol version '.$version);
280 for ($numext = ord($response[$i++]); $numext > 0; $numext--) {
281 if ($i + 3 > strlen($response)) throw new Exception('Truncated');
282 $extid = ord($response[$i++]);
283 $extlen = marc_decode_int16be($response, $i); $i += 2;
284 if ($i + $extlen > strlen($response)) throw new Exception('Truncated');
285 switch ($extid) {
286 case 2:
287 if ($extlen >= 4 * 3) {
288 $result['remotereceived'] = marc_decode_int32be($response, $i + 0);
289 $result['remoteimported'] = marc_decode_int32be($response, $i + 4);
290 $result['remoteexported'] = marc_decode_int32be($response, $i + 8);
291 }
292 break;
293 case 3: if ($extlen >= 4) $result['importtimestamp'] = marc_decode_int32be($response, $i); break;
294 }
295 $i += $extlen;
296 }
297 if ($log) echo "Exported ".$result['exported']." updates of which ".(isset($result['remoteimported']) ? $result['remoteimported'] : 'none')." were imported.\n";
298 $result['updates'] = array();
299 $result['imported'] = 0;
300 $result['updatesreceived'] = 0;
301 while ($i < strlen($response)) {
302 if ($i + 4 > strlen($response)) throw new Exception('Truncated');
303 $len = marc_decode_int32be($response, $i); $i += 4;
304 if ($i + $len > strlen($response)) throw new Exception('Truncated');
305 $result['updatesreceived']++;
306 $value = substr($response, $i, $len); $i += $len;
307 $res = MARCUpdate::Decode($value);
308 $result['updates'][] = $res;
309 if (!$this->Import($value)) continue;
310 $result['imported']++;
311 }
312 if ($log) echo "Imported ".$result['imported']." out of ".$result['updatesreceived']." updates.\n";
313 return $result;
314 }
315 }
316 class MARCDatabaseFlatFile extends MARCDatabase {
317 private $dbfile = NULL;
318 private $resources = array();
319 private $changed = FALSE;
320 public function GetResource($label) {
321 if (!isset($this->resources[$label])) return NULL;
322 return $this->resources[$label]['update'];
323 }
324 public function DeleteResource($label) {
325 if (!isset($this->resources[$label])) return FALSE;
326 unset($this->resources[$label]);
327 $this->changed = TRUE;
328 return TRUE;
329 }
330 protected function ImportInternal($update) {
331 $this->resources[$update['label']] = array('timestamp' => time(), 'update' => $update);
332 $this->changed = TRUE;
333 return TRUE;
334 }
335 public function GetResources($labelprefix = NULL, $mints = NULL) {
336 return new MARCDatabaseFlatFileFilteredResourceIterator(new ArrayIterator($this->resources), $labelprefix, $mints);
337 }
338 public function __construct($filename = FALSE) {
339 if ($filename) $this->Open($filename);
340 }
341 private function OpenFile($filename) {
342 $this->Close();
343 $this->dbfile = fopen($filename, 'c+');
344 if (!$this->dbfile) throw new Exception('Could not open database file');
345 if (!flock($this->dbfile, LOCK_EX)) throw new Exception('Could not lock database file');
346 }
347 public function Open($filename) {
348 $this->OpenFile($filename);
349 $this->resources = array();
350 $this->changed = FALSE;
351 rewind($this->dbfile);
352 while (true) {
353 $data = fread($this->dbfile, 8);
354 if (strlen($data) == 0) break;
355 if (strlen($data) != 8) throw new Exception('Database truncated');
356 $ts = marc_decode_int32be($data, 0);
357 $len = marc_decode_int32be($data, 4);
358 $data = fread($this->dbfile, $len);
359 if (strlen($data) != $len) throw new Exception('Database truncated');
360 $res = MARCUpdate::Decode($data);
361 if (!$res) continue;
362 $this->resources[$res['label']] = array('timestamp' => $ts, 'update' => $res);
363 }
364 }
365 public function Save() {
366 if (!$this->dbfile) throw new Exception('No database file is open');
367 rewind($this->dbfile);
368 ftruncate($this->dbfile, 0);
369 foreach ($this->resources as $res) {
370 fwrite($this->dbfile, marc_encode_int32be($res['timestamp']));
371 $u = (string)$res['update']['updatemessage'];
372 fwrite($this->dbfile, marc_encode_int32be(strlen($u)));
373 fwrite($this->dbfile, $u);
374 }
375 $this->changed = FALSE;
376 }
377 public function SaveAs($filename) {
378 $this->OpenFile($filename);
379 $this->Save();
380 }
381 public function Close() {
382 if ($this->dbfile) {
383 flock($this->dbfile, LOCK_UN);
384 fclose($this->dbfile);
385 $this->dbfile = NULL;
386 }
387 }
388 public function IsChanged() {
389 return $this->changed;
390 }
391 }
392 class MARCDatabaseFlatFileFilteredResourceIterator implements Iterator {
393 private $source, $labelprefixlength, $labelprefix, $mints;
394 public function __construct($source, $labelprefix, $mints) {
395 $this->source = $source;
396 $this->labelprefixlength = is_null($labelprefix) ? 0 : strlen($labelprefix);
397 $this->labelprefix = $labelprefix;
398 $this->mints = intval($mints);
399 }
400 public function current() { $r = $this->source->current(); return $r ? $r['update'] : NULL; }
401 public function key() { return $this->source->key(); }
402 public function valid() { return $this->source->valid(); }
403 public function next() { $this->source->next(); $this->findnext(); }
404 public function rewind() { $this->source->rewind(); $this->findnext(); }
405 private function findnext() {
406 while ($this->source->valid()) {
407 $c = $this->source->current();
408 if ($c['timestamp'] >= $this->mints && (!$this->labelprefixlength || substr($c['update']['label'], 0, $this->labelprefixlength) == $this->labelprefix)) break;
409 $this->source->next();
410 }
411 }
412 }
413 class MARCDatabaseSQLite extends MARCDatabase {
414 private $pdo = NULL;
415 protected function DBPrepareStatement($query, $args = NULL) {
416 $stmt = $this->pdo->prepare($query);
417 if (!is_array($args) && !is_null($args)) $args = array($args);
418 $stmt->execute($args);
419 return $stmt;
420 }
421 private function DBUpdate($query, $args = NULL) {
422 $stmt = $this->DBPrepareStatement($query, $args);
423 $cnt = $stmt->rowCount();
424 $stmt->closeCursor();
425 return $cnt;
426 }
427 public function __construct($filename) {
428 $this->pdo = new PDO('sqlite:'.$filename);
429 $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
430 $this->DBUpdate('CREATE TABLE IF NOT EXISTS `resources` (`timestamp` INTEGER, `label` BLOB PRIMARY KEY, `update` BLOB)');
431 }
432 public function GetResource($label) {
433 $stmt = $this->DBPrepareStatement('SELECT `update` FROM `resources` WHERE `label` = ?', $label);
434 $res = $stmt->fetchColumn();
435 $stmt->closeCursor();
436 if ($res === FALSE) return NULL;
437 return MARCUpdate::Decode($res);
438 }
439 public function DeleteResource($label) {
440 return $this->DBUpdate('DELETE FROM `resources` WHERE `label` = ?', $label) != 0;
441 }
442 protected function ImportInternal($update) {
443 $args = array('d' => $update['updatemessage'], 't' => time(), 'l' => $update['label']);
444 if ($this->DBUpdate('UPDATE `resources` SET `update` = :d, `timestamp` = :t WHERE `label` = :l', $args) == 0)
445 $this->DBUpdate('INSERT OR IGNORE INTO `resources` (`label`, `update`, `timestamp`) VALUES (:l, :d, :t)', $args);
446 return TRUE;
447 }
448 public function GetResources($labelprefix = NULL, $mints = NULL) {
449 $labelprefix = is_null($labelprefix) ? '' : (string)$labelprefix;
450 return $this->GetResourcesFromQuery('SELECT `update` FROM `resources` WHERE `timestamp` >= ? AND SUBSTR(`label`, 1, ?) = ? ORDER BY `label`', array(intval($mints), strlen($labelprefix), $labelprefix));
451 }
452 public function GetResourcesFromQuery($query, $args = NULL) {
453 return $this->GetResourcesFromStatement($this->DBPrepareStatement($query, $args));
454 }
455 public function GetResourcesFromStatement($statement) {
456 return new MARCDatabaseSQLiteResourceIterator($statement);
457 }
458 public function IsChanged() {
459 return FALSE;
460 }
461 public function Close() {
462 }
463 }
464 class MARCDatabaseSQLiteResourceIterator implements Iterator {
465 private $source, $current = FALSE;
466 public function __construct($source) { $this->source = $source; }
467 public function current() { return $this->current === FALSE ? NULL : MARCUpdate::Decode($this->current); }
468 public function key() { return NULL; }
469 public function valid() { return $this->current !== FALSE; }
470 public function next() { $this->current = $this->source->fetchColumn(); if ($this->current === FALSE) $this->source->closeCursor(); }
471 public function rewind() { $this->next(); }
472 }
473 class MARCDatabaseDBA extends MARCDatabase {
474 private $db = NULL;
475 public function __construct($path, $mode = 'cd', $handler = 'qdbm') {
476 $this->db = dba_open($path, $mode, $handler);
477 if ($this->db === FALSE) throw new Exception('Could not open database');
478 }
479 public function GetResource($label) {
480 $r = dba_fetch($label, $this->db);
481 if ($r === FALSE) return NULL;
482 return MARCUpdate::Decode($r);
483 }
484 public function DeleteResource($label) {
485 return dba_delete($label, $this->db);
486 }
487 protected function ImportInternal($update) {
488 return dba_replace($update['label'], $update['updatemessage'], $this->db);
489 }
490 public function GetResources($labelprefix = NULL, $mints = NULL) {
491 return new MARCDatabaseDBAFilteredResourceIterator($this->db, $labelprefix, $mints);
492 }
493 public function Save() {
494 dba_sync($this->db);
495 }
496 public function Close() {
497 dba_close($this->db);
498 }
499 public function IsChanged() {
500 return FALSE;
501 }
502 }
503 class MARCDatabaseDBAFilteredResourceIterator implements Iterator {
504 private $db, $currentkey = FALSE, $labelprefixlength, $labelprefix, $mints;
505 public function __construct($db, $labelprefix, $mints) {
506 $this->db = $db;
507 $this->labelprefixlength = is_null($labelprefix) ? 0 : strlen($labelprefix);
508 $this->labelprefix = $labelprefix;
509 $this->mints = intval($mints);
510 }
511 public function current() { return MARCUpdate::Decode(dba_fetch($this->currentkey, $this->db)); }
512 public function key() { return $this->currentkey; }
513 public function valid() { return strlen($this->currentkey); }
514 public function next() { $this->currentkey = dba_nextkey($this->db); $this->findnext(); }
515 public function rewind() { $this->currentkey = dba_firstkey($this->db); $this->findnext(); }
516 private function findnext() { while ($this->currentkey !== FALSE && ($this->labelprefixlength && substr($this->currentkey, 0, $this->labelprefixlength) != $this->labelprefix)) $this->currentkey = dba_nextkey($this->db); }
517 }
518
519 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]); }
520 function marc_decode_int16be($data, $i = 0) { return (ord($data[$i+0]) << 8) | ord($data[$i+1]); }
521 function marc_encode_int32be($v) { return pack("N", $v); }
522 function marc_encode_int16be($v) { return pack("n", $v); }
523