Couple more tweaks
[loomclient.git] / LoomClient.php
blob36dd047c67f21aab24ed06d63ad691dade3a6891
1 <?php
2 require_once "bcbitwise.php";
3 require_once "Socket.php";
4 require_once "LoomRandom.php";
6 /*
7 * Client to the webapp at http://loom.cc/
8 * See https://loom.cc/?function=help&topic=grid_tutorial&mode=advanced
9 */
11 class LoomClient
13 var $url_prefix; // Who you gonna call?
14 var $bits128; // 128 1-bits = 2**128 - 1
15 var $socket; // our connection to the Loom server
16 var $random; // An instance of LoomRandom
18 function LoomClient($prefix = 'https://loom.cc/') {
19 if (substr($prefix, -1) != '/') $prefix .= '/';
20 $this->url_prefix = $prefix;
21 $this->bits128 = bcsub(bcleftshift(1, 128), 1);
22 $this->socket = FALSE;
23 $this->random = new LoomRandom();
26 // This is all you really need to call
27 // The functions below are just syntactic sugar for calling get()
28 /* Save the old non-persistent connection version
29 function get($keys, &$url) {
30 $url = $this->url($this->url_prefix, $keys);
31 // Kluge around protocol warning
32 // I really want try/finally here, but PHP doesn't have it
33 $erpt = $this->disable_warnings();
34 $kv = file_get_contents($url);
35 $this->reenable_warnings($erpt);
36 return $this->parsekv($kv);
40 // The base of the Loom interface
41 // Send an HTTP GET, with args in $keys.
42 // Return whatever Loom returns.
43 function rawget($keys, &$url) {
44 return $this->rawGetOrPost($keys, 'get', $url);
47 function rawpost($keys, &$url) {
48 return $this->rawGetOrPost($keys, 'post', $url);
51 function rawGetOrPost($keys, $getOrPost, &$url) {
52 if (!$this->socket) {
53 // Should make this less brittle
54 if (substr($this->url_prefix, 0, 5) == 'https') {
55 $host = substr($this->url_prefix, 8, -1);
56 $ssl = TRUE;
57 } else {
58 $host = substr($this->url_prefix, 7, -1);
59 $ssl = FALSE;
61 $this->socket = new Socket($host, $ssl);
62 $this->socket->connect();
64 $uri = $this->url('', $keys);
65 $url = $this->url_prefix . $uri;
66 $uri = '/' . $uri;
67 if ($getOrPost == 'post') $this->socket->post($uri);
68 else $this->socket->get($uri);
69 return $this->socket->body;
72 // This is all you really need to call
73 // The functions below are just syntactic sugar for calling get()
74 function get($keys, &$url) {
75 $kv = $this->rawget($keys, $url);
76 return $this->parsekv($kv);
80 function buy($type, $location, $usage, &$url) {
81 return $this->get(array('function' => 'grid',
82 'action' => 'buy',
83 'type' => $type,
84 'loc' => $location,
85 'usage' => $usage),
86 $url);
89 function sell($type, $location, $usage, &$url) {
90 return $this->get(array('function' => 'grid',
91 'action' => 'sell',
92 'type' => $type,
93 'loc' => $location,
94 'usage' => $usage),
95 $url);
98 function issuer($type, $orig, $dest, &$url) {
99 return $this->get(array('function' => 'grid',
100 'action' => 'issuer',
101 'type' => $type,
102 'orig' => $orig,
103 'dest' => $dest),
104 $url);
107 function touch($type, $location, &$url) {
108 return $this->get(array('function' => 'grid',
109 'action' => 'touch',
110 'type' => $type,
111 'loc' => $location),
112 $url);
115 function look($type, $hash, &$url) {
116 return $this->get(array('function' => 'grid',
117 'action' => 'look',
118 'type' => $type,
119 'hash' => $hash),
120 $url);
123 function move($type, $quantity, $origin, $destination, &$url) {
124 return $this->get(array('function' => 'grid',
125 'action' => 'move',
126 'type' => $type,
127 'qty' => $quantity,
128 'orig' => $origin,
129 'dest' => $destination),
130 $url);
133 // $locs and $types are lists of space-separated hex IDs.
134 // If $zeroes is true, will return 0 values.
135 function scan($locs, $types, $zeroes, &$url) {
136 $a = array('function' => 'grid',
137 'action' => 'scan',
138 'locs' => $locs,
139 'types' => $types);
140 if ($zeroes) $a['zeroes'] = '1';
141 return $this->get($a, $url);
144 // $locs is array(locname => id, ...)
145 // $types is array(typename => array('id' => id,
146 // 'name' => typename,
147 // 'min_precision' => min_precision,
148 // 'scale' => scale),
149 // ...)
150 // Returns array(locname => array(typename => value, ...), ...)
151 // Returns FALSE if it gets an error from the loom server
152 function namedScan($locs, $types, $zeroes, &$url) {
153 $loca = array();
154 $locstring = "";
155 foreach ($locs as $locname => $id) {
156 if ($locstring != '') $locstring .= ' ';
157 $locstring .= $id;
158 $loca[$id] = $locname;
161 $typea = array();
162 $typestring = "";
163 foreach ($types as $typename => $attributes) {
164 $id = $attributes['id'];
165 if ($typestring != '') $typestring .= ' ';
166 $typestring .= $id;
167 $typea[$id] = $attributes;
170 $res = $this->scan($locstring, $typestring, $zeroes, &$url);
172 $resa = array();
173 foreach ($loca as $id => $locname) {
174 $vals = explode(' ', $res["loc/$id"]);
175 $vala = array();
176 foreach ($vals as $val) {
177 $val = explode(':', $val);
178 $value = $val[0];
179 $id = $val[1];
180 $attributes = $typea[$id];
181 $typename = $attributes['name'];
182 $min_precision = $attributes['min_precision'];
183 $scale = $attributes['scale'];
184 $vala[$typename] = $this->applyScale($value, $min_precision, $scale);
186 $resa[$locname] = $vala;
188 return $resa;
191 function applyScale($value, $min_precision, $scale) {
192 if ($value < 0) $value++;
193 if ($scale > 0) $value = bcdiv($value, bcpow(10, $scale, 0), $scale);
195 $dotpos = strpos($value, '.');
197 if ($dotpos > 0) {
198 while (substr($value, -1) == '0') {
199 $value = substr($value, 0, strlen($value)-1);
201 if (substr($value, -1) == '.') {
202 $value = substr($value, 0, strlen($value)-1);
203 $dotpos = 0;
207 if ($min_precision > 0) {
208 if ($dotpos == 0) {
209 $value .= ".";
210 $dotpos = strlen($value);
211 } else $dotpos++;
212 $places = strlen($value) - $dotpos;
213 if ($min_precision > $places) {
214 $value .= str_repeat("0", $min_precision - $places);
217 return $value;
220 function buy_archive($loc, $usage, &$url) {
221 return $this->get(array('function' => 'archive',
222 'action' => 'buy',
223 'loc' => $loc,
224 'usage' => $usage),
225 $url);
228 function sell_archive($loc, $usage, &$url) {
229 return $this->get(array('function' => 'archive',
230 'action' => 'sell',
231 'loc' => $loc,
232 'usage' => $usage),
233 $url);
236 function touch_archive($loc, &$url) {
237 return $this->get(array('function' => 'archive',
238 'action' => 'touch',
239 'loc' => $loc),
240 $url);
243 function look_archive($hash, &$url) {
244 return $this->get(array('function' => 'archive',
245 'action' => 'look',
246 'hash' => $hash),
247 $url);
250 function write_archive($loc, $usage, $content, &$url) {
251 return $this->get(array('function' => 'archive',
252 'action' => 'write',
253 'loc' => $loc,
254 'usage' => $usage,
255 'content' => $content),
256 $url);
259 function url($prefix, $keys) {
260 $str = $prefix;
261 $delim = '?';
262 foreach($keys as $key => $value) {
263 $str .= $delim . $key . '=' . urlencode($value);
264 $delim = '&';
266 return $str;
269 function parseFolder($location, $folder) {
270 global $client;
272 $paren_pos = strpos($folder, "(");
273 if ($paren_pos || $folder[0] == '(') {
274 $kv = substr($folder, $paren_pos);
275 $res = array();
276 $keys = $client->parsekv($kv);
277 $keytypes = explode(' ', $keys['list_type']);
278 $types = array();
279 foreach ($keytypes as $keytype) {
280 $type = array('name' => $keys["type_name.$keytype"],
281 'id' => $keytype,
282 'min_precision' => blankToZero($keys["type_min_precision.$keytype"]),
283 'scale' => blankToZero($keys["type_scale.$keytype"]));
284 //$types[$keytype] = $type;
285 $types[$type['name']] = $type;
287 ksort($types);
288 $res['types'] = $types;
289 $keylocs = explode(' ', $keys['list_loc']);
290 $locs = array();
291 foreach ($keylocs as $keyloc) {
292 $name = $keys["loc_name.$keyloc"];
293 if ($name != '') {
294 if ($keyloc == $location) {
295 $folder_name = $name;
297 $locs[$name] = $keyloc;
298 //$locs[$keyloc] = $name;
301 ksort($locs);
302 $res['locs'] = $locs;
303 $res['name'] = $folder_name;
304 $res['loc'] = $location;
305 return $res;
307 return FALSE;
310 // Convert our array() representation of the folder
311 // into the string to write into the archive
312 // Not used. We let Loom itself munge the folder string.
313 function folderArchiveString($folder) {
314 $res = "Content-type: loom/folder\n\n(\n";
315 $types = "";
316 $ids = "";
317 foreach ($folder['types'] as $name => $type) {
318 $id = $type['id'];
319 if ($ids != '') $ids .= ' ';
320 $ids .= $id;
321 $min_precision = $type['min_precision'];
322 $scale = $type['scale'];
323 $types .= ":type_name.$id\n=" . $this->quote_cstring($name) . "\n";
324 if ($min_precision != '0') {
325 $types .= ":type_min_precision.$id\n=$min_precision\n";
327 if ($scale != '0') {
328 $types .= ":type_scale.$id\n=$scale\n";
331 $res .= ":list_type\n=$ids\n";
332 $res .= $types;
334 $ids = "";
335 $locations = "";
336 foreach ($folder['locs'] as $name => $location) {
337 if ($ids != '') $ids .= ' ';
338 $ids .= $location;
339 $locations .= ":loc_name.$location\n=" . $this->quote_cstring($name) . "\n";
341 $res .= ":list_loc\n=$ids\n";
342 $res .= $locations;
344 $res .= ")\n";
346 return $res;
349 // Return the session associated with a folder location
350 // Create a new session if one doesn't already exist,
351 // buying the location to store it as necessary.
352 // Returns false if it can't buy the session location
353 function folderSession($folder_location) {
354 $loc = $this->leftPadHex(bcxorhex($folder_location, "1"), 32);
355 $res = $this->touch_archive($loc, $url);
356 if ($res['status'] == 'success') {
357 return $res['content'];
359 $session = $this->random->random_id();
360 $res = $this->buy_archive($session, $folder_location, $url);
361 if ($res['status'] != 'success') return false;
362 // Probably don't need this, but you never know
363 $this->buy_archive($loc, $folder_location, $url);
364 $res = $this->write_archive($loc, $folder_location, $session, $url);
365 if ($res['status'] != 'success') return false;
366 return $session;
369 // This doesn't yet test that it worked.
370 // I'll wait for Patrick to make a KV-returning version,
371 // instead of attempting to parse the returned HTML
372 function renameFolderLocation($session, $oldname, $newname) {
373 return $this->rawget(array('function' => 'folder_locations',
374 'session' => $session,
375 'old_name' => $oldname,
376 'new_name' => $newname,
377 'save' => 'Save'),
378 $url);
381 // This doesn't yet test that it worked.
382 // I'll wait for Patrick to make a KV-returning version,
383 // instead of attempting to parse the returned HTML
384 function newFolderLocation($session, $newname, $newlocation) {
385 return $this->rawget(array('function' => 'folder_locations',
386 'session' => $session,
387 'add_location' => '1',
388 'loc' => $newlocation,
389 'nickname' => $newname,
390 'save' => 'Save'),
391 $url);
394 // Logout from Loom, destroying the old session
395 function logout($session) {
396 return $this->rawget(array('function' => 'folder',
397 'logout' => '1',
398 'session' => $session),
399 $url);
402 function parsekv($kv, $recursive=FALSE) {
403 $lines = explode("\n", $kv);
404 $first = true;
405 $res = array();
406 $stackptr = 0;
407 $stack = array();
408 foreach ($lines as $line) {
409 $line = trim($line);
410 //echo "$line<br>\n";
411 if ($first && ($line != '(')) {
412 return $res; // Could throw exception in PHP 5
414 $first = false;
415 if ($line == ')') {
416 if (!$recursive || $stackptr == 0) return $res;
417 $child = $res;
418 $res = $stack[--$stackptr];
419 $key = $stack[--$stackptr];
420 $res[$key] = $child;
421 //echo "popped: $stackptr<pre>\n"; print_r($res); echo "</pre>\n";
422 } else {
423 if (substr($line, 0, 1) == ':') $key = substr($line, 1);
424 elseif (substr($line, 0, 1) == '=') {
425 $value = substr($line, 1);
426 if ($recursive && $value == "(") {
427 $child = array();
428 $stack[$stackptr++] = $key;
429 $stack[$stackptr++] = $res;
430 //echo "pushed: $stackptr<br>\n";
431 $res = $child;
432 } else {
433 $value = $this->unquote_cstring($value);
434 $res[$key] = $value;
441 function array2kv($array, $res="(\n") {
442 foreach ($array as $key => $value) {
443 $res .= ":$key\n";
444 if (is_array($value)) $res = $this->array2kv($value, $res . "=(\n");
445 else $res .= "=" . $this->quote_cstring($value) . "\n";
447 $res .= ")\n";
448 return $res;
451 function quote_cstring($cstring) {
452 $res = '';
453 for ($i=0; $i<strlen($cstring); $i++) {
454 $chr = substr($cstring, $i, 1);
455 if ($chr == "\n") $res .= '\n';
456 elseif ($chr == '"') $res .= '\"';
457 elseif ($chr == "\t") $res .= '\t';
458 elseif ($chr == "\\") $res .= "\\\\";
459 elseif ($chr < ' ' || $chr > '~') $res .= '\\'.sprintf('%03o', ord($chr));
460 else $res .= $chr;
462 return $res;
465 function unquote_cstring($cstring) {
466 $res = '';
467 $len = strlen($cstring);
468 $i = 0;
469 while ($i<$len) {
470 $chr = substr($cstring, $i, 1);
471 if ($chr == "\\") {
472 $i++;
473 if ($i >= $len) {
474 $res .= $chr;
475 break;
477 $chr = substr($cstring, $i, 1);
478 if ($chr == 'n') $res .= "\n";
479 elseif ($chr == '"') $res .= '"';
480 elseif ($chr == 't') $res .= "\t";
481 elseif ($chr == "\\") $res .= "\\";
482 elseif ($chr >= '0' and $chr <= '9') {
483 if ($len < ($i + 3)) {
484 $res .= substr($cstring, $i-1);
485 break;
487 sscanf(substr($cstring, $i, 3), '%o', &$n);
488 $res .= chr($n);
489 $i += 2;
491 else $res .= "\\" . $chr;
493 else {
494 $res .= $chr;
496 $i++;
498 return $res;
501 // This enables a kluge in get() to turn off the protocol warning that
502 // results from doing a HTTP GET to http://loom.cc/
503 function disable_warnings() {
504 $erpt = error_reporting();
505 error_reporting(E_ALL ^ E_WARNING ^ E_NOTICE);
506 return $erpt;
509 function reenable_warnings($erpt) {
510 error_reporting($erpt);
513 // Return the sha256 hash of a string.
514 // The result is encoded as hex, and guaranteed to be 64 charaacters,
515 // with leading zeroes added, if necessary.
516 function sha256($str) {
517 if (function_exists('hash_init')) {
518 // Modern PHP
519 $ctx = hash_init('sha256');
520 hash_update($ctx, $str);
521 $hash = hash_final($ctx);
522 } else if (function_exists('mhash')) {
523 // Old PHP with mhash compiled in
524 $hash = bin2hex(mhash(MHASH_SHA256, $str));
525 } else {
526 // Not a hash, really, but the best we can do
527 $hash = bin2hex($str);
528 if (strlen($hash) > 64) $hash = substr($hash, 1, 64);
530 return $this->leftPadHex($hash, 64);
533 // PHP has bin2hex($x). An easier to remember name for pack("H*", $x)
534 // Note that this does NOT get you a string that looks like a decimal number.
535 // It's raw bits, 8 bits per characetr.
536 function hex2bin($x) {
537 return pack("H*", $x);
540 // Loom changes an SHA256 hash to a location by xoring the two halves
541 // Input and output are both encoded as hex
542 // Won't work correctly
543 function hash2location($hash) {
544 $value = bchexdec($hash);
545 $bits128 = $this->bits128;
546 $location = bcxor(bcrightshift($value, 128), bcand($value, $bits128));
547 return $this->leftPadHex(bcdechex($location), 32);
550 function leftPadHex($hex, $chars) {
551 if (strlen($hex) < $chars) {
552 $hex = str_repeat("0", $chars - $strlen($hex)) . $hex;
554 return $hex;
557 function xorLocations($l1, $l2) {
558 return $this->leftPadHex(bcxorhex($l1, $l2), 32);
561 // Returns true if $location is occupied for $type, i.e. if touch() will succeed
562 function isLocationOccuppied($type, $location) {
563 $res = $this->touch($type, $location, $url);
564 return $res['status'] == 'success';
567 // Returns true if $location is vacant for $type,
568 // i.e. if touch() fails with vacant
569 function isLocationVacant($type, $location) {
570 $res = $this->touch($type, $location, $url);
571 return ($res['status'] == 'fail') && ($res['error_loc'] == 'vacant');
574 // Return a random vacant location.
575 // If can't find one after 10 tries, return false.
576 function randomVacantLocation($type) {
577 for ($i=0; $i<10; $i++) {
578 $id = $this->random->random_id();
579 if ($this->isLocationVacant($type, $id)) return $id;
581 return false;
584 // Test that an ID is a valid 32-character hex string
585 function isValidID($id) {
586 return (strlen($id) == 32) && preg_match('/[a-f0-9]{32}/', $id);
589 } // End of LoomClient class
592 /* Testing code. Uncomment to run.
593 $api = new LoomClient();
594 $values = $api->move('12345678123456781234567812345678',
596 '22345678123456781234567812345678',
597 '32345678123456781234567812345678',
598 &$url);
599 echo $url . "\n";
600 print_r($values);
603 // Copyright 2008 Bill St. Clair
605 // Licensed under the Apache License, Version 2.0 (the "License");
606 // you may not use this file except in compliance with the License.
607 // You may obtain a copy of the License at
609 // http://www.apache.org/licenses/LICENSE-2.0
611 // Unless required by applicable law or agreed to in writing, software
612 // distributed under the License is distributed on an "AS IS" BASIS,
613 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
614 // See the License for the specific language governing permissions
615 // and limitations under the License.