2 require_once "bcbitwise.php";
3 require_once "Socket.php";
4 require_once "LoomRandom.php";
7 * Client to the webapp at http://loom.cc/
8 * See https://loom.cc/?function=help&topic=grid_tutorial&mode=advanced
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) {
53 // Should make this less brittle
54 if (substr($this->url_prefix
, 0, 5) == 'https') {
55 $host = substr($this->url_prefix
, 8, -1);
58 $host = substr($this->url_prefix
, 7, -1);
61 $this->socket
= new Socket($host, $ssl);
62 $this->socket
->connect();
64 $uri = $this->url('', $keys);
65 $url = $this->url_prefix
. $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',
89 function sell($type, $location, $usage, &$url) {
90 return $this->get(array('function' => 'grid',
98 function issuer($type, $orig, $dest, &$url) {
99 return $this->get(array('function' => 'grid',
100 'action' => 'issuer',
107 function touch($type, $location, &$url) {
108 return $this->get(array('function' => 'grid',
115 function look($type, $hash, &$url) {
116 return $this->get(array('function' => 'grid',
123 function move($type, $quantity, $origin, $destination, &$url) {
124 return $this->get(array('function' => 'grid',
129 'dest' => $destination),
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',
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),
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) {
155 foreach ($locs as $locname => $id) {
156 if ($locstring != '') $locstring .= ' ';
158 $loca[$id] = $locname;
163 foreach ($types as $typename => $attributes) {
164 $id = $attributes['id'];
165 if ($typestring != '') $typestring .= ' ';
167 $typea[$id] = $attributes;
170 $res = $this->scan($locstring, $typestring, $zeroes, &$url);
173 foreach ($loca as $id => $locname) {
174 $vals = explode(' ', $res["loc/$id"]);
176 foreach ($vals as $val) {
177 $val = explode(':', $val);
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;
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, '.');
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);
207 if ($min_precision > 0) {
210 $dotpos = strlen($value);
212 $places = strlen($value) - $dotpos;
213 if ($min_precision > $places) {
214 $value .= str_repeat("0", $min_precision - $places);
220 function buy_archive($loc, $usage, &$url) {
221 return $this->get(array('function' => 'archive',
228 function sell_archive($loc, $usage, &$url) {
229 return $this->get(array('function' => 'archive',
236 function touch_archive($loc, &$url) {
237 return $this->get(array('function' => 'archive',
243 function look_archive($hash, &$url) {
244 return $this->get(array('function' => 'archive',
250 function write_archive($loc, $usage, $content, &$url) {
251 return $this->get(array('function' => 'archive',
255 'content' => $content),
259 function url($prefix, $keys) {
262 foreach($keys as $key => $value) {
263 $str .= $delim . $key . '=' . urlencode($value);
269 function parseFolder($location, $folder) {
272 $paren_pos = strpos($folder, "(");
273 if ($paren_pos ||
$folder[0] == '(') {
274 $kv = substr($folder, $paren_pos);
276 $keys = $client->parsekv($kv);
277 $keytypes = explode(' ', $keys['list_type']);
279 foreach ($keytypes as $keytype) {
280 $type = array('name' => $keys["type_name.$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;
288 $res['types'] = $types;
289 $keylocs = explode(' ', $keys['list_loc']);
291 foreach ($keylocs as $keyloc) {
292 $name = $keys["loc_name.$keyloc"];
294 if ($keyloc == $location) {
295 $folder_name = $name;
297 $locs[$name] = $keyloc;
298 //$locs[$keyloc] = $name;
302 $res['locs'] = $locs;
303 $res['name'] = $folder_name;
304 $res['loc'] = $location;
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";
317 foreach ($folder['types'] as $name => $type) {
319 if ($ids != '') $ids .= ' ';
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";
328 $types .= ":type_scale.$id\n=$scale\n";
331 $res .= ":list_type\n=$ids\n";
336 foreach ($folder['locs'] as $name => $location) {
337 if ($ids != '') $ids .= ' ';
339 $locations .= ":loc_name.$location\n=" . $this->quote_cstring($name) . "\n";
341 $res .= ":list_loc\n=$ids\n";
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;
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,
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,
394 // Logout from Loom, destroying the old session
395 function logout($session) {
396 return $this->rawget(array('function' => 'folder',
398 'session' => $session),
402 function parsekv($kv, $recursive=FALSE) {
403 $lines = explode("\n", $kv);
408 foreach ($lines as $line) {
410 //echo "$line<br>\n";
411 if ($first && ($line != '(')) {
412 return $res; // Could throw exception in PHP 5
416 if (!$recursive ||
$stackptr == 0) return $res;
418 $res = $stack[--$stackptr];
419 $key = $stack[--$stackptr];
421 //echo "popped: $stackptr<pre>\n"; print_r($res); echo "</pre>\n";
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 == "(") {
428 $stack[$stackptr++
] = $key;
429 $stack[$stackptr++
] = $res;
430 //echo "pushed: $stackptr<br>\n";
433 $value = $this->unquote_cstring($value);
441 function array2kv($array, $res="(\n") {
442 foreach ($array as $key => $value) {
444 if (is_array($value)) $res = $this->array2kv($value, $res . "=(\n");
445 else $res .= "=" . $this->quote_cstring($value) . "\n";
451 function quote_cstring($cstring) {
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));
465 function unquote_cstring($cstring) {
467 $len = strlen($cstring);
470 $chr = substr($cstring, $i, 1);
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);
487 sscanf(substr($cstring, $i, 3), '%o', &$n);
491 else $res .= "\\" . $chr;
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
);
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')) {
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));
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;
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;
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',
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.