0
|
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 |