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