php/nextcloudOld.php
<?php
require_once 'net.php';
function xmlPP($s) {
/* pretty print xml
each element on a separate line
intendation showing the nesting level
linefeed and intendations are inserted only before the < (tag begin char) text remains unchanged
*/
return preg_replace_callback(
'=<[/?]?|[/?]?>='
, function ($m) {
static $lv = 0, $laO = 0;
if ($m[0] ==='<') {
$laO = 1;
$r = "\n" . str_repeat(' ', $lv++);
} elseif ($m[0] ==='<?') {
$r = "\n" . str_repeat(' ', $lv);
} elseif ($m[0] ==='</') {
$lv--;
$r = $laO ? '' : "\n" . str_repeat(' ', $lv);
$laO = 0;
} else {
$r = '';
if ($m[0] ==='/>') {
$lv--;
$laO = 0;
}
}
return $r . $m[0];
}
, $s);
}
function fn2valid($f) { # in the nfts filesystem certain file names are not allowed, they are replace here by similair valid names
$r = str_replace('*', '@', trim($f, ' ')); // trailing spaces are disallowed, stars are illegal
if (str_ends_with($r, '.'))
$r[-1] = '@';
return $r === '' ? '___' : $r;
}
class NextCloud {
/*
docs: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/index.html
*/
# CFG = ['file' => ['name', 'type', 'id', 'modTst', 'size', 'owner', 'path', 'parent', 'creTst']]
public static $cfg = [
[ ['d:response', '', 'responseB', 'responseE', '']
, ['d:prop', '', 'propB', '', '']
, ['d:status', '', '', 'statusE', '']
, ['d:displayname', 'name', '', 'aText']
, ['d:collection', 'type', 'aDir', '', 'd:resourcetype']
, ['d:getcontenttype', 'type', '', 'aText']
, ['oc:fileid', 'id', '', 'aText']
, ['d:getlastmodified', 'modTst', '', 'aTst']
, ['oc:size', 'size', '', 'aText']
, ['oc:owner-id', 'owner', '', 'aText']
, ['d:href', 'path', '', 'hrefE', '']
# , ['nc:group-folder-id', 'parent', '', 'aText'] always null, possibly acl related
# , ['d:creationdate', 'creTst ', '', ' aTst '] always 0. also nc:creation_time
], ['parent' => 1, 'creTst' => 1]];
public static function ini () {# class initialize
global $a2strLevel;
$a2strLevel=4;
self::$cfg = Pipeline::cfgBase(...self::$cfg);
#out("nextcloud cfg 1", self::$cfg);
foreach(Cloud::ROWKYS as $nm => $flds)
self::cfgAdd($nm, $flds);
#out("nextcloud cfg 2", self::$cfg);
#out("nextcloud cfg 3", self::$cfg['file']);
}
public static function cfgAdd($nm, $flds) {
$c = Pipeline::cfg(self::$cfg, $flds);
$c['ncFlds'] = "\n <" . implode("/>\n <", $c['flds']) . "/>";
out('---add-----------------', $c);
self::$cfg[$nm] = isset(self::$cfg[$nm]) ? err("googledrive cfg key $nm already defined") : $c;
}
public $cloudId, $host, $user, $rootDir, $meta;
private $pw, $hAuth, $bUri;
public function __construct($ci, $ty, $hn, $us, $pw, $di) {
$this->cloudId = $ci;
$this->host = $hn;
$this->user = $us;
$this->pw = $pw;
$this->rootDir = $di;
$this->fPath = "/remote.php/dav/files/$di/"; # prefix in hrefs from propfind
$this->fUri = "https://$hn$this->fPath"; # dav uri for rootdir
$this->sUri = "https://$hn/remote.php/dav/";
$this->hAuth = "WWW-Authenticate: Basic realm=\"$hn\", charset=\"UTF-8\"\n"
. 'Authorization: Basic ' . base64_encode("$us:$pw");
}
public function pipeline($cfg=null, $trg = null) {
if (! is_array($cfg)) {
if (! $trg and is_string($cfg)) {
$cA = explode(',', $cfg);
$cfg = $cA[0] ?? false;
$trg = $cA[1] ?? false;
}
$cfg or $cfg = 'file';
$cfg = self::$cfg[$cfg] ?? err("cfg $cfg not in", array_keys(self::$cfg));
}
$trg or $trg = 'metaRowOut';
$p = new Pipeline($this, $cfg);
$p->req = $trg === 'string' ? new Net2Str : new Net2XMLReader;
$p->httpChk = 'HTTP/1.1 207 Multi-Status';
if ($trg === 'string')
return $p;
$p->walk = new WalkXML($cfg);
return $p->makeWalk($trg);
}
public function meta($fun, $dir) {
$now = new DateTime();
return ['timestamp' => toLocalTst($now), 'cloudId' => $this->cloudId, 'fun' => $fun
, 'dir' => $dir, 'host' => $this->host, 'user' => $this->user
, 'rootDir' => $this->rootDir, 'rootDir Uri' => $this->fUri, 'timeZone' => $now->format('e P')];
}
public function ffReq($dir='', $ppl=null, $out=null) {
/* find files and directories using Propfind Request of WebDav from Nextcloud using http
on host $hst in directory $dir
return the generated xml
*/
is_object($ppl) or $ppl = $this->pipeline($ppl);
$this->ppl = $ppl;
$out and $ppl->out = $out;
$ppl->meta= $this->meta('ff', $dir);
$ctx = stream_context_create(['http' => [
'header' => "Content-Type: application/xml\n$this->hAuth"
. "\n" . 'Depth: infinity'
, 'method' => 'PROPFIND'
, 'content' => <<<data
<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>{$ppl->cfg['ncFlds']}
</d:prop>
</d:propfind>
data ]]);
# out("ffReq uri $this->fUri$dir, pathPre {$ppl->walk->pathPre}");
return $ppl->request("$this->fUri$dir", $ctx);
# out("net got", $r, 'hdr', $ppl->net->hdr);
return $r;
}
public function getId($id, $dir='', $ppl=null) {
is_object($ppl) or $ppl = $this->pipeline($ppl);
$this->ppl = $ppl;
$ppl->out = null;
$ppl->meta = $this->meta('getId', $dir);
$ctx = stream_context_create($c = ['http' => [
'header' => "Content-Type: text/xml\n$this->hAuth"
, 'method' => 'SEARCH'
, 'content' => <<<data
<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:basicsearch>
<d:select>
<d:prop>{$ppl->cfg['ncFlds']}
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/files/$this->rootDir/</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:eq>
<d:prop>
<oc:fileid/>
</d:prop>
<d:literal>$id</d:literal>
</d:eq>
</d:where>
</d:basicsearch>
</d:searchrequest>
data ]]);
return $ppl->request($this->sUri, $ctx);
}
public function ff2xml($dir='') {
/* find files and directories using Propfind Request of WebDav from Nextcloud using http
on host $hst in directory $dir
return the generated xml
*/
$this->meta = $this->meta('ff', $dir);
$ctx = stream_context_create(['http' => [
'header' => "Content-Type: application/xml\n$this->hAuth"
. "\n" . 'Depth: infinity'
, 'method' => 'PROPFIND'
, 'content' => <<<data
<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<d:displayname/>
<oc:fileid/>
<d:resourcetype/>
<d:getcontenttype/>
<d:getlastmodified/>
<!-- oc:permissions -->
<oc:size />
<oc:owner-id />
</d:prop>
</d:propfind>
data ]]);
$r = file_get_contents("$this->fUri$dir", 0, $ctx);
// echo "ff response\n" . print_r($http_response_header, true). "\n";
if ($r == '' or $http_response_header[0] != 'HTTP/1.1 207 Multi-Status')
err("https://$hst/remote.php/dav/files/$dir/ returned $http_response_header", $http_response_header);
/* echo "\nff result\n" . preg_replace_callback('=(<d:getlastmodified>)([^<>]*)(</d:getlastmodified>)='
, fn ($m) => $m[1] . $m[2] . ' => ' . toLocalTst($m[2]) . $m[3]
, xmlpp($r)) . "\n"; */
return $r;
}
public function ff2Node($s, $hr) {
/* parse the xml in $s returned from ff2xml,
for each resource file/directory call $hr with an array with 7 columns
['filename', 'id', 'type', 'path', 'lastmod', 'owner', 'size'] = meta[2]// header row
*/
$this->ffFo = $this->ffFi = $this->ffSz = 0;
$hes = function ($hh, $nm, $at) use(&$pSt, &$p1, &$pPropSt) { // handler element start
$pSt[] = $nm;
if ($nm === 'd:response')
$p1 = ['', '', '', '', '', ''];
elseif ($nm === 'd:propstat')
$pPropSt ++;
// echo "ps ele start $nm, stack " . implode(',', $pSt) . "\n";
};
$hch = function ($hh, $ch) use (&$pCh) { // handler character
$pCh .= $ch;
// echo "pc char $ch!\n";
};
$hee = function ($hh, $nm) use (&$pSt, &$p1, &$pPropSt, &$pCh, $hr) { // handler element end
$e = array_pop($pSt);
if ($nm !== $e)
err("bad pop $nm, but popped $e");
// echo "pe ele end $nm pCh $pCh\n";
if ($e === 'd:response') {
if ($p1[2] === 'd:collection') {
$this->ffFo++;
} else {
$this->ffFi++;
$this->ffSz += $p1[6];
}
$hr($p1);
$p1 = [];
$pPropSt = 0;
} elseif ($e === 'd:prop' or $e === 'd:propserror_get_last()tat' or $e === 'd:resourcetype' or $e === 'd:multistatus') {
} elseif ($e === 'd:status') {
if ($pPropSt === 1 ? $pCh !== 'HTTP/1.1 200 OK' : $pCh !== 'HTTP/1.1 404 Not Found')
err("d:propstat #$pPropSt status $pCh");
} elseif ($pPropSt <= 1 ) {
if ($e === 'd:collection') {
$p1[2] = $e;
} elseif ($e === 'd:href' ) {
$pa = rawurldecode($pCh);
if ('' === ($p1[3] = str_starts_with($pa, $this->fPath) ? substr($pa, strlen($this->fPath)) : "???$pa"))
$p1[3] = './';
} elseif ($e === 'd:getlastmodified' ) {
$p1[4] = toLocalTst($pCh);
} else {
$p1[$e === 'd:displayname' ? 0 : ($e === 'oc:fileid' ? 1 : ($e === 'd:getcontenttype' ? 2
: ($e === 'oc:owner-id' ? 5 : ($e === 'oc:size' ? 6 : err("ff2node bad xml ele $e")))))
] = $pCh;
}
}
$pCh = '';
};
$pSt = [];
$pPropSt = 0;
$pCh = '';
$xp = xml_parser_create();
xml_parser_set_option($xp, XML_OPTION_CASE_FOLDING, 0);
xml_set_element_handler($xp, $hes, $hee);
xml_set_character_data_handler($xp, $hch);
xml_parse($xp, $s, true);
}
public function ff($dir='', $hr=null, $hMeta=null) {
$xml = $this->ff2xml($dir);
if ($hMeta)
$hMeta($this->meta);
$this->ff2node($xml, $hr);
}
public function ffCurl($dir='') {
/* find files and directories using Propfind Request of WebDav from Nextcloud using libcurl
on host $hst in directory $dir
return the generated xml
*/
#curl -u wa@wlkl.ch:RCisXp9HmX6Pk2flSqmWkz9w5LnOiB0xF8H0dVxIxXfefW3OFCmdCvx21H4AUNH7gQxf1188 -X GET 'https://tsueri.cloud/ocs/v1.php/cloud/users/wa@wlkl.ch' -H "OCS-
# $r = ffCurl("https://$host/remote.php/dav", $us, $pw, "$dir");
$ch = curl_init();
curl_setopt_array($ch,
[ CURLOPT_URL => "$this->fUri$dir" // => "https://cloud.hoststar.ch/remote.php/dav/files/wlkl.ch/bike"
, CURLOPT_USERNAME => $this->user
, CURLOPT_PASSWORD => $this->pw
, CURLOPT_RETURNTRANSFER => 1
, CURLOPT_VERBOSE => 1
, CURLOPT_CUSTOMREQUEST => 'PROPFIND'
, CURLOPT_HTTPHEADER => ['Depth: 99']
, CURLOPT_POSTFIELDS => <<<data
<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<oc:fileid />
<d:resourcetype/>
<d:getcontenttype/>
<d:getlastmodified/>
<oc:owner-id />
<!--oc:permissions -->
<oc:size />
</d:prop>
</d:propfind>
data
]);
$r = curl_exec($ch);
curl_close($ch);
return $r;
}
public function mv($fP, $tP) {
/* move file/directory $fr to new path/name $to
*/
$fr =implode('/', array_map('rawurlencode', is_array($fP) ? $fP : explode('/', $fP)));
$to =implode('/', array_map('rawurlencode', is_array($tP) ? $tP :explode('/', $tP)));
$ctx = stream_context_create(['http' => [
'header' => "$this->hAuth\nDestination: $this->fUri$to\nOverwrite: F"
, 'method' => 'MOVE'
]]);
$r = file_get_contents("$this->fUri$fr", 0, $ctx);
if ($http_response_header[0] !== 'HTTP/1.1 201 Created')
err("mv $fr to $to failed: http_response_header", $http_response_header);
}
public function upload($fn, $ty='text/csv', $cnt='') {
// upload to host $hst a file named $fn and contents $cnt, if the file already existst, create new version of the same id
$ctx = stream_context_create(['http' => [
'header' => "Content-Type: $ty\n$this->hAuth"
, 'method' => 'PUT'
, 'content' => $cnt
]]);
$r = file_get_contents("$this->fUri$fn", 0, $ctx);
echo "upload sent " .strlen($cnt) ." chars to $this->cloudId $this->host $fn, response $http_response_header[0]\n";
if ($http_response_header[0] !== "HTTP/1.1 201 Created" and $http_response_header[0] !== "HTTP/1.1 204 No Content")
err("upload $fn failed, response:>n ", $http_response_header);
}
public function byId($id, $dir='') {
$ctx = stream_context_create($c = ['http' => [
'header' => "Content-Type: text/xml\n$this->hAuth"
, 'method' => 'SEARCH'
, 'content' => <<<data
<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:basicsearch>
<d:select>
<d:prop>
<oc:fileid/>
<d:displayname/>
<d:getcontenttype/>
<d:getetag/>
<oc:size/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/files/$this->rootDir/</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:eq>
<d:prop>
<oc:fileid/>
</d:prop>
<d:literal>$id</d:literal>
</d:eq>
</d:where>
</d:basicsearch>
</d:searchrequest>
data ]]);
$r = file_get_contents("$this->sUri", 0, $ctx);
echo "search $this->sUri response $http_response_header[0]\n";
var_dump($http_response_header);
return $r;
}
public function rename($r, $f2v, $hi) {
/* check all resources in xmmResult $r, if they need to be renamed
$f2v: callback: rename last level of path
$hi($nx, $n, $fP, $fM, $tP): callback if last level should be rename:
$nx=count, $n array for resource, $fP from path old, $fM from path for mv, $tp to path for mv
*/
$nx = -1;
$fP = $tP = [];
$hR = function ($n) use (&$nx, &$fP, &$tP, $f2v, $hi) {
$nx++;
$aS = $n[0];
if (($fo = $n[2] === 'd:collection') !== str_ends_with($aS, '/'))
err("path $aS mismatches type $n[2]");
if ($aS === './')
return;
$aP = explode('/', $fo ? substr($aS, 0, -1) : $aS);
$aV = $f2v($aL = $aP[$aY = count($aP) - 1]);
if ($aL === $aV)
return;
$y = min($aY, count($fP));
for ($x=0; $x < $y and $aP[$x] === $fP[$x]; $x++) ; # index of first difference
out("a) $x $aY fP", $fP, ", tP", $tP);
for ( ; $x < $aY ; $x++) # copy new levels
$fP[$x] = $tP[$x] = $aP[$x];
out("b) $x $aY fP", $fP, ", tP", $tP);
while (array_key_last($fP) > $aY)
array_pop($fP);
while (array_key_last($tP) > $aY)
array_pop($tP);
$fM = $tP;
$fP[$aY] = $fM[$aY] = $aL;
$tP[$aY] = $aV;
$hi($nx, $n, $fP, $fM, $tP);
};
$this->ffRsrc($r, $hR);
echo "$nx nodes in $this->cloudId\n";
}
} # end class NextCloud
NextCloud::ini();
/*----------------------------
curl -u "wa@wlkl.ch:ali{A51}sp" https://cloud.spzueri.ch/remote.php/dav/ -X SEARCH -H "content-Type: text/xml" --data '<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:basicsearch>
<d:select>
<d:prop>
<oc:fileid/>
<d:displayname/>
<d:getcontenttype/>
<d:getetag/>
<oc:size/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/files/walterkeller/</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:like>
<d:prop>
<d:getcontenttype/>
</d:prop>
<d:literal>text/%</d:literal>
</d:like>
</d:where>
<d:orderby/>
</d:basicsearch>
</d:searchrequest>'
----- */