[svn-genapp] r1703 - in genappalpha/languages/html5: . seedmelab

svn@...
 

Author: ehb
Date: Tue Jul 23 13:28:37 2019
New Revision: 1703

Log:
more seedmelab updates

Added:
genappalpha/languages/html5/seedmelab/
genappalpha/languages/html5/seedmelab/foldershare (contents, props changed)
genappalpha/languages/html5/seedmelab/syncfilesdirs.php
Modified:
genappalpha/languages/html5/base_footer.html
genappalpha/languages/html5/base_header.html

Modified: genappalpha/languages/html5/base_footer.html
==============================================================================
--- genappalpha/languages/html5/base_footer.html Tue Jul 23 12:55:56 2019 (r1702)
+++ genappalpha/languages/html5/base_footer.html Tue Jul 23 13:28:37 2019 (r1703)
@@ -225,7 +225,7 @@
$( "#logoff" ).html( "Logoff " + l );
$( "#files" ).show();
$( "#jobs" ).show();
- __~seedme2:url{$( "#seedme2" ).show();}
+ __~seedmelab:url{$( "#seedmelab" ).show();}
// $( "#register" ).empty();
// $( "#userconfig" ).html( "<img src=\"etc/config.png\" width=40px id=\"config\" class=\"config\">" );
ga.license.get();
@@ -237,7 +237,7 @@
}
$( "#login" ).html( "Login" );
$( "#logoff" ).empty();
- $( "#seedme2" ).hide();
+ $( "#seedmelab" ).hide();
$( "#sel_project" ).empty();
$( "#files" ).hide();
$( "#jobs" ).hide();

Modified: genappalpha/languages/html5/base_header.html
==============================================================================
--- genappalpha/languages/html5/base_header.html Tue Jul 23 12:55:56 2019 (r1702)
+++ genappalpha/languages/html5/base_header.html Tue Jul 23 13:28:37 2019 (r1703)
@@ -473,7 +473,7 @@
<td>
<table cellspacing="0" cellpadding="0">
<tr>
- <td><div class="help_link"><a href="__seedme2:url__"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAq1BMVEUAAAAgEBAuEBAvDg4vDw8tEBAsEBArEBAvEBAoEBAuDg7/ygD/zAD/ywD/ywD/zAD/zAD/ZDD/ZTP/ZjL/YDAAlzIAmTMAmTIAmDP/ZDL/xwAAlTAAmTMAmDL/ywD/ywAAlzL/zAD/ZTL/ZTP/vwAAmTMAmDIAlzD/YDD/zAD/ZjP/ZjL/ywAAmTIAlzD/ZTL/ygD/ZjL/ZTAAjzD/ZTL/zAD/ZjMAmTP///+MEjhdAAAANXRSTlMAAAAAAAAAAAAAADDvgM9Q30DPjyBgr++/cCAwz59Av4CvYL8QUN9AEJ9QgI9wIJ9g3zAQ74pufOMAAAABYktHRDigB6XWAAAACXBIWXMAAAsTAAALEwEAmpwYAAABR0lEQVQoz6WSXXuCMAyFj6VIFdwUYTI7FR1uZQw/V/n//2xNi44Lvdq54KF5SUJOCnQURiM80pPWzw/QSGs97gYml0s8Tezr2LA2MX2ZNRkxo1dznhPS1FG+NUYLwLLLFFhatjJoRqjJgdjBScvWgM1qGglsHHtH0dbcOvRBfdvEBCtiIRaOfRJTcafo6loydWOoqWModTQHiGTyNqT6qiqa8Ts0jzyvJf6lHvM49/xeJ9RnnHMWAIK0q/OcdrQugKSqNoqCHOYDmdGveSgiM4JvDd4rnwsE2Dr7BCvNfAfuzD+e4N2czYTdw1hwZ1OsTJPMWVSLg/UzHOwdrIBd6+xZjCxbInHsyJFaNJPD4Mqumzkht1myvS6WBZsfZ7BhWXq2N5BQ5DMuuKpiYmxw84P2Vwrh+XfsM4lRwXp3yJD3D1Eo2F/kFy2lQj6DgN8BAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wMi0wN1QxOTo1ODozNC0wNjowMKy3e5IAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTktMDItMDdUMTk6NTg6MzQtMDY6MDDd6sMuAAAAAElFTkSuQmCC" width=27px id="seedme2" class="config opacity"></a></div>__~help:files{<span class="help helpright" style="right:70px">__help:seedme2__</span>}</td>
+ <td><div class="help_link"><a href="__seedmelab:url__"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAq1BMVEUAAAAgEBAuEBAvDg4vDw8tEBAsEBArEBAvEBAoEBAuDg7/ygD/zAD/ywD/ywD/zAD/zAD/ZDD/ZTP/ZjL/YDAAlzIAmTMAmTIAmDP/ZDL/xwAAlTAAmTMAmDL/ywD/ywAAlzL/zAD/ZTL/ZTP/vwAAmTMAmDIAlzD/YDD/zAD/ZjP/ZjL/ywAAmTIAlzD/ZTL/ygD/ZjL/ZTAAjzD/ZTL/zAD/ZjMAmTP///+MEjhdAAAANXRSTlMAAAAAAAAAAAAAADDvgM9Q30DPjyBgr++/cCAwz59Av4CvYL8QUN9AEJ9QgI9wIJ9g3zAQ74pufOMAAAABYktHRDigB6XWAAAACXBIWXMAAAsTAAALEwEAmpwYAAABR0lEQVQoz6WSXXuCMAyFj6VIFdwUYTI7FR1uZQw/V/n//2xNi44Lvdq54KF5SUJOCnQURiM80pPWzw/QSGs97gYml0s8Tezr2LA2MX2ZNRkxo1dznhPS1FG+NUYLwLLLFFhatjJoRqjJgdjBScvWgM1qGglsHHtH0dbcOvRBfdvEBCtiIRaOfRJTcafo6loydWOoqWModTQHiGTyNqT6qiqa8Ts0jzyvJf6lHvM49/xeJ9RnnHMWAIK0q/OcdrQugKSqNoqCHOYDmdGveSgiM4JvDd4rnwsE2Dr7BCvNfAfuzD+e4N2czYTdw1hwZ1OsTJPMWVSLg/UzHOwdrIBd6+xZjCxbInHsyJFaNJPD4Mqumzkht1myvS6WBZsfZ7BhWXq2N5BQ5DMuuKpiYmxw84P2Vwrh+XfsM4lRwXp3yJD3D1Eo2F/kFy2lQj6DgN8BAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wMi0wN1QxOTo1ODozNC0wNjowMKy3e5IAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTktMDItMDdUMTk6NTg6MzQtMDY6MDDd6sMuAAAAAElFTkSuQmCC" width=27px id="seedmelab" class="config opacity"></a></div>__~help:files{<span class="help helpright" style="right:70px">__help:seedmelab__</span>}</td>
<td><div class="help_link"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKeUlEQVRYw+2YXYwk11XHf/dWdU/PTO/ujHfXjtfZ9XrBjhx/iEgQEiEriYkARQQlAV4AvwCKRARI8AQIJCSQeIAHhJBAkUFCwAtvBMl8JuEjgSASe+39/vR6d3a+e3pmuqu76t5zzuWhqnt6N3YcB3gAcaVS99TM6P7qnP/5n3ML/n/9H1/uW/3H3m6fBN7BIbN0IlTVE6L6dCJhaufM9JqIrKaUBjhnTz7+xP8cYKmCOecyswXv/ENmdkbNnjLTZ1X1var6aBQ5qiJtEUFUg4r2VOUNUb1oaq+Z2QU1vamqG9uba6PO/EJ6/kPPv3PAXlnSKwrOLC93nOO4S5zGuSeBZ4H3JjhjyY6b2YKqomqoKmqKiqCqiCgiQpSIREFUEJGRqW2Jys1kdlHNXjOzS2p2y0S2yhTLHM/3f+SjXw+YUgLwwNPA+xI84+Ap4NuAdwHd2YdREiklZgHNaqj6Z5lCqgpRpAaV5lJBVZOpDtVsXVVv4DjfXeyeHw4HZze3t84tzC/YJ3/wh8hnYOdTSr8NfJ9zb535BDgcOPA+m7mbyLIcECAjpcmDJyzVD5Sav4OEA2fOHfIpHcrz/HEz+4GyKimK4u+S8akQYgHcAyg4F1JKmFkdLudwrgG6L+yp/jXOObz3NUACSx5nCe8TWZaRSOQpQQPsnSNv5ZglYgiMxiOKYsS4HFNVFWZWSRQxU+4HjCmlnnMO71yzYULNpjTeuzp+boLcfPcOlxw+ObLkIaujlFIiI6uh8pwsZpTlmKIYMSyGlGVJDLHWa4xEiajoTlVVsd1uHwBaSoQYrJW3dg4iV18ejzZ6i0HQJrpZ5sl8hvMe7zz4RDLwPtXoTWRjgFFZsT8cMBwOKcfjWpPTgqo1GmIghkiU2Mu8tyjxADCbpAh696tPVBmXY5zPcA5IiahKWQpqinOOVp7TarXJ8gxIxBAZFkMGgwFFUVBVVVMcWheQHsCFGIkhEEIgSiRG6WXe83M/85l7U2x1JW8DyTVVYoCYoZbAhBjDNPUAZoaqURQjRIRWKyfGyGAwJMSATqBEp0CigsRInKQ1RkIVatAYk6psx5kATQFFBGDbO2dZnmeWEmK1v5kpWZbjs4wYI1VZkWW+3lQVM6ujEap6w1DNAOkUVGSit4OU3vMZxURijzcDbCLYx7uQzObVFFOtnzbW2ktmmBoikZSymU1rGOdcc69Jodb+Fxs/nOitfohACHH6PcaIiIQYY3/W5aaAmgwSezg3Ssi8imLJMKtTWo7HiCopJUII5K0WMcZppFQEnJuCyMSsVaagojIDFqiqQIy1/lSVGGUUJe7N+vAUMFnCku0nszHOISqU45JL589z7MEHeeDYURAlVKGOZqOfupvoRCL1RpNuITNRbGyk1lsNGZrikBjRZIQYRhJl3/k3AVRTVHXgcEVdAMrWxgZXLlwixsBcp41ZoiiGzHXmJymZtjMRBUcjgdkoHqT0Xs3VWg2ND5oZMcRRCGHfef/1gOPxmDzLyhDCnqqS5S26hw/zwee+h9x7Rnt9er1t+rt7nDrzOD7LZgAPNGimE8NFVKZQk2hN9DZJc4j1QGHJiDHuxhiqgxYK+fVbt/jiP3+ZTrv9nPdZe2Nrc7i2usLy4XkWu0dYOHyIqqrI3Dzzh5bY2h0yGo+Ym+tMIzSbYjObDgZR4gHgNLUHxTG9JEKCKLFviYpkB4ChqvjpF36MO6t3f3S+03nhwWNHWfA9BtvX2L5d4toPMtc9gW/Nk7XavPvkSXDUKZ4URKxhJkNElNhoSw7gQmispKnc5nMiAYAQYt97X6nOAoaK12/fpizLnRDjUp5ldLqn6HS6dPffoL+9ws7KdSw/Trv7CHm7i0adWkdsUlS3JkeW+Tpqk0iFMNVbbCwrxjC1m1omCiRUZUcFM5MDwO945lmuXL+OSNzJMk+MkcKMLOvSOfQMD3VP0e3fYO3OZfbX7kD7Xfi54yTfqX2ySaWKkIAsyyirijhtXfd6nkic6q5pbagqzkGMsp3nOX/6x39yb5GEGFC1nSiiyVJW6ygyGhmZb9FZfJpTT55GilV6m7fY6l2giIukfBllrm55IiTqEStUVZPmuhDqyIU6erORa7SazEgk+v3+3l//5edmJ7oasCxLkqUeJAGXmRmQGI3G4GAwgFarzcL8Yxx/9BGWjq2wtX6Djc0bjMs20R1BU5ssyw9sZaYw6om6Sa/Eaaq18Uo1A0j7u7sZcAIogBEgdQSrCmAXR3TOzZnWgGaG946oShUCg8E+Wd5iceEEx04+xNKxNbbXr7G+cZfewKGth3DZHFXjbxM7mUBPbCfOeGdd/QlVjb3t7TnqY8YAWAd28s//4xcYj0uAoXO+cI6uWd3iRCI+y0jJSClRVQEvQlEM8T5jcfEoy48cw7cv0ultsLqbqEKkqqoZC5FGjzNnksaaVI16cnbEEONOb8eA+SZ6bppiVQEYqspo0g2sGf1DjNPxP0rEm8dS3epG44Kez9jaNEwfIMhMr5UZG2kmmMmodc9pUIU8z0mk4Oqo3QY2myiK/94PPz9JwX5VVf0Y6002VtfY39sjpRqyHJcHU8dkXIqRqixZubvJV18+x/7+ftO+mul42jEmfllPPhMws3r4GI/GG/3ezh8URfF54CbQByKQcoC7K3cZjUb9B44+8OvdbveXVfT9Gyt3s0PLS+A93ntEhZRSbSnNgGCNoYYQ6O/scOTIEebnO/cY8P16E6nnx2acq4pi9A9rK3d/9+K5c18CyvsG+oPj2uHDS+zt9fmVX/vVh0+ePPWThxcXP/3uR06cUg1s93oURUF7oYtZQidn4GZAuHbtGlcuXeb0mTPML8zXh6EZE77nnGw1YFWWl/d2d3/v5vUbf7G2crf3Vkfd6diwv7+Lc47V1dW1P3rxxd96/c7tT169+fqfFaNquLjYpRhX7A+GVCFQVRWhCk21BkSaEasZpw5OajJjKc3kHePe/u7uZ++8cfuH//Wf/uUP11fXet/oHJ7df+PVV86ytrqavv09T6xdvXb1b8Xs0v5weFLNHs7z3EtTCJMIqRn9/g4bGxscPnyELMuoQqg9TmX2rYOORsVXtje3fun8q+d+3zm3ttff5e3WN3x59N0feD9f+bd/5zM//7MPP3zixE91Op1Pe5+dtMm7GFW899y5s8LFCxd47MwZ5jpzlGV5kF5TynG5tr+39+Ld23c+e/3q1ZXO/DxVWb4t3NsCTtZ3ffADXL92zf/4Cz/xvuWl5V/IW/knUkqLdQ/1rK+vcfnyZU49+ihzczWgqhJCrAaD/b/fWF//nde+9sqXFw91pRgMvymwt0zxm63VlRXK0Tg99cyza6ura3+TZdklNTulpidSSm48HjMaj1laWmIigWJYXN3c3PzNKxcv/cbJ06cv37px02II7wjumwacrLMvv0y73Yqf+JFPXTj7ytmXgIHDvWc4HPZ6W9tf6x46dLyqqtDr9f78jddv/eLZ//jqSyIyun7lyjsGe0cpfrP1sY9/nMsXLvoPffT579xYXz/z0uf+6tJzH/nwY847ffWVs19YWl4u3rhx81sG++9e7r/ysP+r138C8oUCZ97iqcAAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDUtMTJUMTE6MjA6NDctMDU6MDC342n8AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTA1LTEyVDExOjIwOjQ3LTA1OjAwxr7RQAAAAABJRU5ErkJggg==" width=40px id="files" class="config opacity"></div>__~help:files{<span class="help helpright" style="right:70px">__help:files__</span>}</td>
<td><div class="help_link"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAANWUlEQVRYw82Ye3Ad1X3Hv+ec3bu796mrt2Q9LCNbWLKwAHtAtoHYuCQQCNDwaJ2WQsIwuDxKIA1NS5uSwBgCeUBMAmkJhhYXYzAUx4AtwMbGxtiOYkuW5YdkvR/3oXuv7t27793TP2QTaGphCGH6ndmZnTm7Zz77O7/fOd/fAv/PRT7LS+91HAcAmTFaDwIRIOCcDwuMJvKGhaUt9V8c4MadB1EQCkAzzAAhxGOU6mWFQbiet8wnis9TAj8hhLqe9+TcmuV/v7V9AzTDFkWBKqmsliOE8OsvPvszA9LpBrd80A0CLruee1k0HFhXWhhaFQrIwaaZ5XBdfg5jtJwJLCwIQpBR2vzjX/+rIioymV1TcvOM0oJNMysKby4IKdH3Oo7j1R2dn28E2/YehmHZvmg4sCqgSLeIghAklBiO7ayxLFsNh/xXBmTfbEoICCGwHSedyKjrJVEYKSkIraSUlBumbady2paMqt9KCRm+4KxZnxpQONXARCqFVGqSlrQ0zmGMBTkASWByNCDfKokMosDACAElBBwA575o2C/f4nn8w88OB2SRMdKQSKV9iuL/TBE8JeC3b7kBo/3HjPVtuz+YO6fu8ohfgl8SIbCprHBcDzYHXNeDKFAwdvLi4B7gYQp0LJbsvOmmb8WaG+f+8Uv80tb94OCF4aDylaBf7h4ciaGyJPrEzMqSVtknACBwPA+O6wEAdNNCJqshGJBRWhACYwQCpSBk6gM45xiOp/uP9I+ubJ5d26fq5sJYOvceJaT/0vPmgpBP3kQ+fOKFt/bh+ovPxWs7D95TVhR5SJF8E57nmiFFqpZ9IuEAbNcFB8AIhSBQmJaDeCqLokgQRRE/CABKCRilIAAMy4FlO7Bdd7Q4HHA0067uHoj9qrN35M7K4oh19YXzTx/wxXfa4bhec3lR5NXCgtAsxghkxiAwAlAK1/NAyFSELMdFIpXl8WTG1DTNVhRZKC8plKrLojQSVEA/kpvZvA4CgkhQBudA/3hq8thQ/BuST9ikaia+/qWW08tBWZKgG+Y5fkWuFthUBFzPA6UM3PNwslpHExm+a1/H2I5t7xw83Ll/KJdOaEqoQD6jobF8yUXLmi656PzaxlmVTGAUjBKE/TISGRVcBfyyD5GgElEk36LlC+ZsuvfBn31iBNnJmznzz8eud9+OS7JcVlJSNI8RQikhoAIDIQSMEAyNp7x1L29s/+Uj97/Svv2NncnR/v25dOJQanzoeG9X+8Cene/2Z0yKyuraGdVlhYxRCkoILNtFTrfgui72d3a3vfnm64/UnbUocePVy+E6zqcqGvHS62++atPOjlT7sWF+ZDjBj4+n+EA8ww8cH+P/+OOnD4QLy74LYCmAagBBABIAP4AKAIupKN1283cf3Hp4IOY6rss10+YTWY0fHojzD7r6rZXfe+gOAPLpAgkA8OCa1yEKApOo4wsFQ6WKooQ8j4MxCkooQAh6+kcmN65/flM2FdsK4BAADQD/yFwagLRnm5MbX3jWv7j1/Pry4sVVlAD8xJZDKBWXX/JnM1rOPUdyqewQSpyVV10wLSBd/dJWjMXTrKqs8J7zz2luO7vxjB9KPlEApgqCUgLbcdHT09N7qP39XQAOA8j/L7iTMgD0xAaPftDR0XEwlc3DcT047tSjhBDU11beumB+09vVZYUPW5YdeHrjrukBTcOEyOCjBFeE/FKrX/GVuq4LQgDDdpDO5ZHXTcTjieOupR87ATedDAC942Nj3bm84WVUHfF0DqbtwHZcUEIiosDOJYRc7jpOwLGnz0HB0HVoeY1QQsnJ/ct2PHAAhmFjUtUQUCSIopgGkALgnUbqZGSfkHA9j2dUHbm8AUXywXZc2I4LEALHdWGZJhXZtH4FVDd05FTVs2wrZZgW9zwO1/Ng2w6KIgFUl0VRHAmi/oy6MAC3sGbeJ7BRAHDrG+aGfKLIwgEFRZHgSUMBy3Ghaoan6aZqux5M250+gm9vXI/uo8dMOzX4wEbudc0/d+GFZzY1LxIYhcAo/JICj3PU19XO+/Mbby95+Zmfp6Y7oh569nU8u3pVqKnxzHMknwDOAcYYDEuDYTk40NnetWv7O22MCZtM206KPml6wPe3vAIAfP2/HzwAYEyz7+wsr5nVxCiNWLYDSWQQGcPMGaVzVnxjxVWEkEcXf/kad+fml/5gsqqZs3HvDV9GeVhYWltV0QoAtuvBsh3kdRN53XR6enufe/mZx18EEAfwiZvgRxPA5JwPXXPtNfGq0qitWxYSGRXsRCUXhgNiS+PsO7bsOnDFcCxJf/DYMx+b6Jv3/ABDfUfxxo59rS3zzrzPr8hh2/XguC7yugHNsCBLordg4UJ1xW3/PHT/6rXaaeTy78/iX296H9lsvnXZeY3PlBSEGvZ1D8CyHSxqnoVwQJ46WzlHRtXHMqr2uJbX1g0Njwyvfny1fdsdfytUVlSWKH7lckmS7qaUnnnS9Vi2g9FEBpZtozAShmnZ6f6R+Pdln7BO8okKB9TLF8+feGbTe7jrumWnBnzk+beQSacvW9561rqWhprgaCKDvYcGUF0WxcLGWsiiCEoJwDnypu0aptXnuN4Bj2Occx7lQLPr8QbX4z7bdaf8ouMimc4hkc6hIByAX5EwMaly0zAnzqgqHWeUBlzP2z4cS93q84nGlUuaT73Ee/fuw/oX1u7Y19XzWiqbR0VxBMUFQd47nEBn7yhcz/3QoYgCZbLkq/eJwtddj9/GQVZ4HM2u530I57guJjIqxpOZqWQXGDTDwuBI7EhByD/uV6R54aBSFwn6L5J9rO7KJc14Y/eheS+07WsGWvDo820fjyAApDjHym+vOn/JwpZHbTWpH+kb1eqbWpYF/EpwTk0ZWuZUISBLsF0XjuMhrWqIpXJQJB+CfglTOTe1rPFUFmOJNAzTdimlXnVFsahqBhi8o0taZheF/HIRm7Jx3qSq7zAs52AkKF8xFEtn9h0eXBH2y13vdR75OGDxrAUw1aQIkNlqcijMPZ574Jdr75rf3Hjz4PgEwkEFDTXlKC8OwycI0C0bsVQOAdkHURRgWg4yqoaxeBrpnIagX0ZeN/IVJVFfJBwUSyN+lEeD8DigWw4c10VROACfQOFxDoFS5A0L7UeGXtl98PjfFIYDuY/1JMnj+wDAxtR5SwHQry49j8yZWYmjgzF09Izgg4PHEVQkRCMBhP0yKKPIawZU3cSkqkPVDQiMobQoglBQgWZY/oAik5DiQ0iRkMmbyGomBkfG1clsVp9TVx2eWzdDYpQgp5vgAIKKL+japuDY4imbJu/G+37hDQ4NRbv7xhqikRDqq0pRVBDEm9vbk31DoySjRgp8osg8j4MQAs/zeGYyOyky6PPnNZTJko/mNAP9Q+PZSECaLG2qr9EtB7pp4/UtWw+ueWr1u/GRgey8BYuqvvOde5Z/6byWCsNyMDyeTDz7H2sfe+Kh+9KhgqLfG9Y/LB8RXe9vdhKqNzqczBa5YDPi8cTYkz//6art72xZt7i1tbj13KZZ5cUFqCiJIpfLHl3z5Op/2b1tc1t13ZyZhsNLftfRveftLW0PR8NBtaa6aqEsiegbHEk+fP99a4517G4ztFzHYM+hYd0ThQULFzZRxlhyUs1nVG3jy5t39C299ErvlG3n/m2vAoDTtvZnbe+31Xbvuegry7XJCXdP24bNACbyuj43r5sXT52xLiaz+d/u3/6bDQDYmqdWq1X1TVVqZqL9zr+7/cKqyvKvOq4H07Jhamp8bKDnXQB7AJgACuLx8dJkJne17fGIJEmllyy78Kmh8dSGfV293z8l4EfkqImBgW0vPfUcploEk3PuXXbDXdt6+ob/i3NPAqFuIh57BYAKwOveu/WN7r1b7VVrNl1SVVnxT0UFQf/QeAo1ZVHMnlUbuO6mlfnb7r1faz0jCkvPTVx73XW8qCAsG7YLzbAQCigVpu0sM3TzkdMBBKbMqfXh7j5lFnYD6DoB7Z2AO2lN9O89sQEjY4nerFqbrKssruGcIzWZx+zq8pp77779dksdfmB4aGAsls7NZ4LvW5wyKZnIgDEG3bSRTGcPb968OXa6gP+XLAATpxrsPXIYu397YKCpvmZbcUj6WjqV4uPJTDQUkEhZcfSvTMteYtpOPBKJ1Km6VTY0PoGBkbiVSsR6GWNaIqO91rVljfGZ/g9+Gl1w+YqGdGx4kRSMln9z5Z13KYpSWllSgKJIEKbjwHE9jCYyyOUNjAwPbX/4H27/iW3kE1Ig1CmFCnPsj0eYXoNHOzPx0YHDX7vur3N/ceUl1xaEA+G+0SSODcbQNxLHRCYPgVFUlBYi4Pf3dHUf+0l88HCfY2qGkZ2Y/v/g5yT3py9s1Qpn1AeHYulAYSSI85rrQax8x1u/efXfJOoOVFeUIJNV3Z7+YbO2odn7y7t/ZONEU/YnjyAAIFyDPTvemjw+OD4xkdVKJtKT9n+//OKP2l56+jnui2Qncoaybeu7/7ll44bH9m1eO3y083fctU7LLn7ukotqGpvqzl66XJCCJQAoZb5iwR+tAyEh4AtZ0U8UOQHyJy/QL0T/A0HAh2vjAADMAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTA1LTEyVDExOjIwOjQ3LTA1OjAwt+Np/AAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNS0wNS0xMlQxMToyMDo0Ny0wNTowMMa+0UAAAAAASUVORK5CYII=" width=40px id="jobs" class="config opacity"></div>__~help:jobs{<span class="help helpright" style="right:40px">__help:jobs__</span>}</td>
<td><div id="userconfig" class="help_link"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAJVklEQVRYw7WYa4ycVRnH/+f2Xmbe2ZmdndnuBcpuYSndLBAEQUzAVAxqo1wFP4AkCDURMZpgSPyGF4wfSBQv4SpIBQpNETEk3o0KjdiIFtqlcmuF3dJ2rzM7897PxQ9z6XS7QKeyJ3kyk3fOnOc3/3Oe5znPEJzguGbzN7Ht/u/hvp9vG87nsp+xLb5Jaz2RpDIjla4YjZ3GmKdrfvjH3oJX/82f/4nH7v12137YicDd9eOHsTBXoUkSfnrtUP9PBsq9N+VzmXHH5r0AvCiRJc7Z2X0Fb1Ox4I2lUu2+8NwzFmJ3CJMvPre6gHff9xi+fP0mEEovHz158P7hof4Nfb0FlslmQCmB0RpBGAMAclnHLvXmz+KCnzl9cP4vZ64fqW7f9nhX/mi3gOViD5589m/rhgfL3xocKA+USiWsGRhALpcD5xwAoI2B1gZSKfhhBC/jbFw7VP76czt38589un31AB/Ysh2XX/oRlHrz1/Tmc2dRSkApRRAEqNVqCMMQNT9EkipIpRFGKdI0BQBkM861F314YqInm1k9QM4o7n/8t57riEuN0ajXfczOzuLQoUOYm5vHzFwFC1UfWjcUDKIEcSxR9wNorYZc1964ppTH1qeePX6f3QAKztBXyPUbY9ZVq0vQxoAxhjhOUK0FqNZDSKnb85XS8MMYBgYwAKPk7FfffIeW+3r08frsCpAQAsZILknSbBDGCMIEIICUGlIpGNOaB3DOYAkGwRmMAeIkBWesf+TksjDGxKsCaExDiyiRJk0Vwjg9Zg4lBJbFYFscthAQgoEzBsYIKKGEUALT+iUfNKDSBjCoJImsKq37V1LYshhcW8C2xRFAzsAoBSFkEYDqxmdXgFobGGPmU6mmATO2/HPBKRxLwLEtOLaAZXEIzsE5B20A7qeUSinl6gACQJSYgFH9ugE2dj5njDS2tcMsywI/AqgIIZMAMDExsXqAWZcaY7DHdSwAQJJIKK3BWSMghOBNExBCgPPGe8bYgjFmb7f+usqDN153JWyLw7HFy64tasYYcE7b0cp5E5KztnItUCHEtBBiSgjRFWDXCiptQAn+E8bJG7092XMIAfwwBiEEnFEwRsEYa1vHFu82xlRWVUEACMIE60876bCU+gnHsXShJ4tCLoOMY4NRilb5axljDJTSOiFkO6VUuq67uoCbb/gcdu3Zj5ofPVKrh89rbZqqERBCQEDacwlpPAPwDCHkTwBQLBZXFxBobHM+5x5eqNS3zFdqupF+AIOGHTVXqUNxnPywXveDwcHBrn2dEODmG65GFEtIpV+bW6z7i0s+UqmgtYbRGrptBvvfPrj/D3/d+cbfX5w8EVfdB0nnIITUtDZJ3Y9BCYVtcUitYQzAhcD0wVm8NX24QgiN0vT4k/MHAsgoAQgcAIwQAkoJPC+LbMYF5xyHZhZx8PAcOKcQnAIdZ7MrEbr9woNbnkI9iGm56PV7GftrhJDbGaOUUgJOGRhn0FpDSgljAG3MvihOb6nUwhcOHl6qFnpc3Lr58x8s4M9+8UtQSqC1yTJGzrctfkXGsS7JZpzTHFvYrfTSjNj2MMYgTlJUloJqECa7k1Q+q7T5dZLK1yilavMNV/9/gD998EncctO1eOjRp/sYI5cQQq4nhFxsC5Z3HQsZx4Lnuci4LhhjxywcJwkq1Rr8MEaSSKRSGaXN20rpXymlt8ap2sUYiW/+wtXdAd75g4ewfnQAC5V6n2NbV1BKbiSEnEsIcSglYJQ2LwQCji3geRnkPA9CCBBCoLVGEISoVqsIowSpbPQoWmsYY6AaDdWslPr3qVQPxYncQSmNV1L0GMB7H96GwXIe1Xp4TrGQ+36hJ7uRAGKpHiCMElDSSMBH110GIQRc14FtWYiiCH4QQkoFpTW00u1Or5V+lNZQSkNqXUkS+WAUy+9SSqpfvP6q9wbc8sQzkFIODK0pbls/NnpRuVTCwsICZmZmMb9YhTFolDNCmqWso7R1rNZI3M0EbkxbOd0EU0q3b9ZxImUQJbf1ZJ0fzVV83Hjdle11jkozDz/2NHJZG0nKPloq5i8o9hba5YoyCtsSTZhmWSMAAQFpAi8PklagtF4bkE3lmoDGAIwxro255I23Z+8r9GSO6leOyYOuLRDF6XiaSmthYRGEEMRxDEsIFItNYOBdYZbb8ufcmGYaUtAtaKURRmRtxrV6jDGz7wFo8KnP3oGtj96+tu4HmJmdA6MExgCEkmbzw0AbNxRQeqRSdgK3YFpB0XrtfM8Ya5dExTQswQc4o/0geHdAzhjuuedW17L4WtpUjjGGcqkEz8u2+oq2o8Z5UjDGrAjYOa/TWs+VUo1zbAwc28oLwU5qtQUrAlJKkHEsz7b4Gsvi4JyBgIDzRn+x0nZqrd/37LVglFKQUkKpxsWic65tW64l+Kmuc/SNmy4HFIIVLcHLluDgzeRrjD7qEtoCeq/+th1czUurZVlwHAfZbBae58F1XSSpbAVJoxO0xGmXbfoEHnjkyB9MbQUv/tjHYQmBKI6HLMHzShtESQyZKnhRjHzHOVoeBO83OtVsCNHQZWnJhzYajm3By2Zg2WLs9jvudoYHStGKCl512ScRheG40iZbq4dQUgMEiMIQSZK0t6e1RSsBL7fOeZ1HIwwjaNP4rFrzMb+4BAqczpEOhqH/blEMTE1PHcp5mblCPldmjEIbA6kk6vU6MpnMiufteBVsjTRNEYYBGKUwMOCaIQgj1Or+TBDUJRdHsNoV/pSRUWx9Yjte3vXvAyB00bKsYduyCnU/oFEUk5yXbTVAx5y1bgC11vB9H0mS4L9vTRnfD1I/COYPvHPwd5OTk9+5+647J2UcYWrqrcb6yxfbsGECs7Mz1jnnnrdhZHTdhVEUrqWEjl9w/nkfGh/f0F8ul2wv64Gx7rsFKSVqtbo5PDMTvvb6m/t3PL/jH1GSvqSV3HvgwPRLO1/YMUspN1ofuX2/388nY2OnO1mvpzebza4bO339+OjIKWeMjo6cOjw8NFAoFAqZjOtxxhzGuEUpoY0/3wADo7XWqVYqTNLUr1aX5mdmZg+8uW/fvr2v7J18Ze/ef9WWqlNpHNVu/tJX5Tdu+8rKAF2KQABYa0dO7RkdGeldf8Z47qThNfl8b1++t1jK9HgZW3DGCCEqkTqu1X1/aXG2Mje/WNm1a1d1z5491ddffaUOIDleh/8DKSEiy75FzW0AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDUtMTJUMTE6MjA6NDctMDU6MDC342n8AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTA1LTEyVDExOjIwOjQ3LTA1OjAwxr7RQAAAAABJRU5ErkJggg==" width=40px id="config" class="config opacity"></div>__~help:user_config{<span class="help helpright" style="right:10px">__help:user_config__</span>}</td>

Added: genappalpha/languages/html5/seedmelab/foldershare
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ genappalpha/languages/html5/seedmelab/foldershare Tue Jul 23 13:28:37 2019 (r1703)
@@ -0,0 +1,9938 @@
+#!/usr/local/php/7.2.10/bin/php
+<?php
+/**
+ * @file
+ * The FolderShare web services (REST) client application.
+ *
+ * DO NOT EDIT THIS FILE!
+ *
+ * This file has been built automatically by concatenating a set of
+ * separate source files in order to create a single stand-alone
+ * application. If you need to modify this application, you should
+ * edit the source files instead, then re-concatenate them into a
+ * new application.
+ *
+ * This file contains the following source files:
+ * src/FolderShareConnect.php
+ * src/FolderShareFormat.php
+ * src/Main.php
+ *
+ * Built on: Thu Mar 28 16:58:00 PDT 2019
+ */
+
+
+/**
+ * Manages communications with a web site using the FolderShare Drupal module.
+ *
+ * This class handles client-server communications for client applications
+ * communicating with a remote server running the Drupal content management
+ * system and the FolderShare module. Drupal manages the content and provides
+ * a general-purpose web services (i.e. "REST") interface. FolderShare
+ * supports that interface in its management of a hierarchy of files and
+ * folders stored on the server.
+ *
+ * This class's methods configure the client-server communications path,
+ * then use that path to send requests to the server and handle responses
+ * back from the server. The requests supported here include:
+ *
+ * - Create folders.
+ * - Upload files and folders.
+ * - Download files and folders.
+ * - Get lists of files and folders.
+ * - Search files and folders.
+ * - Get file and folder names and other descriptive values.
+ * - Rename and edit file and folder description values.
+ * - Copy and move files and folders.
+ * - Delete files and folders.
+ * - Get FolderShare settings.
+ * - Get web site settings related to these services.
+ *
+ * <B>Web server setup</B><BR>
+ * In order for this class to communicate with a web server, the following
+ * must have been configured by the site administrator on that server:
+ *
+ * - The site must be running the Drupal content management system.
+ *
+ * - The FolderShare module for Drupal must be installed.
+ *
+ * - The Drupal REST services module must be installed.
+ *
+ * - The FolderShare resource for REST must be enabled and configured to
+ * enable:
+ * - GET, POST, PATCH, and DELETE methods.
+ * - The JSON serialization format.
+ * - An authentication service (such as basic authentication).
+ *
+ * Applications may use this class to open a connection to a web server,
+ * authenticate the user across that connection, then issue
+ * one or more requests on the connection. The connection is automatically
+ * closed when the connection object is deleted.
+ *
+ * <B>Return formats</B><BR>
+ * Many requests can return information to the client in one of these formats:
+ *
+ * - 'full' returns a complex serialized entity or list of entities as an
+ * associative array of fields that each containing an array of values,
+ * which may in turn include on or more named values that may be scalars
+ * or further arrays. This structure matches the layout of data on the
+ * server and it is suitable for re-use in a future call to create or
+ * update a file or folder. However, it is not very user-friendly.
+ *
+ * - 'keyvalue' returns a simply serialized entity as an associative array
+ * of field names that each contain either a scalar value or an array of
+ * scalar values. The key-value array is easier to process by client
+ * code and it can be converted to a user-friendly output.
+ */
+class FolderShareConnect {
+
+ /*--------------------------------------------------------------------
+ *
+ * Version.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * The version number of this class.
+ *
+ * @var string
+ */
+ const VERSION = '0.6.3 (March 2019)';
+
+ /**
+ * The minimum supported version number of the server's FolderShare module.
+ *
+ * @var string
+ */
+ const MINIMUM_FOLDERSHARE_VERSION = '0.6.3';
+
+ /*--------------------------------------------------------------------
+ *
+ * Authentication constants.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * A connection authentication type that does no authentication.
+ *
+ * This value is a default until a specific authentication style has
+ * been set up. However, most web sites will not allow connections
+ * without authentication.
+ *
+ * @var string
+ * @see ::getAuthenticationType()
+ * @see ::getAuthenticationTypeName()
+ * @see ::login()
+ */
+ const AUTHENTICATION_NONE = '';
+
+ /**
+ * A basic authentication type that uses a user name and password.
+ *
+ * Drupal core supports basic authentication, and it is this style that
+ * is mostly widely used for web services. With basic authentication,
+ * the caller of this class provides a user name and password that are
+ * included on every request to the server.
+ *
+ * Basic authentication can fail if the user name and password
+ * are invalid.
+ *
+ * When a connection uses SSL encryption, the user name and password
+ * are included in the encrypted content so they cannot be seen by
+ * intermediate parties.
+ *
+ * @var string
+ * @see ::getAuthenticationType()
+ * @see ::getAuthenticationTypeName()
+ * @see ::getPassword()
+ * @see ::getUserName()
+ * @see ::login()
+ */
+ const AUTHENTICATION_BASIC = 'basic_auth';
+
+ /**
+ * A cookie authentication type that uses a session cookie.
+ *
+ * Drupal core supports cookie authentication, and it is this style
+ * that is used by web browsers. With cookie authentication, the
+ * caller of this class provides a user name and password that are
+ * used for an initial connection to the server. The server returns
+ * a session cookie and that cookie is used for further requests.
+ *
+ * Cookie authentication can fail if the session cookie cannot be
+ * obtained. This can happen if the server has been configured to use a
+ * non-standard user login form, such as one that requires a CAPTCHA.
+ *
+ * Cookie authentication can also fail if the user name and password
+ * are invalid.
+ *
+ * When a connection uses SSL encryption, the user name and password
+ * are included in the encrypted content so they cannot be seen by
+ * intermediate parties.
+ *
+ * @var string
+ * @see ::getAuthenticationType()
+ * @see ::getAuthenticationTypeName()
+ * @see ::getPassword()
+ * @see ::getUserName()
+ * @see ::login()
+ */
+ const AUTHENTICATION_COOKIE = 'cookie';
+
+ /**
+ * A API-key based authentication type that uses a user's API key.
+ *
+ * This requires external key_auth module support and custom code
+ * for masquerade user.
+ *
+ * @see ::getAuthenticationType()
+ * @see ::getAuthenticationTypeName()
+ * @see ::verifyServer()
+ * @see ::login()
+ */
+ const AUTHENTICATION_APIKEY = 'key_auth';
+
+ /**
+ * The preference order for authentication types.
+ *
+ * When a server supports multiple authentication types, this order
+ * specifies a preference. An authentication type earlier in the list
+ * is preferred over one later in the list.
+ *
+ * @var array
+ * @see ::verifyServer()
+ * @see ::getAuthenticationType()
+ * @see ::getAuthenticationTypeName()
+ * @see ::getPassword()
+ * @see ::getUserName()
+ * @see ::login()
+ */
+ const AUTHENTICATION_PREFERENCE_ORDER = [
+ self::AUTHENTICATION_COOKIE,
+ self::AUTHENTICATION_APIKEY,
+ // TODO As of 2/1/2019, the basic_auth module does not appear to be
+ // working properly, or there is something wrong here in how we parse
+ // results.
+ self::AUTHENTICATION_BASIC,
+ ];
+
+ /**
+ * A flag for an unspecified serialization format.
+ *
+ * This flag is used when the serialization format is not yet known,
+ * prior to server verification.
+ *
+ * This is not a valid serialization format for actual use.
+ *
+ * @var string
+ * @see ::verifyServer()
+ * @see ::getSerializationFormat()
+ * @see ::getSerializationFormatName()
+ */
+ const SERIALIZATION_NONE = '';
+
+ /**
+ * The JSON serialization format.
+ *
+ * When the server connection is first verified, a list of supported
+ * serialization formats is queried from the server. One of those is
+ * selected for all further communications.
+ *
+ * The JSON format serializes entities as JSON objects and arrays.
+ *
+ * @var string
+ * @see ::verifyServer()
+ * @see ::getSerializationFormat()
+ * @see ::getSerializationFormatName()
+ */
+ const SERIALIZATION_JSON = 'json';
+
+ /**
+ * The JSON + HAL serialization format.
+ *
+ * When the server connection is first verified, a list of supported
+ * serialization formats is queried from the server. One of those is
+ * selected for all further communications.
+ *
+ * The JSONHAL format serializes entities as JSON objects and arrays,
+ * and may append linkage information.
+ *
+ * @see ::verifyServer()
+ * @see ::getSerializationFormat()
+ * @see ::getSerializationFormatName()
+ */
+ const SERIALIZATION_JSONHAL = 'hal_json';
+
+ /**
+ * The preference order for serialization formats.
+ *
+ * When a server supports multiple serialization formats, this order
+ * specifies a preference. A format earlier in the list is preferred over
+ * one later in the list.
+ *
+ * @var array
+ * @see ::verifyServer()
+ * @see ::getSerializationFormat()
+ * @see ::getSerializationFormatName()
+ */
+ const SERIALIZATION_PREFERENCE_ORDER = [
+ self::SERIALIZATION_JSON,
+ self::SERIALIZATION_JSONHAL,
+ ];
+
+ /*--------------------------------------------------------------------
+ *
+ * Login state constants.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * An authentication status indicating the user is not logged in.
+ *
+ * This is the state for the connection before a login is performed,
+ * or when there is no user name and all accesses will be anonymous.
+ *
+ * @var int
+ * @see ::login()
+ */
+ const STATUS_NOT_LOGGED_IN = 0;
+
+ /**
+ * An authentication status indicating user login failed.
+ *
+ * This state indicates that login credentials were set and tried, but
+ * failed.
+ *
+ * @var int
+ * @see ::login()
+ */
+ const STATUS_FAILED = (-1);
+
+ /**
+ * An authentication status indicating the user is logged in.
+ *
+ * This state indicates that authentication was performed successfully.
+ *
+ * @var int
+ * @see ::login()
+ */
+ const STATUS_LOGGED_IN = 1;
+
+ /*--------------------------------------------------------------------
+ *
+ * URL constants.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * The URL path for requesting a CSRF token.
+ *
+ * The path only contains the string following the scheme, domain, and
+ * optional port on a URL (e.g. for "http://example:80/stuff", only
+ * include "stuff").
+ *
+ * This path is defined within Drupal core's REST services module and
+ * cannot be changed.
+ */
+ const URL_CSRF_TOKEN = '/session/token';
+
+ /**
+ * The URL path for logging in using cookie authentication.
+ *
+ * This path is defined within Drupal core's User module and cannot
+ * be changed.
+ */
+ const URL_USER_LOGIN = '/user/login';
+
+ /**
+ * The URL path for logging out using cookie authentication.
+ *
+ * This path is defined within Drupal core's User module and cannot
+ * be changed.
+ */
+ const URL_USER_LOGOUT = '/user/logout';
+
+ /**
+ * The URL path for an HTTP GET request.
+ *
+ * The path only contains the string following the scheme, domain, and
+ * optional port on a URL (e.g. for "http://example:80/stuff", only
+ * include "stuff"). An entity ID and format query will be added.
+ *
+ * This path is defined within the FolderShare resource for REST and
+ * cannot be changed.
+ */
+ const URL_GET = '/foldershare';
+
+ /**
+ * The URL path for an HTTP DELETE request.
+ *
+ * The path only contains the string following the scheme, domain, and
+ * optional port on a URL (e.g. for "http://example:80/stuff", only
+ * include "stuff"). There is no entity ID for this path.
+ *
+ * This path is defined within the FolderShare resource for REST and
+ * cannot be changed.
+ */
+ const URL_DELETE = '/entity/foldershare';
+
+ /**
+ * The URL path for an HTTP PATCH request.
+ *
+ * The path only contains the string following the scheme, domain, and
+ * optional port on a URL (e.g. for "http://example:80/stuff", only
+ * include "stuff"). An entity ID and format query will be added.
+ *
+ * This path is defined within the FolderShare resource for REST and
+ * cannot be changed.
+ */
+ const URL_PATCH = '/entity/foldershare';
+
+ /**
+ * The URL path for an HTTP POST request.
+ *
+ * The path only contains the string following the scheme, domain, and
+ * optional port on a URL (e.g. for "http://example:80/stuff", only
+ * include "stuff").
+ *
+ * This path is defined within the FolderShare resource for REST and
+ * cannot be changed.
+ */
+ const URL_POST = '/entity/foldershare';
+
+ /**
+ * The URL path for an HTTP POST request to upload a file or folder.
+ *
+ * The path leads to an upload form instead of to the REST resource
+ * because the REST module currently does not support base class
+ * features needed for file upload.
+ *
+ * This path is defined within the FolderShare module and cannot be
+ * changed.
+ */
+ const URL_UPLOAD = '/foldershare/upload';
+
+ /*--------------------------------------------------------------------
+ *
+ * Comment error messages.
+ *
+ *--------------------------------------------------------------------*/
+
+ const ERROR_EMPTY_REMOTE_NAME =
+ "Missing remote item name.";
+ const ERROR_EMPTY_REMOTE_FOLDER_NAME =
+ "Missing remote folder name.";
+ const ERROR_EMPTY_REMOTE_PATH =
+ "Empty remote file or folder path.";
+ const ERROR_EMPTY_LOCAL_PATH =
+ "Empty local file or folder path.";
+ const ERROR_EMPTY_REMOTE_DESTINATION_PATH =
+ "Empty remote destination file or folder path.";
+
+ /*--------------------------------------------------------------------
+ *
+ * Fields - flags.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * When TRUE, this class writes status messages to STDERR.
+ *
+ * @var string
+ * @see ::isVerbose()
+ * @see ::setVerbose()
+ */
+ protected $verbose;
+
+ /*--------------------------------------------------------------------
+ *
+ * Fields - user-provided parameters.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * The host name (and optional port) for the web site.
+ *
+ * The host name and port are used to build a URL to a Drupal site.
+ * During use, the URL will have Drupal routes added that lead to specific
+ * REST resources.
+ *
+ * @var string
+ * @see ::getHostName()
+ * @see ::setHostName()
+ */
+ protected $hostName;
+
+ /**
+ * The user name if using basic or cookie authentication.
+ *
+ * When using basic authentication, the user name and password are
+ * sent on every request to the server. When using cookie authentication,
+ * the user name and password are only sent on an initial login request,
+ * which returns a session cookie used for all further requests.
+ *
+ * If the user name is empty, access falls back to anonymous access.
+ * GET operations that query site configurations will always work
+ * anonymously. GET, POST, PATCH, and DELETE operations that apply to
+ * entities may fail depending upon how site permissions are set for
+ * anonymous access.
+ *
+ * @var string
+ * @see ::getUserName()
+ * @see ::setUserNameAndPassword()
+ */
+ protected $userName;
+
+ /**
+ * The user password if using basic or cookie authentication.
+ *
+ * When using basic authentication, the user name and password are
+ * sent on every request to the server. When using cookie authentication,
+ * the user name and password are only sent on an initial login request,
+ * which returns a session cookie used for all further requests.
+ *
+ * If the password is empty, the account has no password. This is rare
+ * except for the anonymous account.
+ *
+ * @var string
+ * @see ::getPassword()
+ * @see ::setUserNameAndPassword()
+ */
+ protected $password;
+
+ /**
+ * The API key if using API-key based authentication.
+ *
+ * When using API-key based authentication, a user is authenticated by
+ * provided API key server-side. if API key is found and on server then
+ * authenticate user and give corresponding priviledges to masquerade user.
+ *
+ * @var string
+ */
+ protected $apikey;
+
+ /**
+ * The masqueraded user if using API-key based authentication.
+ *
+ * When using API-key based authentication, a user is authenticated by
+ * provided API key server-side. if API key is found and on server then
+ * authenticate user and give corresponding priviledges to masquerade as
+ * user provided.
+ *
+ * @var string
+ */
+ protected $masquerade;
+
+ /**
+ * The current preferred format for content returned by the server.
+ *
+ * Some requests to the server return content, such as the list of fields
+ * in a FolderShare entity (a file or folder). The format of that
+ * returned content can vary depending upon the selected return format:
+ * - 'full' = returns a full detailed entity or list of entities.
+ * - 'keyvalue' = returns a simplified entity or list of entities.
+ *
+ * @var string
+ * @see ::setReturnFormat()
+ * @see ::getReturnFormat()
+ */
+ protected $format;
+
+ /*--------------------------------------------------------------------
+ *
+ * Fields - server characteristics.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * The CURL session handle after it has been initialized.
+ *
+ * The session handle is created when an object of this class is
+ * constructed, and released when the object is destroyed.
+ *
+ * The CURL session object is used for all communications with the server.
+ *
+ * @var resource
+ */
+ protected $curlSession;
+
+ /**
+ * A flag indicating that server communications has been validated.
+ *
+ * This flag is initialized to FALSE when an object of this class is
+ * constructed, and it is reset to FALSE any time the host name is
+ * changed. The flag is set to TRUE by verifyServer(), which issues
+ * intial communications to the server before the caller of this class
+ * makes their first request. If verification fails, no further server
+ * communications can be done. This may occur if the host name or port
+ * are bad.
+ *
+ * @var bool
+ * @see ::verifyServer()
+ */
+ protected $serverVerified;
+
+ /**
+ * The list of HTTP verbs supported by the remote site.
+ *
+ * The string array is changed from empty to a list of the names of
+ * supported HTTP verbs when the server connection is verified on the
+ * first request. HTTP verbs are always in upper case and are one of the
+ * following values:
+ * - GET.
+ * - DELETE.
+ * - PATCH.
+ * - POST.
+ *
+ * This list of supported HTTP verbs is checked on each call to this
+ * class before trying to send the request. If the verb is not supported,
+ * the request is aborted.
+ *
+ * Typically, servers support all of the above HTTP verbs.
+ *
+ * @var string[]
+ * @see ::verifyServer()
+ * @see ::getHttpVerbs()
+ */
+ protected $httpVerbs;
+
+ /**
+ * The list of serialization and authentication choices supported by the site.
+ *
+ * This associative array is indexed by HTTP verb (e.g. "GET", "POST", etc.).
+ * Each array entry has two children named "serializer-formats" and
+ * "authentication-providers". Each of these are an array of strings that
+ * name the specific serializers and authentication providers supported by
+ * the site.
+ *
+ * This list is initialized when the server connection is verified. It is
+ * to guide authentication and the serialization format used for
+ * communicating entities.
+ *
+ * @var array
+ * @see ::verifyServer()
+ * @see ::getServerConfiguration()
+ */
+ protected $httpConfiguration;
+
+ /*--------------------------------------------------------------------
+ *
+ * Fields - communications.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * The CSRF token, or NULL if empty.
+ *
+ * The "Cross-Site-Request-Forgery" (CSRF) token is retrieved from
+ * the server on the first request to the server. The token is then
+ * attached to all further non-safe requests to the same server
+ * (e.g. POST, PATCH, and DELETE, but not for GET).
+ *
+ * @var string
+ * @see ::clearCsrfToken()
+ * @see ::getCsrfToken()
+ */
+ protected $csrfToken;
+
+ /**
+ * The logout token when using cookies.
+ *
+ * @var string
+ */
+ protected $cookieLogoutToken;
+
+ /**
+ * The authentication type.
+ *
+ * This is one of AUTHENTICATION_NONE, AUTHENTICATION_BASIC, or
+ * AUTHENTICATION_COOKIE.
+ *
+ * @var int
+ * @see ::getUserName()
+ * @see ::getPassword()
+ * @see ::getAuthenticationType()
+ * @see ::setUserNameAndPassword()
+ */
+ protected $authenticationType;
+
+ /**
+ * Whether the user's credentials have been authenticated (logged in).
+ *
+ * @var int
+ * @see ::STATUS_NOT_LOGGED_IN
+ * @see ::STATUS_FAILED
+ * @see ::STATUS_LOGGED_IN
+ */
+ protected $authenticationStatus;
+
+ /**
+ * The chosen serialization format.
+ *
+ * This is one of the formats in $httpConfiguration, which lists all
+ * serialization formats supported by the site. This cannot be empty
+ * and is always one of SERIALIZATION_NONE, SERIALIZATION_JSON, or
+ * SERIALIZATION_JSONHAL.
+ *
+ * @var string
+ * @see ::SERIALIZATION_NONE
+ * @see ::SERIALIZATION_JSON
+ * @see ::SERIALIZATION_JSONHAL
+ * @see ::getServerConfiguration()
+ * @see ::getSerializationFormat()
+ * @see ::verifyServer()
+ */
+ protected $serializationFormat;
+
+ /**
+ * The most recent download file name.
+ *
+ * During a file download from the server, an HTTP header callback invoked
+ * by CURL looks for the "Content-Disposition" header line that names the
+ * file being downloaded. If found, the name is saved to this field where
+ * it is used at the end of the download to rename the file saved to local
+ * storage.
+ *
+ * @var string
+ * @see ::download()
+ * @see ::downloadHeaderCallback()
+ */
+ private $downloadFilename;
+
+ /*--------------------------------------------------------------------
+ *
+ * Construction & Destruction.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Creates a new connection ready to communicate to a remote host.
+ *
+ * Constructing an object of this class initializes internal state, but
+ * does not open a connection to a server yet. The caller must set
+ * the host name and, if needed, the authentication credentials to use.
+ * Thereafter, methods that request data from the server open the
+ * connection and send and receive the appropriate data.
+ *
+ * @see ::setHostName()
+ * @see ::setUserNameAndPassword()
+ */
+ public function __construct() {
+ if (phpversion('curl') === FALSE) {
+ throw new \RuntimeException(
+ "The required PHP CURL package is not installed.");
+ }
+
+ $this->authenticationType = self::AUTHENTICATION_NONE;
+ $this->authenticationStatus = self::STATUS_NOT_LOGGED_IN;
+ $this->serializationFormat = self::SERIALIZATION_NONE;
+ $this->userName = '';
+ $this->password = '';
+ $this->format = 'full';
+ $this->verbose = FALSE;
+
+ $this->clearCsrfToken();
+ $this->unverifyServer();
+
+ $this->curlSession = curl_init();
+ }
+
+ /**
+ * Closes the connection to the remote server.
+ *
+ * If the user is logged in, they are logged out first.
+ */
+ public function __destruct() {
+ $this->logout();
+ curl_close($this->curlSession);
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Verbosity.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Returns TRUE if the connection has been set to be verbose.
+ *
+ * When the connection is verbose, status messages are written to STDERR
+ * for each operation. This is useful when debugging, but probably not
+ * a feature users will enjoy.
+ *
+ * @return bool
+ * Returns TRUE if the connection is verbose.
+ *
+ * @see ::setVerbose()
+ */
+ public function isVerbose() {
+ return $this->verbose;
+ }
+
+ /**
+ * Sets whether the connection is verbose.
+ *
+ * When the connection is verbose, status messages are written to STDERR
+ * for each operation. This is useful when debugging, but probably not
+ * a feature users will enjoy.
+ *
+ * @param bool $verbose
+ * Sets the connection's verbosity to TRUE (verbose) or FALSE (not).
+ *
+ * @see ::isVerbose()
+ */
+ public function setVerbose(bool $verbose) {
+ $this->verbose = $verbose;
+ }
+
+ /**
+ * Prints a message to STDERR if verbosity is enabled.
+ *
+ * @param string $message
+ * The message to print if verbosity is enabled.
+ */
+ protected function printVerbose(string $message) {
+ if ($this->verbose === TRUE && empty($message) === FALSE) {
+ fwrite(STDERR, "FolderShareConnect: $message\n");
+ }
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Host.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Gets the name of the host and an optional port for the connection.
+ *
+ * The host name selects the site with which this connection communicates.
+ * Host names are typically a simple string like "example.com", but they
+ * optionally may include a port number for the web server, such as
+ * "example.com:80".
+ *
+ * @return string
+ * The host name and optional port for the web site.
+ *
+ * @see ::setHostName()
+ */
+ public function getHostName() {
+ return $this->hostName;
+ }
+
+ /**
+ * Sets the host name and an optional port for the connection.
+ *
+ * The host name selects the site with which this connection communicates.
+ * Host names are typically a simple string like "example.com", but they
+ * optionally may include a port number for the web server, such as
+ * "example.com:80".
+ *
+ * Setting the host name is normally done immediately after constructing
+ * the connection and before issuing any requests. If the host name is
+ * changed later, the user is logged out and the connection reset.
+ *
+ * @param string $hostName
+ * The host name and optional for the web site.
+ *
+ * @see ::getHostName()
+ * @see ::logout()
+ */
+ public function setHostName(string $hostName) {
+ $this->logout();
+ $this->hostName = rtrim($hostName, '/');
+ $this->clearCsrfToken();
+ $this->unverifyServer();
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Authentication.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Returns the authentication type in use.
+ *
+ * The authentication type is set at login as one of:
+ * - ATHENTICATION_NONE = there is no user authentication.
+ * - ATHENTICATION_BASIC = authentication uses a user name and password.
+ * - ATHENTICATION_COOKIE = authentication uses a session cookie along
+ * with a user name and password.
+ *
+ * @return string
+ * Returns the authentication type.
+ *
+ * @see ::AUTHENTICATION_NONE
+ * @see ::AUTHENTICATION_BASIC
+ * @see ::AUTHENTICATION_COOKIE
+ */
+ public function getAuthenticationType() {
+ return $this->authenticationType;
+ }
+
+ /**
+ * Returns a human-friendly name string for the authentication type.
+ *
+ * @param string $authenticationType
+ * The authentication type to translate into a name string.
+ *
+ * @return string
+ * Returns a human-friendly name for the authentication type.
+ *
+ * @see ::getAuthenticationType()
+ */
+ public function getAuthenticationTypeName(string $authenticationType) {
+ switch ($authenticationType) {
+ case self::AUTHENTICATION_NONE:
+ return "No Authentication";
+
+ case self::AUTHENTICATION_BASIC:
+ return "Basic Authentication";
+
+ case self::AUTHENTICATION_APIKEY:
+ return "Api-key based Authentication";
+
+ case self::AUTHENTICATION_COOKIE:
+ return "Cookie Authentication";
+
+ default:
+ return "Unknown Authentication: $authenticationType";
+ }
+ }
+
+ /**
+ * Gets the user name, if set, when using basic and cookie authentication.
+ *
+ * The user name is set at login.
+ *
+ * @return string
+ * Returns the current user name.
+ *
+ * @see ::getPassword()
+ * @see ::login()
+ * @see ::logout()
+ */
+ public function getUserName() {
+ return $this->userName;
+ }
+
+ /**
+ * Gets the password, if set, when using basic and cookie authentication.
+ *
+ * The password is set at login.
+ *
+ * @return string
+ * Returns the current password.
+ *
+ * @see ::getUserName()
+ * @see ::login()
+ * @see ::logout()
+ */
+ public function getPassword() {
+ return $this->password;
+ }
+
+ /**
+ * Returns TRUE if the user is logged in.
+ *
+ * A connection is logged in if credentials were supplied to login()
+ * and those credentials have been confirmed by the server. Confirmation
+ * is performed differently for different authentication types:
+ * - AUTHENTICATION_NONE: credentials are never confirmed and this
+ * function always returns FALSE.
+ * - AUTHENTICATION_COOKIE: credentials are confirmed at login().
+ * - AUTHENTICATION_BASIC: credentials are confirmed on the first request
+ * after login().
+ *
+ * @return bool
+ * Returns TRUE if the connection has been logged in, and FALSE
+ * otherwise.
+ */
+ public function isLoggedIn() {
+ switch ($this->authenticationStatus) {
+ default:
+ case self::STATUS_NOT_LOGGED_IN:
+ case self::STATUS_FAILED:
+ return FALSE;
+
+ case self::STATUS_LOGGED_IN:
+ return TRUE;
+ }
+ }
+
+ /**
+ * Returns a human-friendly name string for the authentication status.
+ *
+ * @param int $authenticationStatus
+ * The authentication status to translate into a name string.
+ *
+ * @return string
+ * Returns a human-friendly name for the authentication status.
+ */
+ protected function getAuthenticationStatusName(int $authenticationStatus) {
+ switch ($authenticationStatus) {
+ case self::STATUS_NOT_LOGGED_IN:
+ return "Not logged in";
+
+ case self::STATUS_FAILED:
+ return "Login failed";
+
+ case self::STATUS_LOGGED_IN:
+ return "Logged in";
+
+ default:
+ return "Unknown authentication status: $authenticationStatus";
+ }
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Configuration.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Returns the currently selected format for returned data.
+ *
+ * For requests that return data, that data is returned as an associative
+ * array in one of two formats:
+ * - 'full' = returns a full detailed entity or list of entities.
+ * - 'keyvalue' = returns a simplified entity or list of entities.
+ *
+ * @return string
+ * The name of a server-known format for returned data.
+ *
+ * @see ::setReturnFormat()
+ */
+ public function getReturnFormat() {
+ return $this->format;
+ }
+
+ /**
+ * Sets the currently selected format for returned data.
+ *
+ * For requests that return data, that data is returned as an associative
+ * array in one of two formats:
+ * - 'full' = returns a full detailed entity or list of entities.
+ * - 'keyvalue' = returns a simplified entity or list of entities.
+ *
+ * @param string $format
+ * The name of a server-known format for returned data.
+ *
+ * @see ::getReturnFormat()
+ */
+ public function setReturnFormat(string $format) {
+ $this->format = $format;
+ }
+
+ /**
+ * Returns the client-server serialization format.
+ *
+ * The serialization format describes the internal syntax used for
+ * communicating entities between the client and server. The format is
+ * one of those supported for entities by the server.
+ *
+ * @return string
+ * Returns the name of the serialization format.
+ *
+ * @see ::SERIALIZATION_JSON
+ * @see ::SERIALIZATION_JSONHAL
+ * @see ::verifyServer()
+ * @see ::getServerConfiguration()
+ */
+ public function getSerializationFormat() {
+ return $this->serializationFormat;
+ }
+
+ /**
+ * Returns a human-friendly name string for the serialization format.
+ *
+ * @param string $serializationFormat
+ * The serialization format to translate into a name string.
+ *
+ * @return string
+ * Returns a human-friendly name for the serialization format.
+ *
+ * @see ::getSerializationFormat()
+ */
+ public function getSerializationFormatName(string $serializationFormat) {
+ switch ($serializationFormat) {
+ case self::SERIALIZATION_NONE:
+ return "No Serialization";
+
+ case self::SERIALIZATION_JSON:
+ return "JSON Serialization";
+
+ case self::SERIALIZATION_JSONHAL:
+ return "JSON + Hypertext Application Language (HAL) Serialization";
+
+ default:
+ return "Unknown Serialization: $serializationFormat";
+ }
+ }
+
+ /**
+ * Returns the URL format option for the serialization format.
+ *
+ * The current serialization format mandates an option on request URLs
+ * that indicates the serialization format to use. This method returns
+ * that string.
+ *
+ * @return string
+ * Returns the URL option string for the serialization format.
+ */
+ private function getSerializationUrlQuery() {
+ switch ($this->serializationFormat) {
+ default:
+ case self::SERIALIZATION_NONE:
+ return '';
+
+ case self::SERIALIZATION_JSON:
+ return '_format=json';
+
+ case self::SERIALIZATION_JSONHAL:
+ return '_format=hal_json';
+ }
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Error messages.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Returns a nicer CURL error message.
+ *
+ * CURL errors indicate a communications problem. For all errors, CURL can
+ * provide a brief error message, but these messages are rarely sufficient
+ * to explain to a user what is going on and what they can do about it.
+ *
+ * This method maps selected error numbers to friendlier error messages.
+ * If the error number is not recognized, the default CURL error message
+ * is returned.
+ *
+ * @param int $errno
+ * The CURL error number.
+ *
+ * @return string
+ * Returns a friendly error message.
+ */
+ protected function getNiceCurlErrorMessage(int $errno) {
+ switch ($errno) {
+ case CURLE_URL_MALFORMAT:
+ return <<<'EOS'
+The host name is not in the proper format.
+Please check that the name does not include a "/", space, or other punctuation characters.
+EOS;
+
+ case CURLE_URL_MALFORMAT_USER:
+ case CURLE_MALFORMAT_USER:
+ return <<<'EOS'
+The user name is not in the proper format.
+Please check that the name does not include a ":", space, or other punctuation characters.
+EOS;
+
+ case CURLE_COULDNT_RESOLVE_HOST:
+ return <<<'EOS'
+The host could not be found.
+Please check your host name for typos.
+EOS;
+
+ case CURLE_HTTP_PORT_FAILED:
+ $colonIndex = mb_strpos($this->hostName, ':');
+ if ($colonIndex === FALSE) {
+ return <<<'EOS'
+The host refused a connection on the default port 80.
+The host may not support web service access or it may require a different port. You may specify a port by adding ":" and a port number to the end of the host name (e.g. "example.com:1234").
+EOS;
+ }
+
+ $port = mb_substr($this->hostName, ($colonIndex + 1));
+ return <<<EOS
+The host refused a connection on the specified port $port.
+Please check that the port number is correct.
+EOS;
+
+ case CURLE_COULDNT_CONNECT:
+ case CURLE_OPERATION_TIMEOUTED:
+ return <<<'EOS'
+The host is not responding.
+The host may be down, there could be a network problem, or the host may not support the network connections required by this application.
+EOS;
+
+ case CURLE_BAD_PASSWORD_ENTERED:
+ return <<<'EOS'
+The host denied access because the user name or password is incorrect.
+Please check your user name and password for typos.
+EOS;
+
+ case CURLE_REMOTE_ACCESS_DENIED:
+ return <<<'EOS'
+The host denied access.
+The host may not support web service access, or the user name and password may not grant sufficient permission to respond to your request.
+EOS;
+
+ case CURLE_TOO_MANY_REDIRECTS:
+ return <<<'EOS'
+There is a communications problem with the host.
+The host is repeatedly redirecting requests to another host, or to the same host in a never-ending cycle. Please report this to the host's administrator.
+EOS;
+
+ case CURLE_SEND_ERROR:
+ return <<<'EOS'
+The host unexpectedly stopped receiving requests.
+The host may have gone down or there could be a network problem.
+EOS;
+
+ case CURLE_RECV_ERROR:
+ return <<<'EOS'
+The host unexpectedly stopped sending information.
+The host may have gone down or there could be a network problem.
+EOS;
+
+ case CURLE_FILESIZE_EXCEEDED:
+ return <<<'EOS'
+The host reported that the file size was too large.
+The file may be too large to transfer.
+EOS;
+
+ case CURLE_PARTIAL_FILE:
+ return <<<'EOS'
+The host failed to send the entire file.
+The host may be busy, there may be a connection problem, or the file may be too large to transfer reliably.
+EOS;
+
+ default:
+ return curl_error($this->curlSession);
+ }
+ }
+
+ /**
+ * Returns a nicer HTTP error message.
+ *
+ * Web servers that respond with an error HTTP code may or may not include
+ * a message with that code. If they do not, this method returns a nicer
+ * message for a variety of common HTTP codes.
+ *
+ * @param int $httpCode
+ * The HTTP code.
+ *
+ * @return string
+ * Returns friendlier error message.
+ */
+ protected function getNiceHttpErrorMessage(int $httpCode) {
+ //
+ // The list below is intentionally incomplete. HTTP codes that are not
+ // likely for requests issued by this class are not included. For
+ // instance, codes associated with WebDAV, IM, and proxies are not
+ // included.
+ //
+ switch ($httpCode) {
+ case 200:
+ // OK.
+ return <<<'EOS'
+The request succeeded.
+EOS;
+
+ case 201:
+ // Created.
+ return <<<'EOS'
+The request to create content succeeded.
+EOS;
+
+ case 202:
+ // Accepted.
+ return <<<'EOS'
+The request was accepted and processing is in progress.
+EOS;
+
+ case 204:
+ // No content.
+ return <<<'EOS'
+The request succeeded and, as expected, no content was returned.
+EOS;
+
+ case 400:
+ // Bad request.
+ return <<<'EOS'
+There is a problem with the request.
+The host rejected the request because it is too large or it is improperly formatted. This is probably a programming error. Please contact the developers of this software.
+EOS;
+
+ case 401:
+ // Unauthorized. This really should be handled outside this method
+ // so that the server's response header can be considered.
+ return <<<'EOS'
+The host has denied access.
+The host may require additional authentication or the host may be blocking access from your local host or network.
+EOS;
+
+ case 403:
+ // Forbidden.
+ return <<<'EOS'
+The host denied access. You do not have permission for this request.
+EOS;
+
+ case 404:
+ // Not found.
+ return <<<'EOS'
+The requested information could not be found.
+EOS;
+
+ case 405:
+ // Method not allowed.
+ return <<<'EOS'
+The host does not support this type of information request.
+EOS;
+
+ case 406:
+ // Not acceptable.
+ return <<<'EOS'
+The host does not support information interchange in a format recognized by this application.
+EOS;
+
+ case 408:
+ // Request timeout.
+ return <<<'EOS'
+The host is unexpectedly not responding.
+The host may have gone down, there could be a network problem, or the host may have had a problem with the request.
+EOS;
+
+ case 410:
+ // Gone.
+ return <<<'EOS'
+The requested information is no longer available.
+EOS;
+
+ case 429:
+ // Too many requests.
+ return <<<'EOS'
+The host is declining connections because of too many requests.
+The host is rate limiting its communications and the number of requests has exceeded that limit.
+EOS;
+
+ case 500:
+ // Internal server error.
+ return <<<'EOS'
+The host encountered an unknown internal error.
+If this continues, please report this to the host's administrator.
+EOS;
+
+ case 503:
+ // Service unavailable.
+ return <<<'EOS'
+The host's services are temporarily unavailable.
+Please check back later.
+EOS;
+
+ default:
+ // All other error codes are obscure. Return a generic message.
+ return <<<EOS
+The host responded with an unexpected HTTP error code: $httpCode.
+EOS;
+ }
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Utilities.
+ *
+ * The miscellaneous functions here provide mixed functionality to
+ * later code.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Creates an empty dummy entity.
+ *
+ * During POST and PATCH operations, a new entity and fields are required
+ * in the request. If the operation does not need a detailed entity, or if
+ * the caller does not provide one, this dummy entity is used.
+ *
+ * @return array
+ * Returns a dummy entity array with well-known fields and no values.
+ */
+ protected function createDummyEntity() {
+ // The 'name' field is required, but the others are not.
+ // - 'uid'.
+ // - 'created'.
+ // - 'changed'.
+ // - 'description'.
+ // - 'kind'.
+ // - 'mime'.
+ // - 'parentid'.
+ // - 'rootid'.
+ // - 'size'.
+ // - 'file'.
+ // - 'image'.
+ // - 'media'.
+ // - 'grantauthoruids'.
+ // - 'grantviewuids'.
+ return [
+ 'name' => [],
+ ];
+ }
+
+ /**
+ * Returns CURL options common to most HTTP requests made here.
+ *
+ * These options do the following:
+ * - Set the user agent.
+ * - Request that content be returned.
+ * - Disable returning the header.
+ * - Enable redirect following.
+ * - Set timeouts.
+ *
+ * If $includeAuthentication is TRUE, then HTTP basic user name and
+ * password credentials are added to the header, if they have been set.
+ *
+ * @param bool $includeAuthentication
+ * (optional, default = TRUE) When TRUE, authentication credentials
+ * are included.
+ *
+ * @return array
+ * Returns an array of common Curl options.
+ *
+ * @see ::login()
+ */
+ protected function getCommonCurlOptions(bool $includeAuthentication = TRUE) {
+ $options = [
+ // Requests come from this class.
+ CURLOPT_USERAGENT => 'FolderShareConnect',
+
+ // Return the request's results.
+ CURLOPT_RETURNTRANSFER => TRUE,
+
+ // Don't include the HTTP header in the results. This corrupts
+ // those results. Instead, if the header is needed, a header callback
+ // is used.
+ CURLOPT_HEADER => FALSE,
+
+ // Don't encode.
+ CURLOPT_ENCODING => '',
+
+ // Follow redirects.
+ CURLOPT_FOLLOWLOCATION => TRUE,
+ CURLOPT_AUTOREFERER => TRUE,
+ CURLOPT_MAXREDIRS => 10,
+
+ // Time-out if things stall.
+ CURLOPT_CONNECTTIMEOUT => 120,
+ CURLOPT_TIMEOUT => 120,
+ ];
+
+ // If there is a need to include authentication credentials, add the
+ // right options.
+ if ($includeAuthentication === TRUE) {
+ $typeName = $this->getAuthenticationTypeName($this->authenticationType);
+ $this->printVerbose(" Using $typeName");
+
+ switch ($this->authenticationType) {
+ case self::AUTHENTICATION_BASIC:
+ // Add credentials for basic authentication.
+ $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
+ $options[CURLOPT_USERNAME] = $this->userName;
+ $options[CURLOPT_PASSWORD] = $this->password;
+ break;
+
+ case self::AUTHENTICATION_APIKEY:
+ // Nothing to add for API key based authentication.
+ break;
+
+ case self::AUTHENTICATION_COOKIE:
+ // Add credentials for cookie authentication.
+ $options[CURLOPT_COOKIEJAR] = "";
+ $options[CURLOPT_COOKIEFILE] = "";
+ break;
+ }
+ }
+
+ return $options;
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Server verification.
+ *
+ * These methods verify a connection to a server and request the
+ * features it supports.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Resets verification of server communications.
+ *
+ * Server verification is needed prior to the first server request to
+ * insure that the server exists and can respond to requests, and to
+ * get the list of HTTP verbs supported by the sever.
+ *
+ * Server verification should be reset each time key communications
+ * parameters are changed, such as the host name. It is not necessary to
+ * reset on changes to authentication credentials since the server would
+ * still exist and still support the same range of HTTP requests.
+ *
+ * @see ::verifyServer()
+ */
+ protected function unverifyServer() {
+ // Log the user out if they are logged in.
+ $this->logout();
+
+ $this->serverVerified = FALSE;
+ $this->httpVerbs = [];
+ $this->httpConfiguration = [];
+ $this->serializationFormat = self::SERIALIZATION_NONE;
+ $this->authenticationType = self::AUTHENTICATION_NONE;
+ $this->authenticationStatus = self::STATUS_NOT_LOGGED_IN;
+ $this->logoutToken = '';
+
+ $this->clearCsrfToken();
+ }
+
+ /**
+ * Verifies that the server exists and can respond to requests.
+ *
+ * This method is called the first time a request is made of the server.
+ * It serves three purposes:
+ * - Validates that the server exists and can respond.
+ * - Gets the HTTP verbs that the site responds to.
+ * - Gets the HTTP verb serialization and authentication features supported
+ * by the site.
+ *
+ * An exception is thrown if the server does not exist or cannot respond.
+ * An exception is also thrown if the server does not respond with an
+ * 'Allow' header that lists supported HTTP verbs.
+ *
+ * Otherwise the 'Allow' header is used to initialize the $httpVerbs
+ * field that lists supported HTTP verbs, such as 'GET', 'POST',
+ * 'PATCH', and 'DELETE'.
+ *
+ * An HTTP configuration request is then sent to get the serialization
+ * and authentication features of the site. These may conflict with the
+ * server's response about allowed HTTP verbs. These are reconciled and
+ * checked for supportability by this software. If there is no known
+ * authentication provider supported, or no known serialization format
+ * supported, an exception is thrown.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such as with a bad host name.
+ * - The host refuses contact.
+ * - The host does not support the operation.
+ * - The host does not support a known authentication provider.
+ * - The host does not support a known serialization format.
+ *
+ * @see ::unverifyServer()
+ */
+ protected function verifyServer() {
+ //
+ // Validate
+ // --------
+ // If the server has already been verified, no further action is needed.
+ if ($this->serverVerified === TRUE) {
+ return;
+ }
+
+ //
+ // Get configuration
+ // -----------------
+ // Query the site's configuration. This serves several purposes:
+ //
+ // - It verifies that the server exists and that we can communicate
+ // with it.
+ //
+ // - It determines the serialization format to use for further requests.
+ //
+ // - It determines the authentication mechanism to use for further
+ // requests.
+ //
+ // - It gets us the HTTP verbs supported (GET, POST, PATCH, DELETE).
+ //
+ // The response lists the HTTP verbs, serialization formats, and
+ // authentication mechanisms supported. Unfortunately, making the
+ // request at all requires knowing the right serialization format to
+ // use, so we have to iterate over the possibilities.
+ //
+ // Server verification occurs before a user login, so the current
+ // authentication type is AUTHENTICATION_NONE and no credentials will
+ // be included in the request.
+ //
+ // This GET request will fail if the site does not have the FolderShare
+ // REST resource enabled, or there is a significant version skew between
+ // this client software and the server module.
+ $this->clearCsrfToken();
+
+ // Reduce the HTTP verbs to just "GET", which we presume is supported.
+ // If it isn't, then the configuration request below will fail and
+ // the server does not support communications.
+ $this->httpVerbs = ["GET"];
+
+ $this->printVerbose("Verifying server connection with configuration query");
+
+ $serializationFormats = [
+ // The JSON format is the most common, so try it first.
+ self::SERIALIZATION_JSON,
+
+ // The JSONHAL format is a derivative of JSON that adds links that
+ // we can safely ignore.
+ self::SERIALIZATION_JSONHAL,
+ ];
+
+ $config = NULL;
+ $savedException = NULL;
+
+ foreach ($serializationFormats as $format) {
+ try {
+ $this->serializationFormat = $format;
+ $config = $this->getServerConfiguration();
+ break;
+ }
+ catch (\RuntimeException $e) {
+ if ($e->getCode() < 0) {
+ // Curl error. Fatal.
+ throw $e;
+ }
+
+ // Save the exception and try the next format, if any.
+ $savedException = $e;
+ }
+ }
+
+ if ($savedException !== NULL) {
+ // Unfortunately, Drupal has not provided a meaningful HTTP code or
+ // message on some failures. For instance, if the route we tried is not
+ // recognized because the FolderShare module is not enabled, Drupal
+ // returns a generic "404" error, indicating "The requested information
+ // could not be found." This is no help to the user.
+ $httpCode = $savedException->getCode();
+ $message = $e->getMessage();
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ if ($config === NULL) {
+ // The server failed to return a configuration from which we can
+ // determine what HTTP verbs are supported and what authentication
+ // and serialization formats to use.
+ $message = <<<'EOS'
+The host did not return a server configuration.
+There is probably a bug in the server software. Please contact the host's administrator.
+EOS;
+ $this->printVerbose(" Server failed to return a usable server configuration");
+ $this->printVerbose(" $message");
+ $this->serverVerified = FALSE;
+ throw new \RuntimeException($message);
+ }
+
+ // Check the FolderShare module version number for compatability.
+ $installedVersion = explode('.', $config['foldershare']);
+ $minimumVersion = explode('.', self::MINIMUM_FOLDERSHARE_VERSION);
+ if ($installedVersion[0] < $minimumVersion[0] ||
+ $installedVersion[1] < $minimumVersion[1] ||
+ $installedVersion[2] < $minimumVersion[2]) {
+ //throw new \RuntimeException("Version mismatch with server's FolderShare module.\nThis client requires version " . self::MINIMUM_FOLDERSHARE_VERSION . " but the server module is an older version " . $config['foldershare'] . ". This client will not work with " . $this->hostName . ". Please contact the site administrator about upgrading the site, or please use an older version of this client.");
+ }
+
+ // Go through the returned configuration and determine what HTTP verbs
+ // are supported.
+ foreach ($config as $key => $values) {
+ switch ($key) {
+ case "GET":
+ case "POST":
+ case "PATCH":
+ case "DELETE":
+ $this->httpVerbs[] = $key;
+ break;
+
+ default:
+ // Ignore anything else.
+ break;
+ }
+ }
+
+ $this->printVerbose(" Supported requests: " . implode(', ', $this->httpVerbs));
+
+ // Tentatively verify server connection. This may be reversed below.
+ $this->serverVerified = TRUE;
+
+ //
+ // Select authentication type
+ // --------------------------
+ // Based upon the list of authentication providers supported by the
+ // server, select a preferred authentication type supported by this client.
+ //
+ // Make the decision based upon what GET supports on the server, then
+ // check that POST, PATCH, and DELETE support it as well. If not, abort.
+ $bestAuthenticationType = self::AUTHENTICATION_NONE;
+
+ foreach (self::AUTHENTICATION_PREFERENCE_ORDER as $preference) {
+ if (in_array($preference, $config['GET']['authentication-providers']) === TRUE) {
+ $bestAuthenticationType = $preference;
+ break;
+ }
+ }
+
+ if ($bestAuthenticationType === self::AUTHENTICATION_NONE) {
+ // None of the authentication types supported by this client are
+ // listed as supported by the server.
+ $message = <<<'EOS'
+The host does not support any known authentication methods.
+This can occur if the host's web services configuration has not enabled any of the authentication methods supported by this software. Please contact the host's administrator.
+EOS;
+ $this->printVerbose(" Server does not support any known authentication provider");
+ $this->printVerbose(" $message");
+ $this->serverVerified = FALSE;
+ throw new \RuntimeException($message);
+ }
+
+ foreach ($this->httpVerbs as $verb) {
+ if (in_array($bestAuthenticationType, $config[$verb]['authentication-providers']) === FALSE) {
+ // The best authentication type chosen above is NOT supported by
+ // this HTTP verb.
+ $message = <<<'EOS'
+The host has a confusing configuration for authentication providers.
+This can occur if the host's administrator has missconfigured web services and enabled a different authentication provider for each of the web service methods. Please contact the host's administrator.
+EOS;
+ $this->printVerbose(" Server does not have same authentication providers for all HTTP verbs");
+ $this->printVerbose(" $message");
+ $this->serverVerified = FALSE;
+ throw new \RuntimeException($message);
+ }
+ }
+
+ // If API key and masquerade user are both provided, set authentication type
+ // to be self::AUTHENTICATION_APIKEY.
+ if (empty($this->apikey) === FALSE && empty($this->masquerade) === FALSE) {
+ $this->authenticationType = self::AUTHENTICATION_APIKEY;
+ }
+ else {
+ $this->authenticationType = $bestAuthenticationType;
+ }
+
+ //
+ // Select serialization format
+ // ---------------------------
+ // Based upon the lsit of serialization formats supported by the
+ // server, select a preferred format supported by this client.
+ //
+ // Make the decision based upon what GET supports on the server, then
+ // check that POST, PATCH, and DELETE support it as well. If not, abort.
+ $bestSerializationFormat = self::SERIALIZATION_NONE;
+
+ foreach (self::SERIALIZATION_PREFERENCE_ORDER as $preference) {
+ if (in_array($preference, $config['GET']['serializer-formats']) === TRUE) {
+ $bestSerializationFormat = $preference;
+ break;
+ }
+ }
+
+ if ($bestSerializationFormat === self::SERIALIZATION_NONE) {
+ // None of the serialization formats supported by this client are
+ // listed as supported by the server.
+ $message = <<<'EOS'
+The host does not support any known serialization methods.
+This can occur if the host's web services configuration has not enabled any of the serialization formats supported by this software. Please contact the host's administrator.
+EOS;
+ $this->printVerbose(" Server does not support any known serialization formats");
+ $this->printVerbose(" $message");
+ $this->serverVerified = FALSE;
+ throw new \RuntimeException($message);
+ }
+
+ foreach ($this->httpVerbs as $verb) {
+ if (in_array($bestSerializationFormat, $config[$verb]['serializer-formats']) === FALSE) {
+ // The best format chosen above is NOT supported by this HTTP verb.
+ $message = <<<'EOS'
+The host has a confusing configuration for serialization formats.
+This can occur if the host's administrator has missconfigured web services and enabled a different serialization format for each of the web service methods. Please contact the host's administrator.
+EOS;
+ $this->printVerbose(" Server does not have same serialization format for all HTTP verbs");
+ $this->printVerbose(" $message");
+ $this->serverVerified = FALSE;
+ throw new \RuntimeException($message);
+ }
+ }
+
+ $this->serializationFormat = $bestSerializationFormat;
+
+ // Done at last!
+ $this->printVerbose(" Server connection verified");
+ $this->printVerbose(" Authentication type to use: " .
+ $this->getAuthenticationTypeName($this->authenticationType));
+ $this->printVerbose(" Serialization format to use: " .
+ $this->getSerializationFormatName($this->serializationFormat));
+
+ // Get the CSRF token.
+ $this->getCsrfToken();
+ }
+
+ /**
+ * Returns TRUE if HTTP DELETE requests are supported on the server.
+ *
+ * If no requests have been made to the server yet, this method makes
+ * a server request to get the list of HTTP requests supported.
+ *
+ * @return bool
+ * Returns TRUE if HTTP DELETE requests are supported.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact.
+ * - The host does not support the operation.
+ */
+ public function isHttpDeleteSupported() {
+ $this->verifyServer();
+ return in_array('DELETE', $this->httpVerbs);
+ }
+
+ /**
+ * Returns TRUE if HTTP GET requests are supported on the server.
+ *
+ * If no requests have been made to the server yet, this method makes
+ * a server request to get the list of HTTP requests supported.
+ *
+ * @return bool
+ * Returns TRUE if HTTP GET requests are supported.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact.
+ * - The host does not support the operation.
+ */
+ public function isHttpGetSupported() {
+ $this->verifyServer();
+ return in_array('GET', $this->httpVerbs);
+ }
+
+ /**
+ * Returns TRUE if HTTP PATCH requests are supported on the server.
+ *
+ * If no requests have been made to the server yet, this method makes
+ * a server request to get the list of HTTP requests supported.
+ *
+ * @return bool
+ * Returns TRUE if HTTP PATCH requests are supported.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact.
+ * - The host does not support the operation.
+ */
+ public function isHttpPatchSupported() {
+ $this->verifyServer();
+ return in_array('PATCH', $this->httpVerbs);
+ }
+
+ /**
+ * Returns TRUE if HTTP POST requests are supported on the server.
+ *
+ * If no requests have been made to the server yet, this method makes
+ * a server request to get the list of HTTP requests supported.
+ *
+ * @return bool
+ * Returns TRUE if HTTP POST requests are supported.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact.
+ * - The host does not support the operation.
+ */
+ public function isHttpPostSupported() {
+ $this->verifyServer();
+ return in_array('POST', $this->httpVerbs);
+ }
+
+ /**
+ * Gets a list of HTTP operations supported on the server.
+ *
+ * If no requests have been made to the server yet, this method makes
+ * a server request to get the list of HTTP requests supported. Typically
+ * these include:
+ * - GET = get items or configuration information.
+ * - POST = create or upload new items.
+ * - PATCH = change existing items.
+ * - DELETE = delete existing items.
+ *
+ * A host usually has all of these enabled, but for security reasons it
+ * is possible for a host to disable some or all of them. For instance,
+ * a host that publishes public content may have GET enabled, but it
+ * may disable POST, PATCH, and DELETE so that that content cannot be
+ * modified.
+ *
+ * @return string[]
+ * Returns an array listing the HTTP verbs supported by the server.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact.
+ * - The host does not support the operation.
+ *
+ * @see ::getServerConfiguration()
+ * @see ::isHttpDeleteSupported()
+ * @see ::isHttpGetSupported()
+ * @see ::isHttpPatchSupported()
+ * @see ::isHttpPostSupported()
+ */
+ public function getHttpVerbs() {
+ $this->verifyServer();
+ return $this->httpVerbs;
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * CSRF token handling.
+ *
+ * A CSRF (Cross-Site Request Forgery) token must be requested, returned,
+ * and used for all operations that might change remote content (e.g.
+ * for POST, PATCH, and DELETE requests).
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Clears the CSRF token saved from a prior request.
+ *
+ * The connection's CSRF token is initially cleared and it must be
+ * cleared again (to trigger a future request for a new CSRF token)
+ * any time primary communications parameters are changed, such as
+ * the host name. Logging out also clears the CSRF token.
+ *
+ * @see ::getCsrfToken()
+ * @see ::logout()
+ */
+ protected function clearCsrfToken() {
+ $this->csrfToken = NULL;
+ }
+
+ /**
+ * Returns the current CSRF token.
+ *
+ * A "Cross-Site Request Forgery" (CSRF) is a web site or user attack
+ * that sends a message to a server, without the user's knowledge, to
+ * change the user's data on the server. The message reuses the current
+ * authentication credentials of a logged in user.
+ *
+ * A CSRF token is a randomly generated string created by the server and
+ * issued to the client for use throughout a session. Drupal requires
+ * that all "non-safe" HTTP requests (i.e. anything except OPTIONS, GET,
+ * and HEAD) send a previously acquired CSRF token as part of its
+ * validation mechanism. Requests without the CSRF token, or with a wrong
+ * token, are rejected.
+ *
+ * The communications methods in this class call this method to get the
+ * current CSRF token each time they need to make a "non-safe" operation.
+ * If no token has been requested from the site yet, a request is made
+ * immediately to get a new CSRF token. That token is saved and re-used
+ * on future calls.
+ *
+ * Cookie-based authentication automatically retreives the CSRF token
+ * during login. Other authentication types may require an explicit
+ * request to get a CSRF token.
+ *
+ * @return string
+ * Returns the current CSRF token, or NULL if a token could not
+ * be retrieved because the site is down or it does not respond to
+ * a token request.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ *
+ * @see ::clearCsrfToken()
+ * @see ::login()
+ */
+ protected function getCsrfToken() {
+ //
+ // Return current token (if any)
+ // -----------------------------
+ // If a previous request for a CSRF token has already returned one
+ // for this session, return it immediately.
+ if ($this->csrfToken !== NULL) {
+ return $this->csrfToken;
+ }
+
+ //
+ // Set up request
+ // --------------
+ // Get common CURL options and update them with specifics of this request.
+ //
+ // Authentication credentials are not required. In fact, if they are
+ // included, the request will fail.
+ $options = $this->getCommonCurlOptions(FALSE);
+ $options[CURLOPT_HTTPGET] = TRUE;
+ $options[CURLOPT_URL] = $this->hostName . self::URL_CSRF_TOKEN;
+
+ curl_reset($this->curlSession);
+ curl_setopt_array($this->curlSession, $options);
+
+ //
+ // Issue GET
+ // ---------
+ // Issue an HTTP GET to the web server. There are two types of errors:
+ // - Communications errors reported by CURL as error numbers.
+ // - Web site errors reported as bad HTTP codes.
+ //
+ // The returned content is the CSRF token string.
+ $this->printVerbose("Getting CSRF token");
+ $content = curl_exec($this->curlSession);
+ $httpCode = curl_getinfo($this->curlSession, CURLINFO_RESPONSE_CODE);
+ $errno = curl_errno($this->curlSession);
+
+ if ($errno !== 0) {
+ // Communications error. Something went wrong with Curl or with
+ // its communications with the server.
+ $message = $this->getNiceCurlErrorMessage($errno);
+ $this->printVerbose(" CURL failed with error code $errno");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ if ($httpCode !== 200) {
+ // A CSRF token was not returned! Operations that require the token
+ // cannot be done.
+ if (isset($content['message']) === TRUE) {
+ $message = $content['message'];
+ }
+ else {
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ $this->printVerbose(" CSRF token received");
+ $this->csrfToken = $content;
+ return $this->csrfToken;
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Login and logout.
+ *
+ * Login uses the authentication type and credentials to establish an
+ * authenticated connection with the server. Logout reverses that.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Logs the user in to the server using the given authentication credentials.
+ *
+ * Authentication credentials include a user name and password for
+ * the remote server. Setting these credentials and logging in must be
+ * done before any requests may be sent to the server.
+ *
+ * The way authentication credentials are used depends upon the
+ * authentication type:
+ * - AUTHENTICATION_NONE does not use the credentials at all. Requests
+ * to the server are not authenticated, which usually means the caller
+ * only has access to public content that is available to anonymous
+ * visitors to the site.
+ * - AUTHENTICATION_BASIC sends the credentials along on every request.
+ * The server checks the credentials before responding.
+ * - AUTHENTICATION_COOKIE sends the credentials along on an initial
+ * "login" request during which the server checks the credentials before
+ * responding with a session cookie. The session cookie is then sent
+ * along on all subsequent requests.
+ *
+ * AUTHENTICATION_NONE is the initial state for a newly opened connection
+ * object, but it is rarely suitable for use. Most activities require an
+ * authenticated connection.
+ *
+ * AUTHENTICATION_COOKIE is the style used by web browsers where the user
+ * logs into a site, then makes requests for site pages. The browser saves
+ * the session cookie and automatically re-uses it for each request.
+ *
+ * AUTHENTICATION_BASIC is the style used by many web services. The
+ * credentials are passed along with every request and there are no cookies
+ * involved. This works because web services are often isolated requests
+ * rather than a user asking for page after page after page.
+ *
+ * The AUTHENTICATION_COOKIE type is the most efficient when multiple
+ * requests are made through the same open connection. This type only does
+ * full authentication on the server at the start of a series of requests.
+ * However, if the server has a non-standard login process, then this
+ * class cannot reliably get the session cookie and authentication will
+ * fail.
+ *
+ * The AUTHENTICATION_BASIC type is the most reliable since it can work
+ * regardless of the login process.
+ *
+ * Authentication failure can occur for several reasons:
+ * - The server may not support web services at all.
+ * - The server may have a non-standard user login form that requires a
+ * CAPTCHA or other required input that this software cannot provide.
+ * - The server may not have the authentication type enabled.
+ * - The server may reject the user name and password as invalid.
+ *
+ * If the user name is empty, no authentication can be done and the
+ * authentication type is automatically reset to AUTHENTICATION_NONE.
+ *
+ * @param string $userName
+ * The user name. If the name is empty, authentication is disabled and
+ * the authentication type is set to AUTHENTICATION_NONE. This is
+ * anonymous login for public content access only.
+ * @param string $password
+ * The password. An empty password is allowed, but discouraged.
+ *
+ * @return bool
+ * Returns TRUE if the login is successful, and FALSE otherwise.
+ * Failed authentication often throws an exception.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such as with a bad host name.
+ * - The host refuses contact.
+ * - The authentication type is not accepted.
+ * - The user name or password are not accepted.
+ *
+ * @see ::getUserName()
+ * @see ::getPassword()
+ * @see ::getAuthenticationType()
+ * @see ::isLoggedIn()
+ * @see ::logout()
+ */
+ public function login(string $userName, string $password, string $apikey, string $masquerade) {
+ if (empty($apikey) === FALSE && empty($masquerade) === FALSE) {
+ $this->apikey = $apikey;
+ $this->masquerade = $masquerade;
+ }
+
+ //
+ // Verify server
+ // -------------
+ // If the server connection has not yet been verified, do so now.
+ // We cannot log in without being sure that the host and port are
+ // right and that a server is responding.
+ //
+ // Verification also checks the authentication types supported by the
+ // server and selects the best one. We need this in order to login below.
+ //
+ // Verification also checks the serialization formats supported by the
+ // server and selects the best one. We'll need this later when
+ // communicating with the server.
+ //
+ // This call returns immediately if the server is already verified.
+ //
+ // This call throws an exception if the host name is bad or there
+ // is some other problem communicating with the server, such as no
+ // known authentication type or serialization format.
+ $this->verifyServer();
+
+ //
+ // Log out
+ // -------
+ // If the user is already logged in, then log out first. After this,
+ // the authentication status is guaranteed to be STATUS_NOT_LOGGED_IN.
+ //
+ // This call returns immediately if the user is not logged in.
+ $this->logout();
+
+ //
+ // Save values
+ // -----------
+ // If the user name is empty, ignore the password too.
+ $this->userName = $userName;
+ $this->password = $password;
+ if (empty($userName) === TRUE) {
+ $this->password = '';
+ }
+ $this->printVerbose("Logging in for user='$this->userName'");
+
+ //
+ // Skip logging in for anonymous
+ // -----------------------------
+ // If there is no user name, then there is no logging in. All accesses
+ // are anonymous.
+ if (empty($this->userName) === TRUE) {
+ $this->authenticationStatus = self::STATUS_LOGGED_IN;
+ $this->printVerbose(" Authentication skipped for anonymous access");
+ return TRUE;
+ }
+
+ //
+ // Log in
+ // ------
+ // Use the authentication type determined during server verification.
+ $this->printVerbose(" Logging in with " .
+ $this->getAuthenticationTypeName($this->authenticationType));
+
+ switch ($this->authenticationType) {
+ case self::AUTHENTICATION_COOKIE:
+ // Authentication uses a session cookie.
+ //
+ // When using cookie authentication, the user is logged in by sending
+ // credentials now. On success, the server returns a session cookie
+ // that is used for further requests.
+ try {
+ $this->loginForCookie();
+ $this->authenticationStatus = self::STATUS_LOGGED_IN;
+ $this->printVerbose(" Authentication succeeded");
+ }
+ catch (\Exception $e) {
+ $this->authenticationStatus = self::STATUS_FAILED;
+ $this->printVerbose(" Authentication failed");
+ throw $e;
+ }
+ return TRUE;
+
+ case self::AUTHENTICATION_BASIC:
+ // Authentication is on every request.
+ //
+ // When using basic authentication, the user is logged in separately
+ // on every request. There is no explicit login process. To verify
+ // the user name and password are valid, we need to make a request.
+ //
+ // If there is no user name, then all accesses are anonymous and
+ // there is no need to verify credentials.
+ try {
+ $this->getVersion();
+ $this->authenticationStatus = self::STATUS_LOGGED_IN;
+ $this->printVerbose(" Authentication succeeded");
+ }
+ catch (\Exception $e) {
+ $this->authenticationStatus = self::STATUS_FAILED;
+ $this->printVerbose(" Authentication failed");
+ $message = <<<'EOS'
+The login to the host has failed.
+The user name and/or password may be incorrect. Please check them for typos. If they appear correct, it is also possible that the account has been blocked. If the account is new, it may not yet have been activated. Please contact the host's administrator.
+EOS;
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+ return TRUE;
+
+ case self::AUTHENTICATION_APIKEY:
+ // API key based Authentication is on every request.
+ //
+ // When using API key based authentication, the user is logged in
+ // on every request. There is no explicit login process.
+ try {
+ $this->getVersion();
+ $this->authenticationStatus = self::STATUS_LOGGED_IN;
+ $this->printVerbose(" Authentication succeeded");
+ }
+ catch (\Exception $e) {
+ $this->authenticationStatus = self::STATUS_FAILED;
+ $this->printVerbose(" Authentication failed");
+ $message = <<<'EOS'
+The login to the host has failed.
+The API key may be incorrect. Please check them for typos. If they appear correct, it is also possible that the account has been blocked. If the account is new, it may not yet have been activated. Please contact the host's administrator.
+EOS;
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+ return TRUE;
+
+ default:
+ // Unknown authentication method.
+ $this->authenticationStatus = self::STATUS_FAILED;
+ $this->printVerbose(" Authentication failed");
+ $message = <<<'EOS'
+The login to the host has failed.
+The host uses an unknown authentication method.
+EOS;
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+ }
+
+ /**
+ * Logs in using cookie authentication.
+ *
+ * The current credentials are used to log in and request a session
+ * cookie, which is automatically saved for further use within CURL.
+ * A successful login also returns and saves the CSRF token and a
+ * logout token.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such as with a bad host name.
+ * - The host does not support the operation.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The arguments are malformed.
+ * - The arguments are invalid, such as for non-existent items.
+ *
+ * @see ::login()
+ */
+ private function loginForCookie() {
+ //
+ // Create headers
+ // --------------
+ // The headers specify that post fields are in JSON format.
+ $headers = [];
+ $headers[] = 'Content-Type: application/json';
+
+ $postFields = [
+ 'name' => $this->userName,
+ 'pass' => $this->password,
+ ];
+
+ $postText = json_encode($postFields);
+
+ //
+ // Create URL
+ // ----------
+ // The POST URL includes:
+ // - The host name.
+ // - The post web services path.
+ // - The return syntax.
+ $url = $this->hostName . self::URL_USER_LOGIN . '?_format=json';
+
+ //
+ // Set up request
+ // --------------
+ // Get common CURL options and update them with specifics of this request.
+ //
+ // Setting COOKIELIST to "ALL" clears all prior cookies.
+ //
+ // Setting COOKIEFILE to "" causes CURL to maintain cookies in memory
+ // instead of in a file.
+ $options = $this->getCommonCurlOptions(FALSE);
+
+ $options[CURLOPT_URL] = $url;
+ $options[CURLOPT_POST] = 1;
+ $options[CURLOPT_POSTFIELDS] = $postText;
+ $options[CURLOPT_COOKIELIST] = "ALL";
+ $options[CURLOPT_COOKIEFILE] = "";
+ $options[CURLOPT_HTTPHEADER] = $headers;
+
+ curl_reset($this->curlSession);
+ curl_setopt_array($this->curlSession, $options);
+
+ //
+ // Issue POST
+ // ----------
+ // Issue an HTTP POST to the web server. There are two types of errors:
+ // - Communications errors reported by CURL as error numbers.
+ // - Web site errors reported as bad HTTP codes.
+ //
+ // The returned content varies depending upon the operation.
+ $content = curl_exec($this->curlSession);
+ $httpCode = curl_getinfo($this->curlSession, CURLINFO_RESPONSE_CODE);
+ $errno = curl_errno($this->curlSession);
+
+ if ($errno !== 0) {
+ // Communications error. Something went wrong with Curl or with
+ // its communications with the server.
+ $message = $this->getNiceCurlErrorMessage($errno);
+ $this->printVerbose(" CURL failed with error code $errno");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ // Decode JSON content.
+ if (empty($content) === FALSE) {
+ $content = json_decode($content, TRUE);
+ }
+
+ switch ($httpCode) {
+ case 200:
+ // OK. The request succeeded. Continue.
+ break;
+
+ case 400:
+ // Drupal's UserAuthenticationController checks the incoming
+ // credentials and responds with any of several possible errors.
+ // Unfortunately, they all come back as a generic HTTP 400 error
+ // for a "Bad request":
+ // - Missing credentials.
+ // - Missing credentials.name.
+ // - Missing credentials.pass.
+ // - User account has not been activated or is blocked.
+ // - Sorry, unrecognized username or password.
+ //
+ // The first three are not possible because we created and passed
+ // the POST fields above.
+ //
+ // The latter two errors are important and should have been 401 errors.
+ if (isset($content['message']) === TRUE) {
+ // Use the message returned by Drupal, as bad as it is.
+ $message = $content['message'];
+ mb_ereg_search_init($message);
+ if (mb_ereg_search('blocked') === TRUE) {
+ // Replace the message with something friendlier.
+ $message = <<<'EOS'
+The login to the host has failed.
+The account may have been blocked. If the account is new, it may not yet have been activated. Please contact the host's administrator.
+EOS;
+ }
+ elseif (mb_ereg_search('unrecognized') === TRUE) {
+ $message = <<<'EOS'
+The login to the host has failed.
+The user name and/or password may be incorrect. Please check them for typos.
+EOS;
+ }
+ }
+ else {
+ $message = <<<'EOS'
+The login to the host has failed.
+The user name and/or password may be incorrect. Please check them for typos. If they appear correct, it is also possible that the account has been blocked. If the account is new, it may not yet have been activated. Please contact the host's administrator.
+EOS;
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+
+ case 429:
+ // Drupal's UserAuthenticationController checks for rapid failed
+ // login requests and, after a limit, blocks the account for the
+ // requestor's IP address.
+ //
+ // Fall thru to the 'default' HTTP code handling.
+ //
+ default:
+ if (isset($content['message']) === TRUE) {
+ $message = $content['message'];
+ }
+ else {
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ //
+ // Parse content
+ // -------------
+ // The returned content is an associative array of the form:
+ // @code
+ // [
+ // "current_user" => [
+ // "uid" => 1234,
+ // "roles" => [
+ // "authenticated",
+ // "administrator",
+ // ...
+ // ],
+ // "name" => "myname",
+ // ],
+ // "csrf_token" => "token",
+ // "logout_token" => "token",
+ // ]
+ // @endcode
+ //
+ // The "name" we already know since we provided it in the first place.
+ // The user ID and roles are irrelevant here.
+ //
+ // Get and save the CSRF and logout tokens. The CSRF token must be
+ // sent to the server on all requests that change content (i.e.
+ // POST, PATCH, and DELETE). The logout token must sent to the server
+ // to formally log out.
+ if (isset($content['csrf_token']) === TRUE) {
+ $this->csrfToken = $content['csrf_token'];
+ }
+
+ if (isset($content['logout_token']) === TRUE) {
+ $this->logoutToken = $content['logout_token'];
+ }
+ }
+
+ /**
+ * Logs the user out of the server.
+ *
+ * If the user is not logged in, this method has no effect.
+ *
+ * If the user is logged in, they are logged out and their authentication
+ * credentials cleared. Further requests to the server will not provide
+ * authentication credentials. These requests may succeed if the server
+ * allows unauthenticated access, but they will fail if the user needs
+ * to be logged in.
+ *
+ * @see ::login()
+ * @see ::isLoggedIn()
+ */
+ public function logout() {
+ //
+ // Logout
+ // ------
+ // If the server hasn't been verified yet, then we're already logged out.
+ // If the user is logged in, log them out.
+ if ($this->serverVerified === FALSE) {
+ return;
+ }
+
+ if ($this->authenticationStatus === self::STATUS_LOGGED_IN) {
+ switch ($this->authenticationType) {
+ default:
+ case self::AUTHENTICATION_BASIC:
+ // No logout required.
+ break;
+
+ case self::AUTHENTICATION_APIKEY:
+ // No logout required.
+ break;
+
+ case self::AUTHENTICATION_COOKIE:
+ // Logout with the logout token.
+ $this->printVerbose("Logging out");
+ try {
+ $this->logoutForCookie();
+ }
+ catch (\Exception $e) {
+ // Ignore exceptions.
+ }
+
+ $this->printVerbose(" Logged out");
+ break;
+ }
+ }
+
+ //
+ // Reset
+ // -----
+ // Clear the authentication status, authentication type, user name,
+ // and password.
+ $this->authenticationStatus = self::STATUS_NOT_LOGGED_IN;
+ $this->userName = '';
+ $this->password = '';
+ $this->logoutToken = '';
+ }
+
+ /**
+ * Logs out using cookie authentication.
+ *
+ * The logout token, if any, is used to log out the user.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such as with a bad host name.
+ * - The host does not support the operation.
+ * - The host refuses contact, such as with bad authentication credentials.
+ *
+ * @see ::logout()
+ */
+ private function logoutForCookie() {
+ //
+ // Validate
+ // --------
+ // If there is no saved logout token from a prior login, then there
+ // is no need to logout.
+ if (empty($this->logoutToken) === TRUE) {
+ return;
+ }
+
+ //
+ // Create URL
+ // ----------
+ // The POST URL includes:
+ // - The host name.
+ // - The post logout path.
+ // - The return syntax.
+ // - The logout token.
+ $url = $this->hostName . self::URL_USER_LOGOUT .
+ '?' . $this->getSerializationUrlQuery() .
+ '&token=' . $this->logoutToken;
+
+ //
+ // Set up request
+ // --------------
+ // Get common CURL options and update them with specifics of this request.
+ $options = $this->getCommonCurlOptions(FALSE);
+
+ $options[CURLOPT_URL] = $url;
+ $options[CURLOPT_POST] = 1;
+ $options[CURLOPT_POSTFIELDS] = '';
+
+ curl_reset($this->curlSession);
+ curl_setopt_array($this->curlSession, $options);
+
+ //
+ // Issue POST
+ // ----------
+ // Issue an HTTP POST to the web server. There are two types of errors:
+ // - Communications errors reported by CURL as error numbers.
+ // - Web site errors reported as bad HTTP codes.
+ //
+ // The returned content varies depending upon the operation.
+ $content = curl_exec($this->curlSession);
+ $httpCode = curl_getinfo($this->curlSession, CURLINFO_RESPONSE_CODE);
+ $errno = curl_errno($this->curlSession);
+
+ if ($errno !== 0) {
+ // Communications error. Something went wrong with Curl or with
+ // its communications with the server.
+ $message = $this->getNiceCurlErrorMessage($errno);
+ $this->printVerbose(" CURL failed with error code $errno");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ // Decode JSON content.
+ if (empty($content) === FALSE) {
+ $content = json_decode($content, TRUE);
+ }
+
+ // Check for error codes. The following are standard HTTP success codes:
+ // - 200 = OK.
+ // - 201 = Created.
+ // - 202 = Accepted.
+ // - 203 = Non-authoritative information.
+ // - 204 = No content.
+ // - 205 = Reset content.
+ // - 206 = Partial content.
+ // - 207 = Multi-status.
+ // - 208 = Already reported.
+ //
+ // A POST to log out should always succeed and not return anything.
+ //
+ // All other standard success codes are unacceptable.
+ switch ($httpCode) {
+ case 200:
+ case 204:
+ return;
+
+ default:
+ if (isset($content['message']) === TRUE) {
+ $message = $content['message'];
+ }
+ else {
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Issue requests.
+ *
+ * These methods issue HTTP requests for raw data or to trigger operations.
+ * Other methods parse that data and return it.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Issues a GET request.
+ *
+ * GET requests are used to retrieve information from the server. The
+ * type of information varies with the operation choice:
+ * - Single entity requests:
+ * - 'get-entity' returns an entity.
+ * - 'get-parent' returns an entity's parent entity.
+ * - 'get-root' returns an entity's root entity.
+ * - Special entity information requests:
+ * - 'get-sharing' returns an entity's sharing settings.
+ * - Entity list requests:
+ * - 'get-ancestors' returns a list of an entity's ancestor entities.
+ * - 'get-descendants' returns a list of an entity's descendant entities.
+ * - 'get-search' returns a search through an entity's subtree.
+ * - Configuration and misc data requests:
+ * - 'get-configuration' returns site and module settings.
+ * - 'get-usage' returns module usage stats for the user.
+ * - 'get-version' returns version numbers for site software.
+ *
+ * GET requests focused on an entity (get the entity, parent, root,
+ * ancestors, or descendants) require a full folder path or a numeric
+ * entity ID to indicate the entity.
+ *
+ * @param string $operation
+ * The name of the requested operation (e.g. "get-entity").
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file or folder.
+ * Some operations do not use this value.
+ *
+ * @return mixed
+ * Returns a variety of content types depending upon the operation.
+ * In most cases, the content returned is an array.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The arguments are malformed.
+ * - The arguments are invalid, such as for non-existent items.
+ */
+ protected function httpGet(string $operation, $entityIdOrPath) {
+ //
+ // Initialize
+ // ----------
+ // Check if HTTP GET is supported. This may issue a connection to the
+ // server to find out. If that fails, an exception is thrown.
+ if ($this->serverVerified === TRUE) {
+ if ($this->isHttpGetSupported() === FALSE) {
+ throw new \RuntimeException("The operation cannot be completed.\nThe host does not support requests to get content.");
+ }
+ }
+
+ //
+ // Create headers
+ // --------------
+ // The headers specify the GET operation and the source path, if any.
+ $headers = [];
+
+ // Set the operation.
+ $headers[] = 'X-FolderShare-Get-Operation: ' . $operation;
+
+ // Request the returned data in a specific format.
+ $headers[] = 'X-FolderShare-Return-Format: ' . $this->format;
+
+ // Set the API key.
+ $headers[] = 'api-key: ' . $this->apikey;
+
+ // Set the masqueraded user.
+ $headers[] = 'masquerade: ' . $this->masquerade;
+
+ // Set the source path if an entity ID is not used.
+ if (is_numeric($entityIdOrPath) === TRUE) {
+ $entityId = intval($entityIdOrPath);
+ }
+ else {
+ // Make sure the path is safe. HTTP headers primarily support ASCII,
+ // while paths may include multi-byte characters. Use URL encoding
+ // to add them to the header.
+ $entityId = 0;
+ $headers[] = 'X-FolderShare-Source-Path: ' . rawurlencode($entityIdOrPath);
+ }
+
+ //
+ // Create URL
+ // ----------
+ // The GET URL includes:
+ // - The host name.
+ // - The web services path.
+ // - The entity ID (even if one is not used).
+ // - The serialization format.
+ $url = $this->hostName . self::URL_GET .
+ '/' . $entityId .
+ '?' . $this->getSerializationUrlQuery();
+
+ //
+ // Set up request
+ // --------------
+ // Get common CURL options and update them with specifics of this request.
+ //
+ // Authentication settings should be included for most operations.
+ //
+ // A CSRF token is not required.
+ $this->printVerbose("Issuing GET for $operation");
+ $options = $this->getCommonCurlOptions(TRUE);
+
+ $options[CURLOPT_URL] = $url;
+ $options[CURLOPT_HTTPGET] = TRUE;
+ $options[CURLOPT_HTTPHEADER] = $headers;
+
+ if ($this->serverVerified === FALSE) {
+ // This GET is the first one with a new server choice. Add an option
+ // to clear all cookies.
+ $options[CURLOPT_COOKIELIST] = "ALL";
+ }
+
+ curl_reset($this->curlSession);
+ curl_setopt_array($this->curlSession, $options);
+
+ //
+ // Issue GET
+ // ---------
+ // Issue an HTTP GET to the web server. There are two types of errors:
+ // - Communications errors reported by CURL as error numbers.
+ // - Web site errors reported as bad HTTP codes.
+ //
+ // The returned content varies depending upon the operation.
+ $content = curl_exec($this->curlSession);
+ $httpCode = curl_getinfo($this->curlSession, CURLINFO_RESPONSE_CODE);
+ $errno = curl_errno($this->curlSession);
+
+ if ($errno !== 0) {
+ // Communications error. Something went wrong with Curl or with
+ // its communications with the server.
+ $message = $this->getNiceCurlErrorMessage($errno);
+ $this->printVerbose(" CURL failed with error code $errno");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message, (-$errno));
+ }
+
+ // Decode JSON content.
+ if (empty($content) === FALSE) {
+ $content = json_decode($content, TRUE);
+ }
+
+ // Check for error codes. The following are standard HTTP success codes:
+ // - 200 = OK.
+ // - 201 = Created.
+ // - 202 = Accepted.
+ // - 203 = Non-authoritative information.
+ // - 204 = No content.
+ // - 205 = Reset content.
+ // - 206 = Partial content.
+ // - 207 = Multi-status.
+ // - 208 = Already reported.
+ // - 226 = IM used.
+ //
+ // A 200 is returned when there is content, and 204 when there isn't.
+ // Both are OK.
+ //
+ // All other standard success codes are unacceptable.
+ switch ($httpCode) {
+ case 200:
+ $this->printVerbose(" Request complete");
+ return $content;
+
+ case 204:
+ $this->printVerbose(" Request complete");
+ if (empty($content) === TRUE) {
+ return NULL;
+ }
+ return $content;
+
+ default:
+ if (isset($content['message']) === TRUE &&
+ empty($content['message']) === FALSE) {
+ $message = $content['message'];
+ }
+ else {
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message, $httpCode);
+ }
+ }
+
+ /**
+ * Deletes content on a web server.
+ *
+ * DELETE requests are used to delete a single entity, or a subtree,
+ * based upon the operation choice:
+ * - 'delete-file' deletes a single file.
+ * - 'delete-folder' deletes a single empty folder.
+ * - 'delete-folder-tree' deletes a single folder recursively.
+ * - 'delete-file-or-folder' deletes a single file or empty folder.
+ * - 'delete-file-or-folder-tree' deletes anything recursively.
+ *
+ * DELETE requests require a full folder path or a numeric entity ID
+ * to indicate the entity to delete.
+ *
+ * DELETE requests do not return content. On an error, an exception
+ * is thrown.
+ *
+ * @param string $operation
+ * The name of the requested operation (e.g. "delete-file").
+ * @param int|string $entityIdOrPath
+ * Some operations do not use this value.
+ * The numeric entity ID or string path for a remote file or folder.
+ *
+ * @return mixed
+ * Always empty.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The arguments are malformed.
+ * - The arguments are invalid, such as for non-existent items.
+ */
+ protected function httpDelete(string $operation, $entityIdOrPath) {
+ //
+ // Initialize
+ // ----------
+ // Check if HTTP DELETE is supported. This may issue a connection to the
+ // server to find out. If that fails, an exception is thrown.
+ if ($this->isHttpDeleteSupported() === FALSE) {
+ throw new \RuntimeException("The operation cannot be completed.\nThe host does not support requests to delete content.");
+ }
+
+ //
+ // Create headers
+ // --------------
+ // The headers specify the DELETE operation and the source path.
+ // There is no return format header because DELETE never returns
+ // anything except success or error codes.
+ $headers = [];
+
+ // Set the operation.
+ $headers[] = 'X-FolderShare-Delete-Operation: ' . $operation;
+
+ // Get the CSRF token. This may trigger a GET request to the server.
+ // If that fails, an exception is thrown.
+ $headers[] = 'X-CSRF-Token: ' . $this->getCsrfToken();
+
+ // Set the API key.
+ $headers[] = 'api-key: ' . $this->apikey;
+
+ // Set the masqueraded user.
+ $headers[] = 'masquerade: ' . $this->masquerade;
+
+ // Set the source path if an entity ID is not used.
+ if (is_numeric($entityIdOrPath) === TRUE) {
+ $entityId = intval($entityIdOrPath);
+ }
+ else {
+ // Make sure the path is safe. HTTP headers primarily support ASCII,
+ // while paths may include multi-byte characters. Use URL encoding
+ // to add them to the header.
+ $entityId = -1;
+ $headers[] = 'X-FolderShare-Source-Path: ' . rawurlencode($entityIdOrPath);
+ }
+
+ //
+ // Create URL
+ // ----------
+ // The DELETE URL includes:
+ // - The host name.
+ // - The web services path.
+ // - The serialization format.
+ // - An optional entity ID.
+ if ($entityId === (-1)) {
+ $url = $this->hostName . self::URL_DELETE .
+ '/?' . $this->getSerializationUrlQuery();
+ }
+ else {
+ $url = $this->hostName . self::URL_DELETE .
+ '?' . $this->getSerializationUrlQuery() .
+ '&id=' . $entityId;
+ }
+
+ //
+ // Set up request
+ // --------------
+ // Get common CURL options and update them with specifics of this request.
+ //
+ // Authentication settings should be included for most operations.
+ //
+ // A CSRF token is required.
+ $this->printVerbose("Issuing DELETE for $operation");
+ $options = $this->getCommonCurlOptions(TRUE);
+
+ $options[CURLOPT_URL] = $url;
+ $options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
+ $options[CURLOPT_HTTPHEADER] = $headers;
+
+ curl_reset($this->curlSession);
+ curl_setopt_array($this->curlSession, $options);
+
+ //
+ // Issue DELETE
+ // ------------
+ // Issue an HTTP DELETE to the web server. There are two types of errors:
+ // - Communications errors reported by CURL as error numbers.
+ // - Web site errors reported as bad HTTP codes.
+ //
+ // The returned content varies depending upon the operation.
+ $content = curl_exec($this->curlSession);
+ $httpCode = curl_getinfo($this->curlSession, CURLINFO_RESPONSE_CODE);
+ $errno = curl_errno($this->curlSession);
+
+ if ($errno !== 0) {
+ // Communications error. Something went wrong with Curl or with
+ // its communications with the server.
+ $message = $this->getNiceCurlErrorMessage($errno);
+ $this->printVerbose(" CURL failed with error code $errno");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ // Decode JSON content.
+ if (empty($content) === FALSE) {
+ $content = json_decode($content, TRUE);
+ }
+
+ // Check for error codes. The following are standard HTTP success codes:
+ // - 200 = OK.
+ // - 201 = Created.
+ // - 202 = Accepted.
+ // - 203 = Non-authoritative information.
+ // - 204 = No content.
+ // - 205 = Reset content.
+ // - 206 = Partial content.
+ // - 207 = Multi-status.
+ // - 208 = Already reported.
+ // - 226 = IM used.
+ //
+ // Since a DELETE never returns anything, only a 204 is valid.
+ //
+ // All other standard success codes are unacceptable.
+ switch ($httpCode) {
+ case 204:
+ $this->printVerbose(" Request complete");
+ if (empty($content) === TRUE) {
+ return NULL;
+ }
+ return $content;
+
+ default:
+ if (isset($content['message']) === TRUE &&
+ empty($content['message']) === FALSE) {
+ $message = $content['message'];
+ }
+ else {
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+ }
+
+ /**
+ * Posts new content to the web server.
+ *
+ * POST requests are used to create a single entity based upon the
+ * operation choice:
+ * - 'new-rootfolder' creates a new root folder.
+ * - 'new-folder' creates a new subfolder.
+ * - 'new-file' creates a new file.
+ * - 'new-media' creates a new media item.
+ *
+ * POST requests never support a numeric entity ID since the operation
+ * is creating an entity. The $path argument selects the location in
+ * which to create the new entity.
+ *
+ * @param string $operation
+ * The name of the requested operation.
+ * @param string $path
+ * The path to a remote parent folder into which to place a new entity.
+ * @param string $name
+ * The name of the new item being created.
+ * @param string $localPath
+ * (optional, default = '') The path to a local file to upload as an
+ * attachment to the POST.
+ *
+ * @return mixed
+ * Returns a variety of content types depending upon the operation.
+ * In most cases, the content returned is an array.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The arguments are malformed.
+ * - The arguments are invalid, such as for non-existent items.
+ */
+ protected function httpPost(
+ string $operation,
+ string $path,
+ string $name,
+ string $localPath = '') {
+
+ //
+ // Initialize
+ // ----------
+ // Check if HTTP POST is supported. This may issue a connection to the
+ // server to find out. If that fails, an exception is thrown.
+ if ($this->isHttpPostSupported() === FALSE) {
+ throw new \RuntimeException(
+ "The operation cannot be completed.\nThe host does not support requests to create or upload new content.");
+ }
+
+ //
+ // Create headers
+ // --------------
+ // The headers specify the POST operation and the destination path.
+ $headers = [];
+
+ // Set the operation.
+ $headers[] = 'X-FolderShare-Post-Operation: ' . $operation;
+
+ // Get the CSRF token. This may trigger a GET request to the server.
+ // If that fails, an exception is thrown.
+ $headers[] = 'X-CSRF-Token: ' . $this->getCsrfToken();
+
+ // Make sure the path is safe. HTTP headers primarily support ASCII,
+ // while paths may include multi-byte characters. Use URL encoding
+ // to add them to the header.
+ $headers[] = 'X-FolderShare-Destination-Path: ' . rawurlencode($path);
+
+ // Set the content disposition, which gives the name of the item
+ // to be created, whether it is a folder or a file.
+ $headers[] = 'Content-Disposition: attachment; filename="' . $name . '"';
+
+ // Set the content type, which is always a binary data stream. For
+ // new folders, this stream is empty. For uploaded files, this is the
+ // raw byte stream for the file.
+ $headers[] = "Content-Type: application/octet-stream";
+
+ // Set the API key.
+ $headers[] = 'api-key: ' . $this->apikey;
+
+ // Set the masqueraded user.
+ $headers[] = 'masquerade: ' . $this->masquerade;
+
+ //
+ // Create URL
+ // ----------
+ // The POST URL includes:
+ // - The host name.
+ // - The post web services path.
+ // - The return syntax.
+ $url = $this->hostName . self::URL_POST .
+ '?' . $this->getSerializationUrlQuery();
+
+ //
+ // Set up request
+ // --------------
+ // Get common CURL options and update them with specifics of this request.
+ //
+ // Authentication settings should be included for most operations.
+ //
+ // A CSRF token is required.
+ //
+ // The POST needs the file to attach, if given.
+ $this->printVerbose("Issuing POST for $operation");
+ $options = $this->getCommonCurlOptions(TRUE);
+
+ $options[CURLOPT_URL] = $url;
+ $options[CURLOPT_SAFE_UPLOAD] = TRUE;
+ $options[CURLOPT_HTTPHEADER] = $headers;
+ $options[CURLOPT_POST] = TRUE;
+ $options[CURLOPT_POSTFIELDS] = '';
+
+ if (empty($localPath) === FALSE) {
+ // A local file path has been given for a file to upload.
+ $options[CURLOPT_POSTFIELDS] = file_get_contents($localPath);
+ }
+
+ curl_reset($this->curlSession);
+ curl_setopt_array($this->curlSession, $options);
+
+ //
+ // Issue POST
+ // ----------
+ // Issue an HTTP POST to the web server. There are two types of errors:
+ // - Communications errors reported by CURL as error numbers.
+ // - Web site errors reported as bad HTTP codes.
+ //
+ // The returned content varies depending upon the operation.
+ $content = curl_exec($this->curlSession);
+ $httpCode = curl_getinfo($this->curlSession, CURLINFO_RESPONSE_CODE);
+ $errno = curl_errno($this->curlSession);
+
+ if ($errno !== 0) {
+ // Communications error. Something went wrong with Curl or with
+ // its communications with the server.
+ $message = $this->getNiceCurlErrorMessage($errno);
+ $this->printVerbose(" CURL failed with error code $errno");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ // Decode JSON content.
+ if (empty($content) === FALSE) {
+ $content = json_decode($content, TRUE);
+ }
+
+ // Check for error codes. The following are standard HTTP success codes:
+ // - 200 = OK.
+ // - 201 = Created.
+ // - 202 = Accepted.
+ // - 203 = Non-authoritative information.
+ // - 204 = No content.
+ // - 205 = Reset content.
+ // - 206 = Partial content.
+ // - 207 = Multi-status.
+ // - 208 = Already reported.
+ // - 226 = IM used.
+ //
+ // A POST always creates content, so all successful requests return 201.
+ //
+ // All other standard success codes are unacceptable.
+ switch ($httpCode) {
+ case 201:
+ $this->printVerbose(" Request complete");
+ if (empty($content) === TRUE) {
+ return NULL;
+ }
+ return $content;
+
+ default:
+ if (isset($content['message']) === TRUE &&
+ empty($content['message']) === FALSE) {
+ $message = $content['message'];
+ }
+ else {
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+ }
+
+ /**
+ * Updates content on the web server.
+ *
+ * PATCH requests are used to update a single entity based upon the
+ * operation choice:
+ * - 'update-entity' changes one or more entity fields.
+ * - 'update-sharing' changes an entity's sharing settings.
+ * - 'move-overwrite' moves/renames an entity, optionally overwriting.
+ * - 'move-no-overwrite' moves/renames an entity without overwriting.
+ *
+ * PATCH requests require a full folder path or a numeric entity ID
+ * to indicate the entity to delete.
+ *
+ * @param string $operation
+ * The name of the requested operation.
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file or folder.
+ * @param string $destinationPath
+ * (optional, default = "") The string path for the remote destination
+ * on move and copy operations. Other operations do not use this value.
+ * @param array $postFields
+ * (optional, default = []) The post fields in 'full' entity format
+ * for operations that need additional values.
+ *
+ * @return mixed
+ * Returns a variety of content types depending upon the operation.
+ * In most cases, the content returned is an array.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The arguments are malformed.
+ * - The arguments are invalid, such as for non-existent items.
+ */
+ protected function httpPatch(
+ string $operation,
+ string $entityIdOrPath,
+ string $destinationPath,
+ array $postFields = []) {
+
+ //
+ // Initialize
+ // ----------
+ // Check if HTTP POST is supported. This may issue a connection to the
+ // server to find out. If that fails, an exception is thrown.
+ if ($this->isHttpPostSupported() === FALSE) {
+ throw new \RuntimeException("The operation cannot be completed.\nThe host does not support requests to create or upload new content.");
+ }
+
+ //
+ // Create headers
+ // --------------
+ // The headers specify the POST operation and the source path, if any.
+ $headers = [];
+
+ // Set the operation.
+ $headers[] = 'X-FolderShare-Patch-Operation: ' . $operation;
+
+ // Get the CSRF token. This may trigger a GET request to the server.
+ // If that fails, an exception is thrown.
+ $headers[] = 'X-CSRF-Token: ' . $this->getCsrfToken();
+
+ // Set the API key.
+ $headers[] = 'api-key: ' . $this->apikey;
+
+ // Set the masqueraded user.
+ $headers[] = 'masquerade: ' . $this->masquerade;
+
+ // Set the source path if an entity ID is not used.
+ if (is_numeric($entityIdOrPath) === TRUE) {
+ $entityId = intval($entityIdOrPath);
+ }
+ else {
+ // Make sure the path is safe. HTTP headers primarily support ASCII,
+ // while paths may include multi-byte characters. Use URL encoding
+ // to add them to the header.
+ $entityId = -1;
+ $headers[] = 'X-FolderShare-Source-Path: ' . rawurlencode($entityIdOrPath);
+ }
+
+ // Set the destination path, if any.
+ if (empty($destinationPath) === FALSE) {
+ $headers[] = 'X-FolderShare-Destination-Path: ' . rawurlencode($destinationPath);
+ }
+
+ // POST fields are serialized as JSON.
+ $headers[] = 'Content-Type: application/json';
+
+ // Request the returned data in a specific format.
+ $headers[] = 'X-FolderShare-Return-Format: ' . $this->format;
+
+ // Process post fields, if provided.
+ if (empty($postFields) === TRUE) {
+ $postFields = $this->createDummyEntity();
+ }
+
+ $postText = json_encode($postFields);
+
+ //
+ // Create URL
+ // ----------
+ // The PATCH URL includes:
+ // - The host name.
+ // - The canonical web services path.
+ // - The serialization format.
+ // - The entity ID (if any).
+ if ($entityId === (-1)) {
+ $url = $this->hostName . self::URL_PATCH .
+ '/?' . $this->getSerializationUrlQuery();
+ }
+ else {
+ $url = $this->hostName . self::URL_PATCH .
+ '?' . $this->getSerializationUrlQuery() .
+ '&id=' . $entityId;
+ }
+
+ //
+ // Set up request
+ // --------------
+ // Get common CURL options and update them with specifics of this request.
+ //
+ // Authentication settings should be included for most operations.
+ //
+ // A CSRF token is required.
+ $this->printVerbose("Issuing PATCH for $operation");
+ $options = $this->getCommonCurlOptions(TRUE);
+
+ $options[CURLOPT_URL] = $url;
+ $options[CURLOPT_CUSTOMREQUEST] = 'PATCH';
+ $options[CURLOPT_SAFE_UPLOAD] = TRUE;
+ $options[CURLOPT_POSTFIELDS] = $postText;
+ $options[CURLOPT_HTTPHEADER] = $headers;
+
+ curl_reset($this->curlSession);
+ curl_setopt_array($this->curlSession, $options);
+
+ //
+ // Issue PATCH
+ // -----------
+ // Issue an HTTP PATCH to the web server. There are two types of errors:
+ // - Communications errors reported by CURL as error numbers.
+ // - Web site errors reported as bad HTTP codes.
+ //
+ // The returned content varies depending upon the operation.
+ $content = curl_exec($this->curlSession);
+ $httpCode = curl_getinfo($this->curlSession, CURLINFO_RESPONSE_CODE);
+ $errno = curl_errno($this->curlSession);
+
+ if ($errno !== 0) {
+ // Communications error. Something went wrong with Curl or with
+ // its communications with the server.
+ $message = $this->getNiceCurlErrorMessage($errno);
+ $this->printVerbose(" CURL failed with error code $errno");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ // Decode JSON content.
+ if (empty($content) === FALSE) {
+ $content = json_decode($content, TRUE);
+ }
+
+ // Check for error codes. The following are standard HTTP success codes:
+ // - 200 = OK.
+ // - 201 = Created.
+ // - 202 = Accepted.
+ // - 203 = Non-authoritative information.
+ // - 204 = No content.
+ // - 205 = Reset content.
+ // - 206 = Partial content.
+ // - 207 = Multi-status.
+ // - 208 = Already reported.
+ // - 226 = IM used.
+ //
+ // A PATCH may modify or create content, so 200 and 201 are acceptable.
+ //
+ // All other standard success codes are unacceptable.
+ switch ($httpCode) {
+ case 200:
+ $this->printVerbose(" Request complete");
+ return $content;
+
+ case 201:
+ $this->printVerbose(" Request complete");
+ if (empty($content) === TRUE) {
+ return NULL;
+ }
+ return $content;
+
+ default:
+ if (isset($content['message']) === TRUE &&
+ empty($content['message']) === FALSE) {
+ $message = $content['message'];
+ }
+ else {
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * GET requests.
+ *
+ * These methods issue low-level HTTP GET requests and return
+ * appropriate structured content.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Gets information about a file or folder.
+ *
+ * An array is returned that describes the item.
+ *
+ * The format of the returned content depends upon the currently selected
+ * return format. The content includes mandatory fields for each item,
+ * including its name, creation and modified dates, MIME type, kind,
+ * entity ID, and parent and root item IDs. The returned content may
+ * include additional fields if they are not empty, including the item's
+ * description, comments, etc.
+ *
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file or folder.
+ *
+ * @return array
+ * Returns an array of information describing the file or folder.
+ * The format of the information depends upon the current return format.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The path is malformed.
+ * - The item does not exist.
+ * - The user does not have necessary permissions.
+ *
+ * @see ::download()
+ */
+ public function getFileOrFolder($entityIdOrPath) {
+ //
+ // Validate
+ // --------
+ // If the path is a string, it cannot be empty. Further checking of
+ // the path, or of integer entity IDs, must be done on the server.
+ if (is_string($entityIdOrPath) === TRUE &&
+ empty($entityIdOrPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ return $this->httpGet("get-entity", $entityIdOrPath);
+ }
+
+ /**
+ * Gets information about a file or folder's parent folder.
+ *
+ * An array is returned that describes the item's parent folder.
+ *
+ * The format of the returned content depends upon the currently selected
+ * return format. The content includes mandatory fields for each item,
+ * including its name, creation and modified dates, MIME type, kind,
+ * entity ID, and parent and root item IDs. The returned content may
+ * include additional fields if they are not empty, including the item's
+ * description, comments, etc.
+ *
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file or folder
+ * for whome parent folder information is returned.
+ *
+ * @return array
+ * Returns an array of information describing the file or folder's
+ * parent folder. The format of the information depends upon the
+ * current return format.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The path is malformed.
+ * - The item does not exist.
+ * - The user does not have necessary permissions.
+ *
+ * @see ::download()
+ * @see ::getRootItem()
+ */
+ public function getParentFolder($entityIdOrPath) {
+ //
+ // Validate
+ // --------
+ // If the path is a string, it cannot be empty. Further checking of
+ // the path, or of integer entity IDs, must be done on the server.
+ if (is_string($entityIdOrPath) === TRUE &&
+ empty($entityIdOrPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ return $this->httpGet("get-parent", $entityIdOrPath);
+ }
+
+ /**
+ * Gets information about a file or folder's top-level (root) folder.
+ *
+ * An array is returned that describes the item's top-level (root) folder.
+ *
+ * The format of the returned content depends upon the currently selected
+ * return format. The content includes mandatory fields for each item,
+ * including its name, creation and modified dates, MIME type, kind,
+ * entity ID, and parent and root item IDs. The returned content may
+ * include additional fields if they are not empty, including the item's
+ * description, comments, etc.
+ *
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file or folder
+ * for whome root item information is returned.
+ *
+ * @return array
+ * Returns an array of information describing the file or folder's
+ * root item. The format of the information depends upon the
+ * current return format.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The path is malformed.
+ * - The item does not exist.
+ * - The user does not have necessary permissions.
+ *
+ * @see ::download()
+ * @see ::getParentFolder()
+ * @see ::getRootItems()
+ */
+ public function getRootItem($entityIdOrPath) {
+ //
+ // Validate
+ // --------
+ // If the path is a string, it cannot be empty. Further checking of
+ // the path, or of integer entity IDs, must be done on the server.
+ if (is_string($entityIdOrPath) === TRUE &&
+ empty($entityIdOrPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ return $this->httpGet("get-root", $entityIdOrPath);
+ }
+
+ /**
+ * Gets a list of ancestor folders for a file or folder.
+ *
+ * An array is returned that contains a list of ancestors, ordered from
+ * the top-level folder down to (but not including) the indicated item.
+ *
+ * The format of the returned content depends upon the currently selected
+ * return format. The content includes mandatory fields for each item,
+ * including its name, creation and modified dates, MIME type, kind,
+ * entity ID, and parent and root item IDs. The returned content may
+ * include additional fields if they are not empty, including the item's
+ * description, comments, etc.
+ *
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file or folder
+ * for whome ancestor information is returned.
+ *
+ * @return array
+ * Returns an array of information describing a list of the file or
+ * folder's ancestors. The format of the information depends upon
+ * the current return format.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The path is malformed.
+ * - The item does not exist.
+ * - The user does not have necessary permissions.
+ *
+ * @see ::download()
+ */
+ public function getAncestors($entityIdOrPath) {
+ //
+ // Validate
+ // --------
+ // If the path is a string, it cannot be empty. Further checking of
+ // the path, or of integer entity IDs, must be done on the server.
+ if (is_string($entityIdOrPath) === TRUE &&
+ empty($entityIdOrPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ return $this->httpGet("get-ancestors", $entityIdOrPath);
+ }
+
+ /**
+ * Gets a list of descendant (child) files and folders for a folder.
+ *
+ * An array is returned that contains a list of descendants, if any.
+ * If the item is a file, there are no descendants.
+ *
+ * The format of the returned content depends upon the currently selected
+ * return format. The content includes mandatory fields for each item,
+ * including its name, creation and modified dates, MIME type, kind,
+ * entity ID, and parent and root item IDs. The returned content may
+ * include additional fields if they are not empty, including the item's
+ * description, comments, etc.
+ *
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file or folder
+ * for whome descendant (child) folder information is returned.
+ *
+ * @return array
+ * Returns an array of information describing a list of the item's
+ * descendants. The format of the information depends upon the current
+ * return format.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The path is malformed.
+ * - The item does not exist.
+ * - The user does not have necessary permissions.
+ *
+ * @see ::download()
+ */
+ public function getDescendants($entityIdOrPath) {
+ //
+ // Validate
+ // --------
+ // If the path is a string, it cannot be empty. Further checking of
+ // the path, or of integer entity IDs, must be done on the server.
+ if (is_string($entityIdOrPath) === TRUE &&
+ empty($entityIdOrPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ return $this->httpGet("get-descendants", $entityIdOrPath);
+ }
+
+ /**
+ * Gets a list of top-level items.
+ *
+ * An array is returned that contains a list of top-level items in
+ * the selected "scheme". Recognized schemes include:
+ * - 'personal' returns the user's own top-level items and those shared
+ * with them.
+ * - 'public' returns all publically accessible top-level items.
+ *
+ * The format of the returned content depends upon the currently selected
+ * return format. The content includes mandatory fields for each item,
+ * including its name, creation and modified dates, MIME type, kind,
+ * entity ID, and parent and root item IDs. The returned content may
+ * include additional fields if they are not empty, including the item's
+ * description, comments, etc.
+ *
+ * @param string $scheme
+ * (optional, default = 'personal') The 'personal' or 'public' scheme
+ * that indicates the root list to return.
+ *
+ * @return array
+ * Returns an array of information describing a list of root items.
+ * The format of the information depends upon the current return format.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the scheme is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The user does not have necessary permissions.
+ * - The root item scheme is malformed.
+ * - The user's root item list is locked by another process.
+ *
+ * @see ::download()
+ * @see ::getRootItem()
+ */
+ public function getRootItems(string $scheme = 'personal') {
+ //
+ // Validate
+ // --------
+ // The scheme cannot be empty. Further scheme checking is left to
+ // the server.
+ if (empty($scheme) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ return $this->httpGet("get-descendants", $scheme . ':/');
+ }
+
+ /**
+ * Get a report on the user's FolderShare usage.
+ *
+ * An associative array is returned where keys and values indicate the
+ * current user's level of usage of the FolderShare virtual file system
+ * on the server. The following keys are expected:
+ * - 'nFolders' = the number of subfolders.
+ * - 'nFiles' = the number of files.
+ * - 'nBytes' = the amount of storage space used, in bytes.
+ *
+ * The returned array also includes the following keys:
+ * - 'host' = the host name.
+ * - 'user-id' = the numeric user ID of the authenticated user.
+ * - 'user-account-name' = the text account name of the authenticated user.
+ * - 'user-display-name' = the text display name of the authenticated user.
+ *
+ * There may be additional values, depending upon the site and the
+ * software installed at the site.
+ *
+ * @return array
+ * Returns a key-value array describing the user's usage of the module.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The user does not have necessary permissions.
+ */
+ public function getUsage() {
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ return $this->httpGet("get-usage", 0);
+ }
+
+ /**
+ * Get a report on the server's software configuraiton.
+ *
+ * An associative array is returned where keys and values are for
+ * server software parameters. The following keys are expected:
+ *
+ * - 'file-restrict-extensions' = TRUE or FALSE indicating if file name
+ * extension restrictions are enabled.
+ *
+ * - 'file-allowed-extensions' = a space-separated list of file name
+ * extensions checked, if extension restrictions are enabled.
+ *
+ * - 'sharing-allowed' = TRUE or FALSE indicating if folder sharing
+ * is enabled on the site.
+ *
+ * - 'sharing-allowed-with-anonymous' = TRUE or FALSE indicating if folder
+ * sharing with the "anonymous" user (the unauthenticated public) is
+ * enabled on the site.
+ *
+ * - 'serializer-formats' = a list of serialization formats configured
+ * for the method.
+ *
+ * - 'authentication-providers' = a list of authentication providers
+ * configured for the method.
+ *
+ * The returned array also includes the following keys:
+ * - 'host' = the host name.
+ *
+ * There may be additional values, depending upon the site and the
+ * software installed at the site.
+ *
+ * @return array
+ * Returns a key-value array of values describing the site and module
+ * configuration.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The user does not have necessary permissions.
+ *
+ * @see ::getHttpVerbs()
+ * @see ::getVersion()
+ */
+ public function getServerConfiguration() {
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ return $this->httpGet("get-configuration", 0);
+ }
+
+ /**
+ * Get a report on software version numbers.
+ *
+ * An associative array is returned where keys are software items and
+ * values are associative arrays that each have two keys and values:
+ * - 'name' = the human-readable name of the item.
+ * - 'version' = the version number of the item.
+ *
+ * If $doConnect is FALSE, the returned array only describes the client
+ * side software. Otherwise a connection to the server is made and the
+ * returned array describes both the client side and server side software.
+ *
+ * When a connection is made, the returned array also includes the
+ * following keys:
+ * - 'host' = the host name.
+ *
+ * @param bool $doConnect
+ * (optional, default = TRUE) When TRUE, the returned array includes
+ * client and server information. When FALSE, the array only includes
+ * client information and no communications with the server is done.
+ *
+ * @return array
+ * Returns a key-value array of software items and their names and
+ * version numbers.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The user does not have necessary permissions.
+ *
+ * @see ::getServerConfiguration()
+ */
+ public function getVersion(bool $doConnect = TRUE) {
+ //
+ // Create API versions
+ // -------------------
+ // For non-connecting requests, the API's information is all that is
+ // returned. For connecting requests, the API's information is prepended
+ // to whatever the server returns.
+ $curlVersion = curl_version();
+
+ $clientVersions = [
+ 'client' => [
+ 'foldershareconnect' => [
+ 'name' => 'FolderShareConnect client API',
+ 'version' => self::VERSION,
+ ],
+ 'clientphp' => [
+ 'name' => 'PHP',
+ 'version' => phpversion(),
+ ],
+ 'curl' => [
+ 'name' => 'cURL library',
+ 'version' => $curlVersion['version'],
+ ],
+ 'ssl' => [
+ 'name' => 'OpenSSL library',
+ 'version' => $curlVersion['ssl_version'],
+ ],
+ 'libz' => [
+ 'name' => 'Z compression library',
+ 'version' => $curlVersion['libz_version'],
+ ],
+ ],
+ ];
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ if ($doConnect === FALSE) {
+ return $clientVersions;
+ }
+
+ return array_merge($clientVersions, $this->httpGet("get-version", 0));
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * DELETE requests.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Deletes a file.
+ *
+ * This request will delete any type of file, including data files,
+ * image files, or media items.
+ *
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The user does not have necessary permissions.
+ * - The item does not exist.
+ * - The item is not a file.
+ * - The item is locked by another process.
+ *
+ * @see ::deleteFileOrFolder()
+ * @see ::deleteFolder()
+ */
+ public function deleteFile($entityIdOrPath) {
+ //
+ // Validate
+ // --------
+ // If the path is a string, it cannot be empty. Further checking of
+ // the path, or of integer entity IDs, must be done on the server.
+ if (is_string($entityIdOrPath) === TRUE &&
+ empty($entityIdOrPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ $this->httpDelete("delete-file", $entityIdOrPath);
+ }
+
+ /**
+ * Deletes a file or folder, optionally recursively.
+ *
+ * If the item indicated by the integer ID or path is a folder, and
+ * $recurse is FALSE, the folder must be empty. If $recurse is TRUE,
+ * non-empty folders will have their content deleted first, recursing
+ * as necessary through subfolders.
+ *
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote file or folder.
+ * @param bool $recurse
+ * (optional, default = FALSE) When TRUE, non-empty folders are deleted
+ * recursively, including any subfolders and their content. When FALSE,
+ * the operation only deletes files or empty folders.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The user does not have necessary permissions.
+ * - The item does not exist.
+ * - The item is locked by another process.
+ *
+ * @see ::deleteFile()
+ * @see ::deleteFolder()
+ */
+ public function deleteFileOrFolder($entityIdOrPath, bool $recurse = FALSE) {
+ //
+ // Validate
+ // --------
+ // If the path is a string, it cannot be empty. Further checking of
+ // the path, or of integer entity IDs, must be done on the server.
+ if (is_string($entityIdOrPath) === TRUE &&
+ empty($entityIdOrPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ if ($recurse === TRUE) {
+ $this->httpDelete("delete-file-or-folder-tree", $entityIdOrPath);
+ }
+ else {
+ $this->httpDelete("delete-file-or-folder", $entityIdOrPath);
+ }
+ }
+
+ /**
+ * Deletes a folder, optionally recursively.
+ *
+ * The item indicated by an integer ID or a path must be a folder.
+ * If $recurse is FALSE, the folder must be empty. If $recurse is TRUE,
+ * non-empty folders will have their content deleted first, recursing
+ * as necessary through subfolders.
+ *
+ * @param int|string $entityIdOrPath
+ * The numeric entity ID or string path for a remote folder.
+ * @param bool $recurse
+ * (optional, default = FALSE) When TRUE, non-empty folders are deleted
+ * recursively, including any subfolders and their content. When FALSE,
+ * the operation only deletes empty folders.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path is an empty string.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The user does not have necessary permissions.
+ * - The item does not exist.
+ * - The item is not a folder.
+ * - The item is locked by another process.
+ *
+ * @see ::deleteFile()
+ * @see ::deleteFileOrFolder()
+ */
+ public function deleteFolder($entityIdOrPath, bool $recurse = FALSE) {
+ //
+ // Validate
+ // --------
+ // If the path is a string, it cannot be empty. Further checking of
+ // the path, or of integer entity IDs, must be done on the server.
+ if (is_string($entityIdOrPath) === TRUE &&
+ empty($entityIdOrPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ if ($recurse === TRUE) {
+ $this->httpDelete("delete-folder-tree", $entityIdOrPath);
+ }
+ else {
+ $this->httpDelete("delete-folder", $entityIdOrPath);
+ }
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * POST requests.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Creates a new top-level folder.
+ *
+ * A new top-level folder is created with the selected name in the
+ * user's top-level folder list. An exception is thrown if the name
+ * is already in use.
+ *
+ * @param string $name
+ * The name of the new top-level folder.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the name is empty.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The user does not have necessary permissions.
+ * - The top level folder list is locked by another process.
+ * - The name is malformed.
+ * - The new name would cause a collision.
+ *
+ * @see ::newFolder()
+ */
+ public function newRootFolder(string $name) {
+ //
+ // Validate
+ // --------
+ // The name cannot be empty. Checking that the name is well formed
+ // must be done on the server.
+ if (empty($name) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_FOLDER_NAME);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ $this->httpPost(
+ "new-rootfolder",
+ "/",
+ $name);
+ }
+
+ /**
+ * Creates a new folder in a parent folder.
+ *
+ * A new folder with the selected name is created in the parent folder.
+ * An exception is thrown if the name is already in use.
+ *
+ * @param string $path
+ * The string path for a remote parent folder.
+ * @param string $name
+ * The name of the new folder.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path or name are empty.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The path is malformed.
+ * - The parent folder does not exist.
+ * - The user does not have necessary permissions.
+ * - The parent folder is locked by another process.
+ * - The name is malformed.
+ * - The new name would cause a collision.
+ *
+ * @see ::newRootFolder()
+ */
+ public function newFolder(string $path, string $name) {
+ //
+ // Validate
+ // --------
+ // The path and name cannot be empty. Checking that the path and name
+ // are well formed, and that the path refers to an existing folder must be
+ // done on the server.
+ if (empty($path) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ if (empty($name) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_FOLDER_NAME);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ $this->httpPost(
+ "new-folder",
+ $path,
+ $name);
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * PATCH requests.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Renames a file or folder.
+ *
+ * The file or folder is named without moving it. An exception is
+ * thrown if the new name is already in use.
+ *
+ * @param string $path
+ * The string path for a remote file or folder.
+ * @param string $name
+ * The new name for the item.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path or name are empty.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The path is malformed.
+ * - The item to rename does not exist.
+ * - The user does not have necessary permissions.
+ * - The item is locked by another process.
+ * - The name is malformed.
+ * - The rename would cause a collision.
+ *
+ * @see ::move()
+ */
+ public function rename(string $path, string $name) {
+ //
+ // Validate
+ // --------
+ // The path and name cannot be empty. Checking that the path and name
+ // are well formed, and that the path refers to an existing item must be
+ // done on the server.
+ if (empty($path) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ if (empty($name) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_NAME);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ $this->httpPatch(
+ "update-entity",
+ $path,
+ '',
+ [
+ 'name' => [
+ 0 => [
+ 'value' => $name,
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * Updates a file or folder field.
+ *
+ * The value for the selected field in the file or folder is updated
+ * to the new value. An exception is thrown if the field is not known
+ * or the value is not valid.
+ *
+ * Only simple fields may be updated using this method.
+ *
+ * @param string $path
+ * The string path for a remote file or folder.
+ * @param string $fieldName
+ * The name of a simple field in the file or folder.
+ * @param string $fieldValue
+ * The new value for the field.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the path or field name are empty.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - The path is malformed.
+ * - The item to rename does not exist.
+ * - The user does not have necessary permissions.
+ * - The item is locked by another process.
+ * - The name is malformed.
+ * - The rename would cause a collision.
+ *
+ * @see ::rename()
+ */
+ public function update(string $path, string $fieldName, string $fieldValue) {
+ //
+ // Validate
+ // --------
+ // The path and field name cannot be empty. Checking that the path,
+ // field name, and value are well formed, and that the path refers to an
+ // existing item must be done on the server.
+ if (empty($path) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ if (empty($fieldName) === TRUE) {
+ throw new \InvalidArgumentException("Empty field name.");
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ $this->httpPatch(
+ "update-entity",
+ $path,
+ '',
+ [
+ $fieldName => [
+ 0 => [
+ 'value' => $fieldValue,
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * Moves a file or folder to a new location.
+ *
+ * This method is modeled after the way the Linux/macOS/BSD "mv" command
+ * operates. It supports moving an item, renaming an item in place, or
+ * moving and renaming at the same time.
+ *
+ * The $fromPath must refer to an existing file or folder to be moved
+ * and/or renamed.
+ *
+ * The $toPath may be one of:
+ * - A "/" to refer to the top-level folder list.
+ * - A path to an existing file or folder.
+ * - A path to a non-existant item within an existing parent folder.
+ *
+ * If $toPath is "/", $fromPath must refer to a folder since files cannot be
+ * moved into "/". The moved folder will have the same name as in $fromPath.
+ * If there is already an item with the same name in "/", the move will
+ * fail unless $overwrite is TRUE.
+ *
+ * If $toPath refers to a non-existant item, then the item referred to
+ * by $fromPath will be moved into the $toPath's parent folder and
+ * renamed to use the last name on $toPath. If $toPath's parent folder
+ * is "/", then $fromPath must refer to a folder since files cannot be
+ * moved into "/".
+ *
+ * If $toPath refers to an existing folder, the file or folder referred
+ * to by $fromPath will be moved into the $toPath folder and retain its
+ * current name. If there is already an item with that name in the
+ * $toPath folder, the move will fail unless $overwrite is TRUE.
+ *
+ * If $toPath refers to an existing file, the move will fail unless
+ * $overwrite is TRUE. If overwrite is allowed, the item referred to by
+ * $fromPath will be moved into the $toPath item's parent folder and
+ * renamed to have th last name in $toPath. If $toPath's parent folder
+ * is "/", then $fromPath must refer to a folder since files cannot be
+ * moved into "/".
+ *
+ * In any case where an overwrite is required, if $overwrite is FALSE
+ * the operation will fail. Otherwise the item to overwrite will be
+ * deleted before the move and/or rename takes place.
+ *
+ * The user must have permission to modify the item referred to by $fromPath.
+ * They user must have permission to modify the folder into which the
+ * item will be placed. When moving a folder to "/", the user must have
+ * permission to create a new top-level folder. And when overwriting an
+ * existing item, the user must have permission to delete that item.
+ *
+ * @param string $fromPath
+ * The string path for a remote file or folder to move.
+ * @param string $toPath
+ * The string path to the remote parent folder to receive the moved item.
+ * Use "/" to move a folder to the root list.
+ * @param bool $overwrite
+ * (optional, default = FALSE) When TRUE, a move of an item into a folder
+ * that already has an item of the same name will overwrite that item.
+ * When FALSE, an error is generated instead.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - Either path is malformed.
+ * - The item to move/rename does not exist.
+ * - The destination folder does not exist.
+ * - The item is the wrong type for the destination.
+ * - The user does not have necessary permissions.
+ * - One of the item's involved is locked by another process.
+ * - The move/rename would cause a collision, but $overwrite is FALSE.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if either path is empty.
+ *
+ * @see ::rename()
+ */
+ public function move(string $fromPath, string $toPath, bool $overwrite = TRUE) {
+ //
+ // Validate
+ // --------
+ // Neither path can be empty. Checking that the paths are well formed
+ // and refer to appropriate entities must be done on the server.
+ if (empty($fromPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ if (empty($toPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_DESTINATION_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ if ($overwrite === TRUE) {
+ $this->httpPatch("move-overwrite", $fromPath, $toPath);
+ }
+ else {
+ $this->httpPatch("move-no-overwrite", $fromPath, $toPath);
+ }
+ }
+
+ /**
+ * Copies a file or folder to a new location.
+ *
+ * This method is modeled after the way the Linux/macOS/BSD "cp" command
+ * operates. It supports copying one or more items to a new destination.
+ *
+ * The $fromPath must refer to an existing file or folder to be copied
+ * and optionally renamed.
+ *
+ * The $toPath may be one of:
+ * - A "/" to refer to the top-level folder list.
+ * - A path to an existing file or folder.
+ * - A path to a non-existant item within an existing parent folder.
+ *
+ * If $toPath is "/", $fromPath must refer to a folder since files cannot be
+ * copied into "/". The copied folder will have the same name as in $fromPath.
+ * If there is already an item with the same name in "/", the copy will
+ * fail unless $overwrite is TRUE.
+ *
+ * If $toPath refers to a non-existant item, then the item referred to
+ * by $fromPath will be copied into the $toPath's parent folder and
+ * renamed to use the last name on $toPath. If $toPath's parent folder
+ * is "/", then $fromPath must refer to a folder since files cannot be
+ * copied into "/".
+ *
+ * If $toPath refers to an existing folder, the file or folder referred
+ * to by $fromPath will be copied into the $toPath folder and retain its
+ * current name. If there is already an item with that name in the
+ * $toPath folder, the copy will fail unless $overwrite is TRUE.
+ *
+ * If $toPath refers to an existing file, the copy will fail unless
+ * $overwrite is TRUE. If overwrite is allowed, the item referred to by
+ * $fromPath will be copied into the $toPath item's parent folder and
+ * renamed to have th last name in $toPath. If $toPath's parent folder
+ * is "/", then $fromPath must refer to a folder since files cannot be
+ * copied into "/".
+ *
+ * In any case where an overwrite is required, if $overwrite is FALSE
+ * the operation will fail. Otherwise the item to overwrite will be
+ * deleted before the copy takes place.
+ *
+ * The user must have permission to view the item referred to by $fromPath.
+ * They user must have permission to modify the folder into which the
+ * item will be placed, and permission to create the new item there.
+ * When copying a folder to "/", the user must have permission to create
+ * a new top-level folder. And when overwriting an existing item, the
+ * user must have permission to delete that item.
+ *
+ * @param string $fromPath
+ * The string path for a remote file or folder.
+ * @param string $toPath
+ * The string path to the remote parent folder to receive the copied item.
+ * Use "/" to copy a folder to the root list.
+ * @param bool $overwrite
+ * (optional, default = FALSE) When TRUE, a copy of an item into a folder
+ * that already has an item of the same name will overwrite that item.
+ * When FALSE, an error is generated instead.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - Either path is malformed.
+ * - The item to copy does not exist.
+ * - The destination folder does not exist.
+ * - The item is the wrong type for the destination.
+ * - The user does not have necessary permissions.
+ * - One of the item's involved is locked by another process.
+ * - The copy would cause a collision, but $overwrite is FALSE.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if either path is empty.
+ *
+ * @see ::move()
+ */
+ public function copy(string $fromPath, string $toPath, bool $overwrite = TRUE) {
+ //
+ // Validate
+ // --------
+ // Neither path can be empty. Checking that the paths are well formed
+ // and refer to appropriate entities must be done on the server.
+ if (empty($fromPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ if (empty($toPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_DESTINATION_PATH);
+ }
+
+ //
+ // Execute
+ // -------
+ // Issue the request.
+ if ($overwrite === TRUE) {
+ $this->httpPatch("copy-overwrite", $fromPath, $toPath);
+ }
+ else {
+ $this->httpPatch("copy-no-overwrite", $fromPath, $toPath);
+ }
+ }
+
+ /**
+ * Downloads a remote file or folder to a local file or folder.
+ *
+ * @param string $remotePath
+ * The string path for a remote file or folder.
+ * @param string $localPath
+ * The string path for a local file or folder.
+ * @param bool $overwrite
+ * (optional, default = FALSE) When TRUE and $localPath refers to an
+ * existing file, the file will be deleted before the download is done.
+ * When FALSE, an existing local file will cause an exception.
+ *
+ * @return string
+ * Returns the absolute path to the newly downloaded file. If $localPath
+ * is a folder, then the downloaded file path will start with that path,
+ * followed by the file name as returned by the server.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - Either path is malformed.
+ * - The item to download does not exist.
+ * - The item to download is not a file or folder.
+ * - The user does not have necessary permissions.
+ * - One of the item's involved is locked by another process.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception on a client error, including the following
+ * conditions:
+ * - The remote path is empty.
+ * - The local path is empty.
+ * - The local path does not indicate a folder that exists.
+ * - The local path does not indicate a folder that can be written to.
+ * - The local path indicates a file that already exists.
+ *
+ * @see ::move()
+ */
+ public function download(
+ string $remotePath,
+ string $localPath,
+ bool $overwrite = FALSE) {
+ //
+ // Validate
+ // --------
+ // Neither path can be empty. Checking that the remote path is well formed
+ // and refers to an appropriate entity must be done on the server.
+ if (empty($remotePath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ if (empty($localPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_LOCAL_PATH);
+ }
+
+ //
+ // Create a temporary local file
+ // -----------------------------
+ // The downloaded data will be placed into a new local file that must
+ // be created now. The open file handle is then passed to CURL.
+ //
+ // If the local path is for a directory, then the local file name will
+ // be a temporary name. At the end of the file download the temporary
+ // name will be changed to the name returned by the server.
+ $cleanLocalPath = realpath($localPath);
+ if (is_dir($cleanLocalPath) === TRUE) {
+ // Create a temporary file path in the directory.
+ $tmpLocalPath = tempnam($cleanLocalPath, 'foldershare');
+ $renameFile = TRUE;
+ }
+ elseif (is_file($cleanLocalPath) === TRUE) {
+ // The file already exists.
+ if ($overwrite === FALSE) {
+ throw new \InvalidArgumentException(
+ "A local file with that name already exists.");
+ }
+
+ if (@unlink($cleanLocalPath) === FALSE) {
+ throw new \InvalidArgumentException(
+ "A local file with that name already exists and cannot be overwritten.");
+ }
+
+ $tmpLocalPath = $cleanLocalPath;
+ $renameFile = FALSE;
+ }
+ else {
+ // The path appears to be a name for a new file that doesn't exist yet.
+ $tmpLocalPath = $cleanLocalPath;
+ $renameFile = FALSE;
+ }
+
+ // Create and open the file so that CURL can save attached data into it.
+ $tmpLocalFile = @fopen($tmpLocalPath, 'wb');
+ if ($tmpLocalFile === FALSE) {
+ throw new \InvalidArgumentException(
+ "Cannot create a local file to receive the remote file contents.");
+ }
+
+ //
+ // Create headers
+ // --------------
+ // The headers specify the GET operation and the source path, if any.
+ $headers = [];
+
+ // Set the operation.
+ $headers[] = 'X-FolderShare-Get-Operation: download';
+
+ // Make sure the path is safe. HTTP headers primarily support ASCII,
+ // while paths may include multi-byte characters. Use URL encoding
+ // to add them to the header.
+ $headers[] = 'X-FolderShare-Source-Path: ' . rawurlencode($remotePath);
+
+ // Set the API key.
+ $headers[] = 'api-key: ' . $this->apikey;
+
+ // Set the masqueraded user.
+ $headers[] = 'masquerade: ' . $this->masquerade;
+
+ //
+ // Create URL
+ // ----------
+ // The GET URL includes:
+ // - The host name.
+ // - The canonical web services path.
+ // - The bogus entity ID (even though one is not used).
+ // - The return syntax.
+ $url = $this->hostName . self::URL_GET .
+ '/0?' . $this->getSerializationUrlQuery();
+
+ //
+ // Set up request
+ // --------------
+ // Get common CURL options and update them with specifics of this request.
+ //
+ // Authentication settings should be included.
+ //
+ // A CSRF token is not required.
+ $this->printVerbose("Issuing GET for download");
+ $options = $this->getCommonCurlOptions(TRUE);
+
+ $options[CURLOPT_URL] = $url;
+ $options[CURLOPT_HTTPGET] = TRUE;
+ $options[CURLOPT_FILE] = $tmpLocalFile;
+ $options[CURLOPT_HTTPHEADER] = $headers;
+ $options[CURLOPT_HEADERFUNCTION] = [
+ $this,
+ 'downloadHeaderCallback',
+ ];
+
+ curl_reset($this->curlSession);
+ curl_setopt_array($this->curlSession, $options);
+
+ //
+ // Issue GET
+ // ---------
+ // Issue an HTTP GET to the web server. There are two types of errors:
+ // - Communications errors reported by CURL as error numbers.
+ // - Web site errors reported as bad HTTP codes.
+ //
+ // The returned content varies depending upon the operation.
+ $this->downloadFilename = '';
+
+ $content = curl_exec($this->curlSession);
+ $httpCode = curl_getinfo($this->curlSession, CURLINFO_RESPONSE_CODE);
+ $errno = curl_errno($this->curlSession);
+
+ // Close the temp file. It may or may not have received anything useful.
+ fclose($tmpLocalFile);
+
+ if ($errno !== 0) {
+ // Communications error. Something went wrong with Curl or with
+ // its communications with the server.
+ @unlink($tmpLocalPath);
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ // Check for error codes. The following are standard HTTP success codes:
+ // - 200 = OK.
+ // - 201 = Created.
+ // - 202 = Accepted.
+ // - 203 = Non-authoritative information.
+ // - 204 = No content.
+ // - 205 = Reset content.
+ // - 206 = Partial content.
+ // - 207 = Multi-status.
+ // - 208 = Already reported.
+ // - 226 = IM used.
+ //
+ // A 200 is returned when there is a file.
+ //
+ // All other standard success codes are unacceptable.
+ switch ($httpCode) {
+ case 200:
+ break;
+
+ default:
+ // On an error, the error message has been written to the temp file.
+ // Read it in, delete the file, and parse the message (if any).
+ $content = file_get_contents($tmpLocalPath);
+ @unlink($tmpLocalPath);
+
+ if ($content !== FALSE) {
+ $content = json_decode($content, TRUE);
+ }
+
+ if (empty($content) === FALSE &&
+ isset($content['message']) === TRUE &&
+ empty($content['message']) === FALSE) {
+ $message = $content['message'];
+ }
+ else {
+ $message = $this->getNiceHttpErrorMessage($httpCode);
+ }
+
+ $this->printVerbose(" Server failed with HTTP code $httpCode");
+ $this->printVerbose(" $message");
+ throw new \RuntimeException($message);
+ }
+
+ // Everything worked. Rename the temp file using the file name from
+ // the HTTP header.
+ //
+ // Using the HTTP header's name is necessary if the download is for
+ // a folder. The server automatically ZIPs that folder into a file
+ // and sends the file. The name of that ZIP file is what we want for
+ // the name of the final file, not the name of the directory.
+ if ($renameFile === TRUE) {
+ $finalPath = $cleanLocalPath . '/' . $this->downloadFilename;
+ rename($tmpLocalPath, $finalPath);
+ }
+ else {
+ $finalPath = $cleanLocalPath;
+ }
+
+ $this->printVerbose(" Request complete");
+ return $finalPath;
+ }
+
+ /**
+ * Responds to an HTTP header receipt during a file download.
+ *
+ * This method is called by CURL while downloading a file. Each call
+ * passes a line from the incoming HTTP header. The line is parsed
+ * to look for the "Content-Disposition", which includes the name
+ * of the file being downloaded. This name is then saved for later use.
+ *
+ * @param resource $curlSession
+ * The resource for the current CURL session.
+ * @param string $line
+ * The HTTP header line to parse.
+ *
+ * @return int
+ * Returns the number of bytes in the incoming HTTP header line.
+ */
+ private function downloadHeaderCallback($curlSession, $line) {
+ // Get the length (in bytes) of the incoming line. This must be
+ // returned when the function is done.
+ $lineBytes = strlen($line);
+
+ // Split the line to get the header key and value.
+ $parts = mb_split(':', $line, 2);
+ if (count($parts) !== 2) {
+ return $lineBytes;
+ }
+
+ // We only want the content disposition header line.
+ if ($parts[0] !== 'Content-Disposition') {
+ return $lineBytes;
+ }
+
+ // Extract the file name from the line, if there is one.
+ $found = [];
+ if (mb_ereg('filename="(.*)"', $parts[1], $found) === FALSE) {
+ return $lineBytes;
+ }
+
+ // Save the file name to use at the end of the download.
+ $this->downloadFilename = $found[1];
+ return $lineBytes;
+ }
+
+ /**
+ * Uploads a local file to a remote file.
+ *
+ * The local path must refer to a file, not a directory. Directory uploading
+ * is not yet supported.
+ *
+ * @param string $localPath
+ * The string path for a local file or folder. The file or folder must
+ * exist and be readable.
+ * @param string $remotePath
+ * The string path for a remote folder into which to upload items.
+ * The remote folder must exist. "/" refers to the user's top-level
+ * list (a.k.a. "rootlist").
+ * @param callable $callback
+ * (optional, default = NULL) A function to call to announce the start
+ * of each file upload or folder creation. This may be used for verbose
+ * output that lists each item as it is uploaded.
+ *
+ * @throws \RuntimeException
+ * Throws an exception on a communications or server error, including
+ * the following conditions:
+ * - The host cannot be contacted, such a with a bad host name.
+ * - The host refuses contact, such as with bad authentication credentials.
+ * - The host does not support the operation.
+ * - Either path is malformed.
+ * - The item to upload does not exist.
+ * - The user does not have necessary permissions to create a file.
+ * - One of the item's involved is locked by another process.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception on a client error, including the following
+ * conditions:
+ * - The remote path is empty.
+ * - The local path is empty.
+ * - The local path does not indicate a file or folder that exists.
+ * - The local path does not indicate a file or folder that can be read.
+ * - The local path does not indicate a regular file or folder.
+ */
+ public function upload(
+ string $localPath,
+ string $remotePath,
+ callable $callback = NULL) {
+
+ //
+ // Validate
+ // --------
+ // - Neither path can be empty.
+ // - The local path must refer to a file that exists.
+ if (empty($remotePath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_REMOTE_PATH);
+ }
+
+ if (empty($localPath) === TRUE) {
+ throw new \InvalidArgumentException(self::ERROR_EMPTY_LOCAL_PATH);
+ }
+
+ // Convert the local path into an absolute path, expanding any
+ // symbolic links, "/./", "/../", etc. If this fails, the path is bad
+ // and may include non-existant directories or a non-existant file.
+ $localRealPath = realpath($localPath);
+ if ($localRealPath === FALSE) {
+ // Use the path as given in the error message, not the absolute path.
+ throw new \InvalidArgumentException(
+ "Local \"$localPath\" could not be found to upload.");
+ }
+
+ //
+ // Execute single file upload
+ // --------------------------
+ // When the local path is for a file, upload it.
+ if (is_file($localRealPath) === TRUE ||
+ is_link($localRealPath) === TRUE) {
+ if (is_readable($localRealPath) === FALSE) {
+ // Use the path as given in the error message, not the absolute path.
+ throw new \InvalidArgumentException(
+ "Local \"$localPath\" could not be read to upload.");
+ }
+
+ if ($callback !== NULL) {
+ // Use the path as given in the callback, not the absolute path.
+ $callback('upload', $localPath, $remotePath);
+ }
+
+ // Use the absolute path in the POST. But get the name from the
+ // path as given. This insures the file name is the name the user
+ // sees, not whatever a symlink might point to.
+ $newName = basename($localPath);
+
+ $this->httpPost(
+ ($remotePath === '/') ? 'new-rootfile' : 'new-file',
+ $remotePath,
+ $newName,
+ $localRealPath);
+ return;
+ }
+
+ // If the real path is not for a file (above) or a directory (below), then
+ // it must point to a special device file, or something else we cannot
+ // upload.
+ if (is_dir($localRealPath) === FALSE) {
+ // Use the path as given in the error message, not the absolute path.
+ $ft = filetype($localRealPath);
+ throw new \InvalidArgumentException(
+ "Local \"$localPath\" has an unknown file type: $ft");
+ }
+
+ //
+ // Build upload list
+ // -----------------
+ // When the local path is for a directory, traverse the directory tree
+ // to build up a list of folders to create and files to upload.
+ //
+ // The directory to upload has a path of the form "/A/B/C" if absolute,
+ // or "A/B/C" if relative. In both cases it names the top directory
+ // of a directory tree to upload. The uploaded items all need remote
+ // paths that replace "/A/B/C" or "A/B/C" with the top remote folder.
+ //
+ // For instance, let a local directory be "local/stuff" and it contains a
+ // file "local/stuff/myfile.txt". The upload remote folder is "/remote".
+ // We need to:
+ // - Create a remote "/remote/stuff" folder.
+ // - Upload "local/stuff/myfile.txt" to become "/remote/stuff/myfile.txt".
+ //
+ // Every local file uploaded needs to have "local/" stripped off the
+ // front of its path, and "/remote/" prepended back on to create the
+ // remote path.
+ //
+ // Get first part of the local path to strip off.
+ $stripPath = dirname($localPath);
+ if ($stripPath === '.') {
+ $stripPathLength = 0;
+ }
+ else {
+ $stripPathLength = mb_strlen($stripPath) + 1;
+ }
+
+ // Loop over local directories in the directory tree and assemble a
+ // list of folders to create and files to upload.
+ $foldersToCreate = [];
+ $filesToUpload = [];
+ $pendingDirectories = [$localPath];
+
+ // Remote paths use '/' as separators.
+ if ($remotePath !== '/') {
+ $remotePath .= '/';
+ }
+
+ while (empty($pendingDirectories) === FALSE) {
+ // Pop the pending queue and add the directory's local path to the
+ // folder list.
+ $pendingLocalPath = array_shift($pendingDirectories);
+
+ // Strip off the leading part of the local path so we can build a
+ // remote path.
+ if ($stripPathLength === 0) {
+ // Nothing to strip off.
+ $pendingRemotePath = $remotePath . $pendingLocalPath;
+ }
+ else {
+ $pendingRemotePath = $remotePath .
+ mb_substr($pendingLocalPath, $stripPathLength);
+ }
+
+ if (is_readable($pendingLocalPath) === FALSE) {
+ // Use the path as given in the error message, not the absolute path.
+ throw new \InvalidArgumentException(
+ "Local \"$pendingLocalPath\" could not be read to upload.");
+ }
+
+ $foldersToCreate[] = [
+ 'localPath' => $pendingLocalPath,
+ 'remotePath' => $pendingRemotePath,
+ ];
+
+ // Open the directory and add each of its files to the upload list,
+ // and each of its subdirectories to the pending list.
+ $d = opendir($pendingLocalPath);
+ while (($f = readdir($d)) !== FALSE) {
+ // Skip '.' and '..' on Linux/macOS/BSD-style systems.
+ if ($f === '.' || $f === '..') {
+ continue;
+ }
+
+ $subLocalPath = $pendingLocalPath . DIRECTORY_SEPARATOR . $f;
+ $subRemotePath = $pendingRemotePath . '/' . $f;
+
+ if (is_file($subLocalPath) === TRUE ||
+ is_link($subLocalPath) === TRUE) {
+ if (is_readable($subLocalPath) === FALSE) {
+ throw new \InvalidArgumentException(
+ "Local \"$subLocalPath\" could not be read to upload.");
+ }
+
+ $filesToUpload[] = [
+ 'localPath' => $subLocalPath,
+ 'remotePath' => $subRemotePath,
+ ];
+ }
+ else {
+ $pendingDirectories[] = $subLocalPath;
+ }
+ }
+
+ closedir($d);
+ }
+
+ //
+ // Execute multiple folder creations
+ // ---------------------------------
+ // Loop over the list of folders to create and create each one.
+ foreach ($foldersToCreate as $f) {
+ $folderLocalPath = $f['localPath'];
+ $folderRemotePath = $f['remotePath'];
+
+ if ($callback !== NULL) {
+ $callback('newfolder', $folderLocalPath, $folderRemotePath);
+ }
+
+ $parentPath = dirname($folderRemotePath);
+ $newName = basename($folderRemotePath);
+
+ $this->httpPost(
+ ($parentPath === '/') ? 'new-rootfolder' : "new-folder",
+ $parentPath,
+ $newName);
+ }
+
+ //
+ // Execute multiple file uploads
+ // -----------------------------
+ // Loop over the list of files to upload and upload each one.
+ foreach ($filesToUpload as $f) {
+ $fileLocalPath = $f['localPath'];
+ $fileRemotePath = $f['remotePath'];
+
+ if ($callback !== NULL) {
+ $callback('upload', $fileLocalPath, $fileRemotePath);
+ }
+
+ $parentPath = dirname($fileRemotePath);
+ $newName = basename($fileRemotePath);
+
+ $this->httpPost(
+ ($parentPath === '/') ? 'new-rootfile' : 'new-file',
+ $parentPath,
+ $newName,
+ realpath($fileLocalPath));
+ }
+ }
+
+}
+
+
+/**
+ * Provides static functions that assist in specialized content formatting.
+ *
+ * Intended as a companion class to FolderShareConnect, this class provides
+ * optional formatting utility functions to present returned content in
+ * special ways.
+ */
+class FolderShareFormat {
+
+ /*--------------------------------------------------------------------
+ *
+ * Key-value formatting.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Formats a nested key-value array as text.
+ *
+ * The keys and values of the array are returned as multi-line text in
+ * two columns holding the key and value. Rows are optionally indented.
+ *
+ * Nested arrays are handled by recursing.
+ *
+ * @param array $response
+ * The key-value response to convert to text.
+ * @param string $indent
+ * (optional, default = ' ') The indent for each key-value line in
+ * the result.
+ *
+ * @return string
+ * The string representation.
+ */
+ public static function formatAsText(
+ array $response,
+ string $indent = '') {
+
+ //
+ // Validate
+ // --------
+ // It is not an error to try and format an empty response array.
+ // It could simply mean that there is no response.
+ if (empty($response) === TRUE) {
+ return '';
+ }
+
+ //
+ // Prepare
+ // -------
+ // Make a first pass through all keys to find the longest key.
+ // This will become the column width below.
+ $maxLength = 0;
+ foreach (array_keys($response) as $key) {
+ $length = mb_strlen($key);
+ if ($length > $maxLength) {
+ $maxLength = $length;
+ }
+ }
+
+ // Add one for a ':'.
+ ++$maxLength;
+
+ //
+ // Generate output
+ // ---------------
+ // Make a second pass to format everything. Use the maximum key length
+ // to set a uniform field size for outputing key names.
+ $format = $indent . '%-' . $maxLength . "s %s\n";
+ $text = '';
+ foreach ($response as $key => $value) {
+ // If the value is a scalar, convert it to a string and output.
+ if (is_scalar($value) === TRUE) {
+ $text .= sprintf($format, $key . ':', $value);
+ continue;
+ }
+
+ if (empty($value) === TRUE) {
+ $text .= sprintf($format, $key . ':', '');
+ continue;
+ }
+
+ // Otherwise the value must be an array. Sweep through the array to
+ // see if all of its values are scalars and its keys are numeric.
+ // This is the case for simple lists.
+ $simpleList = TRUE;
+ foreach ($value as $k => $v) {
+ if (is_int($k) === FALSE || is_scalar($v) === FALSE) {
+ $simpleList = FALSE;
+ break;
+ }
+ }
+
+ // If the array is a simple list, format the array as a comma-separated
+ // list.
+ if ($simpleList === TRUE) {
+ $text .= sprintf($format, $key . ':', implode(', ', $value));
+ continue;
+ }
+
+ // Otherwise recurse.
+ $indentMore = $indent . ' ';
+ $text .= sprintf($format, $key . ':', '');
+ $text .= self::formatAsText($value, $indentMore);
+ }
+
+ return $text;
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Linux-style formatting.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Formats a single entity similar to the output of a Linux 'stat' command.
+ *
+ * The Linux 'stat' command prints information about a file, directory,
+ * or device. It has two primary formats:
+ *
+ * - (default) A full format shows multiple lines of text with key-value
+ * pairs giving the file name, size, permissions, etc.
+ *
+ * - A terse format shows a single line of text that condenses the most
+ * important values, including the file name, size, permissions, etc.
+ *
+ * This method takes 'stat' key-value data from FolderShare and formats
+ * it in a full-like or terse-like output that includes essential values
+ * for a FolderShare entity. It does not support format control arguments.
+ *
+ * Linux, BSD, macOS, and others all have 'stat' commands, but their
+ * formats differ. This method outputs in a format similar to Linux.
+ *
+ * The Linux 'stat' output is not particularly user-friendly. Linux users
+ * are more likely to be familiar with 'ls' output, which provides similar
+ * information.
+ *
+ * <B>Full mode (default)</B><BR>
+ * The full multi-line output of Linux 'stat' includes:
+ * - File name.
+ * - Size.
+ * - Number of blocks.
+ * - IO block size.
+ * - File type (e.g. "regular file" vs. "device").
+ * - Device.
+ * - Inode number.
+ * - Number of hard links.
+ * - Permissions mode as hex and text.
+ * - User as ID and account name.
+ * - Group as ID and group name.
+ * - Time of last access as text.
+ * - Time of last modification as text.
+ * - Time of last status change as text.
+ *
+ * Some of these values have FolderShare equivalents, while others do not.
+ * This method drops these values:
+ * - Number of blocks.
+ * - IO block size.
+ * - Number of hard links.
+ * - Group as ID and group name.
+ * - Time of last access as text.
+ *
+ * And replaces these values:
+ * - File type replaced with MIME type.
+ * - Device replaced with host name.
+ * - Inode number replaced with entity ID.
+ * - Time of last status change replaced with creation date.
+ *
+ * This method also adds a few fields:
+ * - Full path.
+ * - Parent entity ID.
+ * - Root entity ID.
+ *
+ * This method also slightly adjusts the Linux 'stat' names to be more
+ * meaningful here:
+ * - "Name" is used instead of the misleading "File", which Linux uses
+ * even for things that are not files.
+ * - "Mime type" is used instead of the Linux "FileType".
+ *
+ * The output of this method looks like:
+ * <pre>
+ * Path: "PATH"
+ * Name: "FILENAME"
+ * Size: SIZE Mime type: TYPE
+ * Host: HOSTNAME ID: ID ParentID: ID RootID: ID
+ * Access: SHARING Uid: (UID/ACCOUNT)
+ * Modify: DATE
+ * Create: DATE
+ * </pre>
+ *
+ * <B>Terse mode</B><BR>
+ * If the $terseMode argument is TRUE, a terse mode is returned instead
+ * of the full version above.
+ *
+ * The terse output of Linux 'stat' is an alias for the format string:
+ * - %n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %W %o
+ * where:
+ * - %n = file name.
+ * - %s = size.
+ * - %b = number of blocks.
+ * - %f = raw permission mode in hex.
+ * - %u = user ID.
+ * - %g = group ID.
+ * - %D = device number in hex.
+ * - %i = inode number.
+ * - %h = number of hard links.
+ * - %t = major device type in hex.
+ * - %T = minor device type in hex.
+ * - %X = time of last access as a timestamp.
+ * - %Y = time of last modification as a timestamp.
+ * - %Z = time of last status change as a timestamp.
+ * - %o = optimal I/O transfer size hint.
+ *
+ * Some of these values have FolderShare equivalents, while others do not.
+ * Rather than reduce the line to just the FolderShare values, this method
+ * retains the columns of the Linux terse mode, but shows a '-' if the
+ * value is not relevant.
+ *
+ * This method shows the following values:
+ * - %n = file name.
+ * - %s = size.
+ * - %b = -.
+ * - %f = sharing status.
+ * - %u = user ID.
+ * - %g = -.
+ * - %D = host name.
+ * - %i = entity ID.
+ * - %h = -.
+ * - %t = -.
+ * - %T = -.
+ * - %X = -.
+ * - %Y = time of last modification as a timestamp.
+ * - %Z = time of creation as a timestamp.
+ * - %o = -.
+ *
+ * @param array $response
+ * The key-value array response when querying a file or folder.
+ * @param bool $terseMode
+ * (optional, default = FALSE) When TRUE, a terse one-line form of
+ * the information is created. Otherwise a multi-line form is created.
+ *
+ * @return string
+ * Returns a text representation of the response.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the $response array is not for an entity or
+ * list of entities.
+ */
+ public static function formatAsLinuxStat(
+ array $response,
+ bool $terseMode = FALSE) {
+ //
+ // Validate
+ // --------
+ // Make sure the given server response is key-value data for
+ // a single entity or a list of entities.
+ if (empty($response) === TRUE) {
+ throw new \InvalidArgumentException(
+ "Programmer error: Empty response array when formatting as 'stat'.");
+ }
+
+ if (isset($response['path']) === FALSE) {
+ // The array is not for a single entity. Is it for a list of entities?
+ foreach ($response as $r) {
+ if (isset($r['path']) === FALSE) {
+ throw new \InvalidArgumentException(
+ "Programmer error: Response array is not in key-value form when formatting as 'stat'.");
+ }
+ }
+
+ // Recurse to build the output for a list of entities.
+ $text = '';
+ foreach ($response as $r) {
+ $text .= self::formatAsLinuxStat($r, $terseMode);
+ }
+
+ return $text;
+ }
+
+ //
+ // Generate output
+ // ---------------
+ // Extract and format relevant values.
+ //
+ // The 'size' may not be present if a entity does not yet have a size
+ // computed.
+ //
+ // The 'id' may not be present if the entity is a temporary false entity
+ // for "/" or some other virtual entity.
+ //
+ // All other values should be present.
+ //
+ // Entity fields.
+ $size = (isset($response['size']) === TRUE) ?
+ (string) $response['size'] : '0';
+ $mimeType = (isset($response['mime']) === TRUE) ?
+ $response['mime'] : '-';
+ $id = (isset($response['id']) === TRUE) ?
+ (string) $response['id'] : '-';
+ $parentId = (isset($response['parentid']) === TRUE) ?
+ (string) $response['parentid'] : '-';
+ $rootId = (isset($response['rootid']) === TRUE) ?
+ (string) $response['rootid'] : '-';
+ $modified = (isset($response['changed']) === TRUE) ?
+ $response['changed'] : '-';
+ $created = (isset($response['created']) === TRUE) ?
+ $response['created'] : '-';
+
+ // Virtual fields.
+ $path = $response['path'];
+ $permission = $response['sharing-status'];
+ $uid = $response['user-id'];
+ $accountName = $response['user-account-name'];
+
+ // Create formatted dates. While the server provides virtual fields
+ // for formatted dates ('changed-date' and 'created-date'), these
+ // are not necessarily formatted the way Linux stat formats them.
+ // - D = 3-letter day of week.
+ // - M = 3-letter month abbreviation.
+ // - j = date of week without leading zeroes.
+ // - H = hour on 24-hour clock without leading zeroes.
+ // - i = minutes with leading zeroes.
+ // - s = seconds with leading zeroes.
+ // - Y = 4-digit year.
+ $modifiedDate = date('D M j H:i:s Y', $modified);
+ $createdDate = date('D M j H:i:s Y', $created);
+
+ // Create simplified file type per Linux.
+ // - Root and subfolders = "Directory".
+ // - Everything else = "File".
+ if ($mimeType === 'folder/directory') {
+ $fileType = "Directory ($mimeType)";
+ }
+ else {
+ $fileType = "Regular File ($mimeType)";
+ }
+
+ // Site info.
+ $host = $response['host'];
+
+ // Get the file name from the path. We cannot use PHP's 'basename'
+ // since it uses the current OS's conventions ("locale") rather than
+ // our generic conventions. We also need to be multi-byte safe.
+ if ($path === '/') {
+ $filename = $path;
+ }
+ else {
+ $lastSlashPosition = mb_strrpos($path, '/');
+ if ($lastSlashPosition === FALSE) {
+ $filename = $path;
+ }
+
+ $filename = mb_substr($path, ($lastSlashPosition + 1));
+ }
+
+ // Format the output.
+ $text = '';
+ if ($terseMode === TRUE) {
+ $text .= sprintf(
+ "%s %s - %s %d - - %d - - - - %d %d -\n",
+ $filename,
+ $size,
+ $permission,
+ $uid,
+ $id,
+ $modified,
+ $created);
+ }
+ else {
+ // Formatting here approximately mimics Linux et al.
+ // - %6s = the first-in-line label, which Linux shows as <= 6 chars.
+ //
+ // - %-13s = the indent to the 2nd column, which is also used in Linux.
+ // This length is dictated by the length of human-readable dates in
+ // the Modify/Create lines.
+ //
+ // File: "FILE"
+ // - Mimics Linux. Like Linux, it always uses the "File" label, even
+ // when the item is a folder. Linux shows whatever value is given
+ // to the command (e.g. "./stuff" or "/a/b/c/stuff"). Since we
+ // do not support relative paths, the full path is always shown.
+ $text .= sprintf(
+ "%6s: \"%s\"\n",
+ 'File',
+ $path);
+
+ // Host: NAME
+ // - FolderShare-specific. We cannot add other items to the line
+ // because host/domain names can be long and it would mess up
+ // further line formatting.
+ $text .= sprintf(
+ "%6s: %-13s\n",
+ 'Host',
+ $host);
+
+ // Size: SIZE FileType: TYPE (MIME)
+ // - Mimics Linux. Spacing to the FileType differs - increased so that
+ // items line up with the next few multi-value lines.
+ // - Linux shows the FileType as either "Regular File" or "Directory"
+ // and doesn't include a Mime type. We add the Mime type in parens.
+ $text .= sprintf(
+ "%6s: %-13s %-9s %s\n",
+ 'Size',
+ $size,
+ 'FileType:',
+ $fileType);
+
+ // ID: ID ParentID: ID RootID: ID
+ // - FolderShare-specific. Linux instead shows the device major/minor
+ // numbers, Inode number, and the number of links. We show entity IDs.
+ $text .= sprintf(
+ "%6s: %-13s %-9s %-9s %s %-9s\n",
+ 'ID',
+ $id,
+ 'ParentID:',
+ $parentId,
+ 'RootID:',
+ $rootId);
+
+ // Mode: STATUS UID: (UID/LOGIN)
+ // - Mimics Linux. Instead of the RWX permissions groups of Linux,
+ // we show the generic sharing status.
+ // - Linux formats the UID and account name as (%5d/%8s) because the
+ // original account name length limit was 8 characters, and the
+ // original account ID limit was for a 16-bit number. Linux still
+ // uses this formatting, but overflows this presentation as needed.
+ $text .= sprintf(
+ "%6s: %-13s %s: (%5d/%8s)\n",
+ 'Mode',
+ $permission,
+ 'Uid',
+ $uid,
+ $accountName);
+
+ // Modify: DATE
+ // - Mimics Linux.
+ $text .= sprintf(
+ "%6s: %s\n",
+ 'Modify',
+ $modifiedDate);
+
+ // Create: DATE
+ // - FolderShare-specific. Linux et al do not store the creation date.
+ // Instead it has a "Change" time that records the last time inode
+ // data was set (owner, group, link count, mode).
+ $text .= sprintf(
+ "%6s: %s\n",
+ 'Create',
+ $createdDate);
+ }
+
+ return $text;
+ }
+
+ /**
+ * Formats an entity list similar to the output of a Linux 'ls' command.
+ *
+ * The Linux 'ls' command prints a list of files and folders. In its
+ * default mode, file and folder names are shown in multiple columns
+ * sized to fit on the screen. Several common options modify this output
+ * to list one file per line and include more information. Some of the
+ * most useful options are:
+ * - '-l' = show a long form that adds the file's permissions, number of
+ * hard links, owner name, group name, size (in blocks), modified date,
+ * and file name.
+ *
+ * - '-s' = add the file's size in bytes.
+ *
+ * - '-i' = add the Inode number.
+ *
+ * - '-o' = omit the group name with paired with '-l'..
+ *
+ * There are many more options to Linux 'ls', but they are more obscure
+ * and not relevant here.
+ *
+ * For this method, several options are supported to create a few common
+ * output modes:
+ *
+ * - Short mode. File and folder names are output in mulitple columns
+ * sized to fit within an 80-character wide window. Output is sorted
+ * by the file name. If '-s' or '-i' options are given, the size and
+ * entity ID are included before each name.
+ *
+ * - Long mode. File and folder names are output with one per line,
+ * including attributes in a Linux style. Lines are sorted by the
+ * file name. The '-s' option is ignored because sizes are always
+ * shown in bytes. If '-i' is given, the entity ID is included at
+ * the start of each line. The group name is always skipped, as if
+ * '-o' were given. The number of hard links is always skipped
+ * since there is no such thing in FolderShare.
+ *
+ * The following values are available to show in short or long modes:
+ * - Entity ID in place of Inode number.
+ * - Sharing status in place of Linux permissions.
+ * - User account name.
+ * - Size (always in bytes).
+ * - Modification date.
+ * - Name.
+ *
+ * The following formatting flags are supported:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -F | Add special symbols on files and folders. |
+ * | -i | Include the inode number. |
+ * | -l | Show a long listing format. |
+ * | -s | Include the size of each item. |
+ * | -S | Sort by size. |
+ * | -t | Sort by modification time. |
+ *
+ * @param array $response
+ * The key-value array response when querying a file or folder.
+ * @param array $flags
+ * (optional, default = []) Linux 'ls' style command-line flags.
+ *
+ * @return string
+ * Returns a text representation of the response.
+ *
+ * @throws \InvalidArgumentException
+ * Throws an exception if the $response array is not for an entity or
+ * list of entities.
+ */
+ public static function formatAsLinuxLs(array $response, array $flags = []) {
+ //
+ // Validate
+ // --------
+ // Make sure the given server response is key-value data for
+ // a single entity or a list of entities.
+ if (empty($response) === TRUE) {
+ // It is not an error to try and format an empty response array.
+ // It could simply mean that a folder listing found nothing to list.
+ return '';
+ }
+
+ if (isset($response['path']) === TRUE) {
+ // Response is for a single entity. Make it an array of one entry
+ // and continue.
+ $response = [$response];
+ }
+ else {
+ // The array is not for a single entity. Is it for a list of entities?
+ foreach ($response as $r) {
+ if (isset($r['path']) === FALSE) {
+ throw new \InvalidArgumentException(
+ "Programmer error: Response array is not in key-value form when formatting for 'ls'.");
+ }
+ }
+ }
+
+ //
+ // Check options
+ // -------------
+ // Look for options we support.
+ $longMode = FALSE;
+ $showSize = FALSE;
+ $showId = FALSE;
+ $showSymbol = FALSE;
+ $sortBy = 'name';
+
+ foreach ($flags as $flag) {
+ switch ($flag) {
+ case '-F':
+ $showSymbol = TRUE;
+ break;
+
+ case '-i':
+ $showId = TRUE;
+ break;
+
+ case '-l':
+ $longMode = TRUE;
+ break;
+
+ case '-s':
+ $showSize = TRUE;
+ break;
+
+ case '-S':
+ $sortBy = 'size';
+ break;
+
+ case '-t':
+ $sortBy = 'changed';
+ break;
+ }
+ }
+
+ //
+ // Sort
+ // ----
+ // For all modes, sort the response list by the file name. The sort is
+ // case sensitive, so upper case names bubble to the top of the list.
+ switch ($sortBy) {
+ default:
+ case 'name':
+ usort(
+ $response,
+ function ($a, $b) {
+ $aname = basename($a['path']);
+ $bname = basename($b['path']);
+ return ($aname < $bname) ? (-1) : 1;
+ });
+ break;
+
+ case 'size':
+ usort(
+ $response,
+ function ($a, $b) {
+ if (isset($a['size']) === TRUE) {
+ $asize = (float) $a['size'];
+ }
+ else {
+ $asize = INF;
+ }
+
+ if (isset($b['size']) === TRUE) {
+ $bsize = (float) $b['size'];
+ }
+ else {
+ $bsize = INF;
+ }
+
+ return ($asize < $bsize) ? (-1) : 1;
+ });
+ break;
+
+ case 'changed':
+ usort(
+ $response,
+ function ($a, $b) {
+ $atime = $a['changed'];
+ $btime = $b['changed'];
+ return ($atime < $btime) ? (-1) : 1;
+ });
+ break;
+ }
+
+ //
+ // Generate output
+ // ---------------
+ // Extract and format relevant values.
+ //
+ // The 'size' may not be present if a folder does not yet have a size
+ // computed. All other values are always present.
+ if ($longMode === FALSE) {
+ // Loop through the response and find the longest file name.
+ $filenames = [];
+ $maxLength = 0;
+ foreach ($response as $r) {
+ // Get the item name.
+ $name = basename($r['path']);
+
+ // Get a symbol suffix based on the kind, if any.
+ if ($showSymbol === TRUE) {
+ $kind = $r['kind'];
+ if ($kind === 'folder') {
+ $name .= '/';
+ }
+ }
+
+ // Save the name for the list below.
+ $filenames[] = $name;
+
+ // Keep track of the longest name.
+ $length = mb_strlen($name);
+ if ($length > $maxLength) {
+ $maxLength = $length;
+ }
+ }
+
+ // If the size and entity ID are included in the output, increase
+ // the maximum length to account for the space they require.
+ if ($showSize === TRUE) {
+ $maxLength += (8 + 1);
+ }
+
+ if ($showId === TRUE) {
+ $maxLength += (8 + 1);
+ }
+
+ // And add one for a space between columns.
+ ++$maxLength;
+
+ // Compute the number of columns of names in the output.
+ // Allow a maximum of 80 characters to a line.
+ //
+ // (80 is an historical line length that dates back to Hollerith punch
+ // card widths and FORTRAN, but it remains a common magic number of
+ // terminal window widths).
+ $n = count($response);
+ $nColumns = ceil(80 / $maxLength);
+ $nRows = (int) ($n / $nColumns);
+ if (($nRows * $nColumns) !== $n) {
+ ++$nRows;
+ }
+
+ // Generate output.
+ $text = '';
+ for ($i = 0; $i < $nRows; ++$i) {
+ $rowText = '';
+ for ($j = 0; $j < $nColumns; ++$j) {
+ // Compute the index of the next response.
+ $index = (($i * $nColumns) + $j);
+ if ($index >= $n) {
+ continue;
+ }
+
+ // Get values.
+ $filename = $filenames[$index];
+ $size = isset($response[$index]['size']) === TRUE ?
+ (string) $response[$index]['size'] : '-';
+ $id = $response[$index]['id'];
+
+ // Format the item.
+ $itemText = '';
+ if ($showId === TRUE) {
+ $itemText .= sprintf("%8d ", $id);
+ }
+
+ if ($showSize === TRUE) {
+ $itemText .= sprintf("%8s ", $size);
+ }
+
+ $itemText .= sprintf("%-" . $maxLength . "s", $filename);
+ if ($j === 0) {
+ $rowText .= $itemText;
+ }
+ else {
+ $rowText .= ' ' . $itemText;
+ }
+ }
+
+ $text .= $rowText . "\n";
+ }
+
+ return $text;
+ }
+
+ // Generate output.
+ $text = '';
+ foreach ($response as $item) {
+ // Get values.
+ $filename = basename($item['path']);
+ $size = isset($item['size']) === TRUE ? (string) $item['size'] : '-';
+ $permission = $item['sharing-status'];
+ $accountName = $item['user-account-name'];
+ $id = $item['id'];
+ $modifiedDate = $item['changed-date'];
+
+ // Get a symbol suffix based on the kind, if any.
+ if ($showSymbol === TRUE) {
+ $kind = $item['kind'];
+ if ($kind === 'folder') {
+ $filename .= '/';
+ }
+ }
+
+ // Format the row.
+ $idText = '';
+ if ($showId === TRUE) {
+ $idText = sprintf("%8d ", $id);
+ }
+
+ $text .= sprintf("%s%7s %8s %8s %s %s\n",
+ $idText,
+ $permission,
+ $accountName,
+ $size,
+ $modifiedDate,
+ $filename);
+ }
+
+ return $text;
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Usage report formatting.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Formats a usage report as text.
+ *
+ * The keys and values of the array are interpreted as a usage report
+ * and formatted as a text table of values.
+ *
+ * @param array $response
+ * The key-value response to convert to text.
+ *
+ * @return string
+ * The string representation.
+ */
+ public static function formatAsUsage(array $response) {
+ //
+ // Validate
+ // --------
+ // It is not an error to try and format an empty response array.
+ // It could simply mean that there is no response.
+ if (empty($response) === TRUE) {
+ return '';
+ }
+
+ // Make sure the response does appear to be usage information.
+ if (isset($response['nFolders']) === FALSE) {
+ throw new \InvalidArgumentException(
+ "Programmer error: Response array is not in key-value form when formatting for usage data.");
+ }
+
+ //
+ // Generate output
+ // ---------------
+ // Pull out user and host information and output that first.
+ // Then map the remaining usage information to friendlier names
+ // and output that as a key-value list.
+ //
+ // Get host and user.
+ $host = (isset($response['host']) === TRUE) ?
+ $response['host'] : 'Unknown host';
+
+ if (isset($response['user-account-name']) === TRUE) {
+ $userName = $response['user-account-name'];
+ }
+ elseif (isset($response['user-id']) === TRUE) {
+ $userName = $response['user-id'];
+ }
+ else {
+ $userName = 'Unknown user';
+ }
+
+ $text = "$host usage for $userName:\n";
+
+ // Create an alternate usage array with user-friendly names.
+ $alt = [];
+ foreach ($response as $k => $r) {
+ switch ($k) {
+ case 'user-id':
+ case 'user-account-name':
+ case 'user-display-name':
+ case 'host':
+ break;
+
+ case 'nFolders':
+ $alt['Folders'] = $r;
+ break;
+
+ case 'nFiles':
+ $alt['Files'] = $r;
+ break;
+
+ case 'nBytes':
+ $alt['Bytes'] = $r;
+ break;
+
+ default:
+ $alt[$k] = $r;
+ break;
+ }
+ }
+
+ $text .= self::formatAsText($alt, ' ');
+ return $text;
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Configuration report formatting.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Formats a configuration report as text.
+ *
+ * The keys and values of the array are interpreted as a configuration report
+ * and formatted as a text table of values.
+ *
+ * @param array $response
+ * The key-value response to convert to text.
+ *
+ * @return string
+ * The string representation.
+ */
+ public static function formatAsConfiguration(array $response) {
+ //
+ // Validate
+ // --------
+ // It is not an error to try and format an empty response array.
+ // It could simply mean that there is no response.
+ if (empty($response) === TRUE) {
+ return '';
+ }
+
+ //
+ // Generate output
+ // ---------------
+ // Show the host first, then map the remaining information into
+ // friendlier names and a better structure. Then output that as a
+ // key-value list.
+ //
+ // Get host and user.
+ $host = (isset($response['host']) === TRUE) ?
+ $response['host'] : 'Unknown host';
+
+ // Create an alternate configuration array with user-friendly names.
+ $alt = [];
+ $alt['Web services'] = [];
+ $alt['File handling'] = [];
+
+ $showExtensions = TRUE;
+
+ foreach ($response as $k => $r) {
+ switch ($k) {
+ case 'host':
+ // Already handled.
+ break;
+
+ case 'GET':
+ case 'POST':
+ case 'DELETE':
+ case 'PATCH':
+ $webalt = [];
+ foreach ($r as $kk => $rr) {
+ switch ($kk) {
+ case 'serializer-formats':
+ $webalt['Formats'] = $rr;
+ break;
+
+ case 'authentication-providers':
+ $webalt['Authentication'] = $rr;
+ break;
+
+ default:
+ $webalt[$kk] = $rr;
+ }
+ }
+
+ $alt['Web services'][$k] = $webalt;
+ break;
+
+ case 'file-restrict-extensions':
+ $alt['File handling']['Restrict extensions'] = $r;
+ if (is_bool($r) === TRUE && $r === FALSE) {
+ $showExtensions = FALSE;
+ }
+ elseif (is_scalar($r) === TRUE &&
+ (((string) $r) === "false") || ((string) $r) === "FALSE") {
+ $showExtensions = FALSE;
+ }
+ break;
+
+ case 'file-allowed-extensions':
+ $alt['File handling']['Allowed extensions'] = $r;
+ break;
+
+ default:
+ if (isset($alt['Other']) === FALSE) {
+ $alt['Other'] = [];
+ }
+
+ $alt['Other'][$k] = $r;
+ break;
+ }
+ }
+
+ if ($showExtensions === FALSE) {
+ unset($alt['File handling']['Allowed extensions']);
+ }
+
+ $text = "$host configuration:\n";
+ $text .= self::formatAsText($alt, ' ');
+ return $text;
+ }
+
+ /*--------------------------------------------------------------------
+ *
+ * Version report formatting.
+ *
+ *--------------------------------------------------------------------*/
+
+ /**
+ * Formats a version number report as text.
+ *
+ * The keys and values of the array are interpreted as a version number
+ * report and formatted as a text table of values.
+ *
+ * @param array $response
+ * The key-value response to convert to text.
+ *
+ * @return string
+ * The string representation.
+ */
+ public static function formatAsVersion(array $response) {
+ //
+ // Validate
+ // --------
+ // It is not an error to try and format an empty response array.
+ // It could simply mean that there is no response. But it is unlikely.
+ if (empty($response) === TRUE) {
+ return '';
+ }
+
+ // Confirm that this does seem to be a version response.
+ if (isset($response['client']) === FALSE) {
+ throw new \InvalidArgumentException(
+ "Programmer error: Response array is not in key-value form when formatting version data.");
+ }
+
+ //
+ // Generate output
+ // ---------------
+ // The output has several rows:
+ // - The application name and version.
+ // - The client API name and version.
+ // - The host server name.
+ // - A list of server item names and versions.
+ //
+ // Normally all of these are present in a version request's response.
+ // But to be robust we need to check for each one and have a fallback.
+ $text = '';
+ $indent = ' ';
+
+ //
+ // Print versions.
+ //
+ foreach (['client', 'server'] as $section) {
+ if (isset($response[$section]) === FALSE) {
+ continue;
+ }
+
+ if ($section === 'client') {
+ $text .= "Client software\n";
+ }
+ elseif ($section === 'server') {
+ if (isset($response['host']) === FALSE) {
+ // If there is no host, then there cannot be server side version
+ // numbers. Anything that might be present is bogus. Skip it.
+ break;
+ }
+
+ $hostname = $response['host'];
+ $text .= "\nWeb services at $hostname\n";
+ }
+
+ foreach ($response[$section] as $k => $v) {
+ $name = $k;
+ $version = '';
+
+ if (isset($response[$section][$k]['name']) === TRUE) {
+ $name = $response[$section][$k]['name'];
+ }
+
+ if (isset($response[$section][$k]['version']) === TRUE) {
+ $version = ', version ' . $response[$section][$k]['version'];
+ }
+
+ $text .= "$indent$name$version\n";
+ }
+ }
+
+ return $text;
+ }
+
+}
+
+/**
+ * @file
+ * Issues command-line requests to a host using the FolderShare module.
+ */
+
+
+
+/*----------------------------------------------------------------------
+ *
+ * Verify execution environment.
+ *
+ *----------------------------------------------------------------------*/
+
+/**
+ * The required PHP version.
+ */
+const REQUIRED_PHP_VERSION = "7.0.0";
+
+/**
+ * The required PHP packages.
+ */
+const REQUIRED_PACKAGES = [
+ [
+ 'extension' => 'curl',
+ 'name' => 'PHP CURL',
+ 'version' => '7.0.0',
+ ],
+ [
+ 'extension' => 'date',
+ 'name' => 'PHP Date',
+ 'version' => '7.0.0',
+ ],
+ [
+ 'extension' => 'json',
+ 'name' => 'PHP JSON',
+ 'version' => '1.5.0',
+ ],
+ [
+ 'extension' => 'mbstring',
+ 'name' => 'PHP Multi-byte String',
+ 'version' => '7.0.0',
+ ],
+ [
+ 'extension' => 'readline',
+ 'name' => 'PHP Readline',
+ 'version' => '7.0.0',
+ ],
+];
+
+/**
+ * Verify suitability of PHP installation.
+ *
+ * This file uses PHP 7 syntax, such as for type hints on function arguments.
+ * If the file is read by PHP <7, it will report syntax errors *before* we
+ * get a chance to check the version number.
+ */
+if (version_compare(phpversion(), REQUIRED_PHP_VERSION) < 0) {
+ $v = REQUIRED_PHP_VERSION;
+ fwrite(STDERR, "Abort. The current version of PHP is out of date.\n");
+ fwrite(STDERR, "This application requires at least version $v.\n");
+ exit(1);
+}
+
+foreach (REQUIRED_PACKAGES as $package) {
+ $e = $package['extension'];
+ $v = $package['version'];
+ $n = $package['name'];
+ $pv = phpversion($e);
+
+ if ($pv === FALSE) {
+ fwrite(STDERR, "Abort. The required $n package is not installed.\n");
+ exit(1);
+ }
+
+ if (empty($v) === FALSE && empty($pv) === FALSE) {
+ // A specific version number is required.
+ if (version_compare($pv, $v) < 0) {
+ fwrite(STDERR, "Abort. A newer version of the $n package is required.\n");
+ fwrite(STDERR, "This application requires at least version $v, but only $pv is available.\n");
+ exit(1);
+ }
+ }
+}
+
+/*----------------------------------------------------------------------
+ *
+ * Define constants.
+ *
+ *----------------------------------------------------------------------*/
+
+/**
+ * This application name.
+ */
+const NAME = "FolderShare application";
+
+/**
+ * This application version number.
+ */
+const VERSION = "0.6.3 (March 2019)";
+
+/**
+ * The anonymous login.
+ */
+const ANONYMOUS = "anonymous";
+
+/**
+ * The wait interval for the sync command.
+ */
+const SYNC_WAIT_SECONDS = 5;
+
+/*----------------------------------------------------------------------
+ *
+ * Define available commands.
+ *
+ *----------------------------------------------------------------------*/
+
+/**
+ * Describes commands and their options.
+ *
+ * The associative array's keys are command names available for use on the
+ * command line (e.g. "ls" and "stat"). For each command, an array of values
+ * describes the command, including its help text, a list of supported
+ * flags, its return formats, synonyms, and finally the name of the function
+ * to invoke to execute the command.
+ *
+ * Command help:
+ *
+ * - 'synopsis' = a one-line note on the command name and options, similar to
+ * the synopsis line of a traditional Linux/macOS/BSD man page.
+ *
+ * - 'description' = a multiparagraph description of the command.
+ *
+ * - 'flags' = an associative array that lists no-argument flags for the
+ * command. For each flag, the array key names the flag, including leading
+ * dashes ("-" or "--" or both). The key's value is an associative array
+ * with keys for:
+ * - 'brief' = a one-line description of the flag.
+ * - 'synonyms' = an array of equivalent flag names. Defaults to empty.
+ *
+ * - 'synonyms' = an array of alternate names for the command. Defaults to
+ * empty.
+ *
+ * Command return format keys:
+ *
+ * - 'formats' = an array of return format names supported by the command.
+ * Known values are: 'full', 'keyvalue', 'text', and 'linux'.
+ *
+ * - 'defaultFormat' = the default format.
+ *
+ * Execution keys:
+ *
+ * - 'function' = the name of the function to invoke for the command.
+ */
+const COMMANDS = [
+ //
+ // Local state.
+ // ------------
+ //
+ // Print user name.
+ //
+ 'whoami' => [
+ 'synopsis' => 'whoami',
+ 'brief' => 'Show the current user name.',
+ 'description' => <<<'EOS'
+Show the user name of the current user.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ ],
+ 'synonyms' => [
+ 'id',
+ ],
+ 'formats' => [],
+ 'defaultFormat' => '',
+ 'function' => 'runWhoami',
+ ],
+
+ //
+ // Print host name.
+ //
+ 'hostname' => [
+ 'synopsis' => 'hostname',
+ 'brief' => 'Show the current host name.',
+ 'description' => <<<'EOS'
+Show the host name of the current server connection.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ ],
+ 'formats' => [],
+ 'defaultFormat' => '',
+ 'function' => 'runHostname',
+ ],
+
+ //
+ // HTTP GET operations supported.
+ // ------------------------------
+ //
+ //
+ // GET version numbers.
+ //
+ 'version' => [
+ 'synopsis' => 'version [OPTIONS]',
+ 'brief' => 'Show version numbers.',
+ 'description' => <<<'EOS'
+Show version numbers for client and server software.
+
+If a host name is provided, the host is contacted and its software version
+numbers reported along with those for the client software. But if no host
+name is provided, only client software version numbers are reported.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ 'text',
+ ],
+ 'defaultFormat' => 'text',
+ 'function' => 'runVersion',
+ ],
+
+ //
+ // GET server configuration.
+ //
+ 'config' => [
+ 'synopsis' => 'config [OPTIONS]',
+ 'brief' => 'Show the host administrator configuration.',
+ 'description' => <<<'EOS'
+Summarize the administrator configuration settings for the host.
+
+Reported configuration values include:
+ - A list of HTTP verbs supported by the host.
+ - The serialization formats supported for each HTTP verb.
+ - The authentication providers supported for each HTTP verb.
+ - The maximum file upload size and number.
+ - The file name extensions supported, if restrictions are enabled.
+
+This information is primiarily of interest to host administrators and to those
+developing client-server software. The reported values cannot be altered
+through this client software. Administrators may adjust their host's
+configuration using the web interface to the REST and FolderShare modules.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ ],
+ 'synonyms' => [
+ 'configuration',
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ 'text',
+ ],
+ 'defaultFormat' => 'text',
+ 'function' => 'runConfig',
+ ],
+
+ //
+ // GET user's usage.
+ //
+ 'usage' => [
+ 'synopsis' => 'usage [OPTIONS]',
+ 'brief' => 'Show the user\'s storage use.',
+ 'description' => <<<'EOS'
+Report the number of top-level folders, subfolders, files, and bytes in use
+by the user on the host.
+
+The user must have permission to create and modify remote files and folders.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ 'text',
+ ],
+ 'defaultFormat' => 'text',
+ 'function' => 'runUsage',
+ ],
+
+ //
+ // GET file or folder status.
+ //
+ 'stat' => [
+ 'synopsis' => 'stat [OPTIONS] REMOTE_PATHS...',
+ 'brief' => 'Show the status of files and folders.',
+ 'description' => <<<'EOS'
+Show detailed status for remote files or folders.
+
+Status information includes the item's name, remote folder path, owner, size,
+MIME type, numeric entity ID, parent entity ID, top-level folder entity ID,
+creation date, modified date, and whether the item is private, shared with
+others, or shared with the public.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to view the remote files and folders.
+
+This command is similar to the 'stat' command on Linux, BSD, and macOS.
+EOS
+ ,
+ 'flags' => [
+ '-t' => [
+ 'brief' => 'Show a terse one-line version of the information.',
+ 'synonyms' => [
+ '--terse',
+ ],
+ ],
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ ],
+ 'synonyms' => [
+ 'status',
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ 'linux',
+ 'text',
+ ],
+ 'defaultFormat' => 'linux',
+ 'function' => 'runStat',
+ ],
+
+ //
+ // GET folder contents.
+ //
+ 'ls' => [
+ 'synopsis' => 'ls [OPTIONS] REMOTE_PATHS...',
+ 'brief' => 'List files and folders.',
+ 'description' => <<<'EOS'
+List remote files and folders.
+
+The command supports a default short form and a long form with the '-l' option.
+Both forms default to sorting items by name:
+ - The short form lists item names in multiple columns.
+ - The long form lists item names and attributes in a single-column list.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to view the remote files and folders.
+
+This command is similar to the 'ls' command on Linux, BSD, and macOS.
+EOS
+ ,
+ 'flags' => [
+ '-d' => [
+ 'brief' => 'List folders without recursion.',
+ ],
+ '-F' => [
+ 'brief' => 'Mark folders by adding a "/" suffix.',
+ ],
+ '-i' => [
+ 'brief' => 'Show the numeric entity ID for each item.',
+ ],
+ '-l' => [
+ 'brief' => 'Show a long form that lists more detail.',
+ ],
+ '-R' => [
+ 'brief' => 'Recurse to list folder contents.',
+ ],
+ '-s' => [
+ 'brief' => 'Show the size, in bytes, for each item.',
+ ],
+ '-S' => [
+ 'brief' => 'Sort by size instead of name.',
+ ],
+ '-t' => [
+ 'brief' => 'Sort by modification time instead of name.',
+ ],
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ ],
+ 'synonyms' => [
+ 'list',
+ 'dir',
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ 'linux',
+ 'text',
+ ],
+ 'defaultFormat' => 'linux',
+ 'function' => 'runLs',
+ ],
+
+ //
+ // HTTP DELETE operations supported.
+ // ---------------------------------
+ //
+ // DELETE file or folder.
+ //
+ 'rm' => [
+ 'synopsis' => 'rm [OPTIONS] REMOTE_PATHS...',
+ 'brief' => 'Remove files or folders.',
+ 'description' => <<<'EOS'
+Remove remote files and and folders.
+
+Use 'rm' without options to remove remote files. Use 'rm -d' to also remove
+empty folders. Use 'rm -r' to recursively remove files and non-empty folders
+and their contents, recursively.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to remove the remote files and folders.
+
+This command is similar to the 'rm' command on Linux, BSD, and macOS.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '-d' => [
+ 'brief' => 'Delete folders as well as files.',
+ 'synonyms' => [],
+ ],
+ '-f' => [
+ 'brief' => 'Force deletion to continue even after errors.',
+ 'synonyms' => [],
+ ],
+ '-r' => [
+ 'brief' => 'Recursively remove folders and their contents.',
+ 'synonyms' => [
+ '-R',
+ ],
+ ],
+ '-v' => [
+ 'brief' => 'Show the name of each item as it is deleted.',
+ 'synonyms' => [],
+ ],
+ ],
+ 'synonyms' => [
+ 'remove',
+ 'del',
+ 'delete',
+ ],
+ 'formats' => [],
+ 'defaultFormat' => '',
+ 'function' => 'runRm',
+ ],
+
+ //
+ // DELETE folder.
+ //
+ 'rmdir' => [
+ 'synopsis' => 'rmdir [OPTIONS] REMOTE_PATHS...',
+ 'brief' => 'Remove empty folders.',
+ 'description' => <<<'EOS'
+Remove remote empty folders.
+
+Use 'rmdir' to remove remote empty folders. It is an error to attempt to
+remove a non-empty folder.
+
+To remove files and non-empty folders, use the 'rm -r' command instead.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to remove the remote folders.
+
+This command is similar to the 'rmdir' command on Linux, BSD, and macOS.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '-v' => [
+ 'brief' => 'Show the name of each item as it is deleted.',
+ 'synonyms' => [],
+ ],
+ ],
+ 'formats' => [],
+ 'defaultFormat' => '',
+ 'function' => 'runRmdir',
+ ],
+
+ //
+ // HTTP POST operations supported.
+ // -------------------------------
+ //
+ // POST new root folder or subfolder.
+ //
+ 'mkdir' => [
+ 'synopsis' => 'mkdir [OPTIONS] REMOTE_PATHS...',
+ 'brief' => 'Make new folders.',
+ 'description' => <<<'EOS'
+Make new remote empty folders.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to create the remote folders.
+
+This command is similar to the 'mkdir' command on Linux, BSD, and macOS.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '-v' => [
+ 'brief' => 'Show the name of each item as it is created.',
+ 'synonyms' => [],
+ ],
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ ],
+ 'defaultFormat' => 'keyvalue',
+ 'function' => 'runMkdir',
+ ],
+
+ //
+ // HTTP PATCH operations supported.
+ // --------------------------------
+ //
+ // PATCH name and/or location.
+ //
+ 'mv' => [
+ 'synopsis' => 'mv [OPTIONS] REMOTE_PATHS... REMOTE_DESTINATION_PATH',
+ 'brief' => 'Move files and folders.',
+ 'description' => <<<'EOS'
+Move remote files or folders.
+
+The command has two forms:
+ - With two paths, the command moves the file or folder indicated by the
+ first path into the destination selected by the second path.
+
+ - With more than two paths, the command moves the files and folders indicated
+ by all of the paths, except the last, into the destination folder selected
+ by the last path.
+
+With two paths, if the destination does not exist, but its parent folder does,
+the moved item will be moved into the parent and renamed to use the name in
+the destination path.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to modify the remote folders.
+
+This command is similar to the 'mv' command on Linux, BSD, and macOS.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '--sync' => [
+ 'brief' => 'Sync with server by waiting for move to complete.',
+ 'synonyms' => [
+ '--wait',
+ ],
+ ],
+ '-n' => [
+ 'brief' => 'Do not overwrite an existing file.',
+ 'synonyms' => [],
+ ],
+ '-v' => [
+ 'brief' => 'Show the name of each item as it is moved.',
+ 'synonyms' => [],
+ ],
+ ],
+ 'synonyms' => [
+ 'move',
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ ],
+ 'defaultFormat' => 'keyvalue',
+ 'function' => 'runMv',
+ ],
+
+ //
+ // PATCH to create a copy.
+ //
+ 'cp' => [
+ 'synopsis' => 'cp [OPTIONS] REMOTE_PATHS... REMOTE_DESTINATION_PATH',
+ 'brief' => 'Copy files and folders.',
+ 'description' => <<<'EOS'
+Copy remote files or folders.
+
+The command has two forms:
+ - With two paths, the command copies the file or folder indicated by the
+ first path into the destination selected by the second path.
+
+ - With more than two paths, the command copies the files and folders
+ indicated by all of the paths, except the last, into the destination folder
+ selected by the last path.
+
+With two paths, if the destination does not exist, but its parent folder does,
+the copied item will be copied into the parent and renamed to use the name in
+the destination path.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to create and modify the remote files and
+folders.
+
+This command is similar to the 'cp' command on Linux, BSD, and macOS.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '--sync' => [
+ 'brief' => 'Sync with server by waiting for copy to complete.',
+ 'synonyms' => [
+ '--wait',
+ ],
+ ],
+ '-n' => [
+ 'brief' => 'Do not overwrite an existing file.',
+ 'synonyms' => [],
+ ],
+ '-v' => [
+ 'brief' => 'Show the name of each item as it is copied.',
+ 'synonyms' => [],
+ ],
+ ],
+ 'synonyms' => [
+ 'copy',
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ ],
+ 'defaultFormat' => 'keyvalue',
+ 'function' => 'runCp',
+ ],
+
+ //
+ // PATCH to update fields.
+ //
+ 'update' => [
+ 'synopsis' => 'update [OPTIONS] FIELD_NAME FIELD_VALUE REMOTE_PATHS...',
+ 'brief' => 'Update fields on files and folders.',
+ 'description' => <<<'EOS'
+Update the value for a named field on remote files or folders.
+
+Fields that may be updated include:
+ - 'name' = the name of the remote file or folder (see also the 'mv' command).
+ - 'description' = the text description of the remote file or folder.
+
+Additional fields may be supported by third-party extensions or customizations
+made on specific hosts.
+
+Field values that are more than one word (such as a description's text) or that
+include special characters (such as in HTML) should be surrounded by quotes.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to modify the remote files and folders.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '-v' => [
+ 'brief' => 'Show the name of each item as it is changed.',
+ 'synonyms' => [],
+ ],
+ ],
+ 'formats' => [
+ 'full',
+ 'keyvalue',
+ 'text',
+ ],
+ 'defaultFormat' => 'text',
+ 'function' => 'runUpdate',
+ ],
+
+ //
+ // GET to download file.
+ //
+ 'download' => [
+ 'synopsis' => 'download [OPTIONS] REMOTE_PATH [LOCAL_PATH]',
+ 'brief' => 'Download files and folders.',
+ 'description' => <<<'EOS'
+Download a remote file or folder indicated to the local host.
+
+The first path indicates a remote file or folder to download, while the second
+path selects the local host location in which to save the downloaded item. If
+the second path is omitted, the current directory is used and the new local
+file has the same name as the remote file.
+
+When downloading a remote folder, that folder and its contents are first
+compressed into a ZIP archive and that archive is returned as a file saved onto
+the local host. The ZIP archive is not automatically un-zipped.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to view the remote files and folders, and create
+the local file.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '-v' => [
+ 'brief' => 'Show the name of each item as it is downloaded.',
+ 'synonyms' => [],
+ ],
+ ],
+ 'synonyms' => [
+ 'get',
+ ],
+ 'formats' => [],
+ 'defaultFormat' => '',
+ 'function' => 'runDownload',
+ ],
+
+ //
+ // POST to upload file.
+ //
+ 'upload' => [
+ 'synopsis' => 'upload [OPTIONS] LOCAL_PATH REMOTE_PATH',
+ 'brief' => 'Upload files and folders.',
+ 'description' => <<<'EOS'
+Upload a local file or folder to a remote folder.
+
+The first path selects a local file or folder to upload, while the second
+path indicates the remote location for the uploaded items.
+
+Paths to remote files and folders must start with '/'. Wild cards are not
+supported.
+
+The user must have permission to view the local file and create the remote
+files and folders.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '-v' => [
+ 'brief' => 'Show the name of each item as it is uploaded.',
+ 'synonyms' => [],
+ ],
+ ],
+ 'synonyms' => [
+ 'put',
+ ],
+ 'formats' => [],
+ 'defaultFormat' => '',
+ 'function' => 'runUpload',
+ ],
+
+ //
+ // POST to get specific attributes of a file or folder.
+ //
+ 'xattr' => [
+ 'synopsis' => 'xattr [OPTIONS] REMOTE_PATHS',
+ 'brief' => 'Get extended attributes of files and folders.',
+ 'description' => <<<'EOS'
+Get extended attributes of a remote file or folder.
+
+The path selects a remote file or folder. The path must start with '/'.
+Wild cards are not supported.
+
+Extended attributes include whether a file or folder is disabled because
+it is in use by another operation.
+
+The user must have permission to view the remote file or folder.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '--disabled' => [
+ 'brief' => 'Return "true" if the item is disabled, "false" otherwise.',
+ 'synonyms' => [
+ '--systemdisabled',
+ ],
+ ],
+ '--hidden' => [
+ 'brief' => 'Return "true" if the item is hidden, "false" otherwise.',
+ 'synonyms' => [
+ '--systemhidden',
+ ],
+ ],
+ '--kind' => [
+ 'brief' => 'Return the item kind (e.g. "file", "folder", etc.',
+ 'synonyms' => [],
+ ],
+ '--mime' => [
+ 'brief' => 'Return the item MIME type.',
+ 'synonyms' => [
+ '--mimetype',
+ ],
+ ],
+ '--size' => [
+ 'brief' => 'Return the item size, in bytes.',
+ 'synonyms' => [],
+ ],
+ // TODO Add view and author grant UIDs.
+ ],
+ 'synonyms' => [],
+ 'formats' => [],
+ 'defaultFormat' => '',
+ 'function' => 'runXattr',
+ ],
+
+ //
+ // POST and wait to check when an item becomes enabled.
+ //
+ 'sync' => [
+ 'synopsis' => 'sync REMOTE_PATHS',
+ 'brief' => 'Sync I/O by waiting until remote files and folders are enabled.',
+ 'description' => <<<'EOS'
+Synchronize with remote operations by waiting until an item is enabled.
+
+The path selects a remote file or folder. The path must start with '/'.
+Wild cards are not supported.
+
+Operations that copy, move, or change ownership can take a long time and
+require background processing. Files and folders involved are marked
+disabled while processing is in progress. Meanwhile, the request returns
+before the full operation is complete. Synchronizing waits until the
+indicated file or folder becomes marked enabled.
+
+The user must have permission to view the remote file or folder.
+EOS
+ ,
+ 'flags' => [
+ '--help' => [
+ 'brief' => 'Show this help information.',
+ ],
+ '-v' => [
+ 'brief' => 'Be verbose.',
+ ],
+ ],
+ 'synonyms' => [],
+ 'formats' => [],
+ 'defaultFormat' => '',
+ 'function' => 'runSync',
+ ],
+];
+
+/*--------------------------------------------------------------------
+ *
+ * Help and error messages.
+ *
+ * These functions print error messages and help information.
+ *
+ *--------------------------------------------------------------------*/
+
+/**
+ * Prints help information for command line use.
+ *
+ * The printed information is written for users that add commands to
+ * the Linux/macOS/BSD shell command line. The host, user name, password,
+ * and other base options are listed.
+ *
+ * @param string $appPath
+ * The path to the application.
+ */
+function printCommandLineHelp(string $appPath) {
+ $appName = basename($appPath);
+ $helpStart = <<<EOS
+Usage is: $appName [HOST_OPTIONS] [COMMAND] [OPTIONS]
+
+Create, delete, change, and manage files and folders on a remote web site
+that uses the FolderShare module.
+
+Commands
+--------
+
+EOS;
+
+ $helpEnd = <<<EOS
+
+Add "--help" after any command to show its description and flags.
+
+Host and account
+----------------
+All commands require a remote host and account:
+
+ --host HOSTNAME Select the host (and optional port) for the site.
+ --username NAME Specify the user login name on the site.
+ --password PASS Specify the user password on the site.
+
+Example:
+ $appName --host myhost --username me --password pw ls /
+
+If the username and/or password are omitted, the command will prompt for them.
+
+Additional options
+------------------
+ --help Show this help information.
+ --verbose Be verbose while performing the operation.
+ --version Show application version numbers.
+ --format type Select how information is displayed:
+ auto (default) Use an operation-specific default
+ full Show full-detail nested-array output
+ keyvalue Show a table of key-value pairs
+ linux Show a Linux-style output
+ text Show key-value pairs as text
+
+For Linux/macOS/BSD-style commands, the format type defaults to 'linux'
+and output is formatted similar to those OS commands.
+
+Example:
+ $appName --host myhost --format full stat /myfolder
+
+Paths
+-----
+Remote file and folder paths have the form:
+
+ [SCHEME:][//USER]/PATH.
+
+SCHEME is optional and one of:
+ personal (default) Your files and folders, and those shared with you.
+ public The host's public files and folders available to anyone.
+
+USER is optional and selects the user ID or account name. It defaults to
+the current user.
+
+PATH is a file and folder path, separated by forward slashes. Wildcards are
+not accepted. If a path contains spaces or special characters, the whole
+path should be included in quotes.
+
+Examples:
+ $appName --host myhost ls /
+ $appName --host myhost ls personal:/
+ $appName --host myhost ls public:/
+
+EOS;
+
+ $help = $helpStart;
+
+ $commandNames = array_keys(COMMANDS);
+ sort($commandNames, (SORT_STRING | SORT_FLAG_CASE));
+
+ foreach ($commandNames as $commandName) {
+ $commandInfo = COMMANDS[$commandName];
+
+ if (isset($commandInfo['brief']) === TRUE) {
+ $brief = $commandInfo['brief'];
+ }
+ else {
+ $brief = $commandName;
+ }
+
+ $help .= sprintf(" %-18s%s\n", $commandName, $brief);
+ }
+
+ $help .= $helpEnd;
+
+ fwrite(STDOUT, $help);
+}
+
+/**
+ * Prints help information for prompt use.
+ *
+ * The printed information is written for users that are using the interactive
+ * prompt. The host, user name, password, and other base options are not listed
+ * since they would already have been provided.
+ *
+ * @param string $appPath
+ * The path to the application.
+ */
+function printPromptHelp(string $appPath) {
+ $helpStart = <<<EOS
+Commands
+--------
+
+EOS;
+
+ $helpEnd = <<<EOS
+
+Add "--help" after any command to show its description and flags.
+
+Paths
+-----
+Remote file and folder paths have the form:
+
+ [SCHEME:][//USER]/PATH.
+
+SCHEME is optional and one of:
+ personal (default) Your files and folders, and those shared with you.
+ public The host's public files and folders available to anyone.
+
+USER is optional and selects the user ID or account name. It defaults to
+to the current user.
+
+PATH is a file and folder path, separated by forward slashes. Wildcards are
+not accepted. If a path contains spaces or special characters, the whole
+path should be included in quotes.
+
+Examples:
+ ls /
+ ls personal:/
+ ls public:/
+
+EOS;
+
+ $help = $helpStart;
+
+ $commandNames = array_keys(COMMANDS);
+ $commandNames[] = 'help';
+ $commandNames[] = 'quit';
+ $commandNames[] = 'exit';
+ sort($commandNames, (SORT_STRING | SORT_FLAG_CASE));
+
+ foreach ($commandNames as $commandName) {
+ if ($commandName === 'help') {
+ $brief = "Show this help information.";
+ }
+ elseif ($commandName === 'quit') {
+ $brief = "Quit.";
+ }
+ elseif ($commandName === 'exit') {
+ $brief = "Quit.";
+ }
+ else {
+ $commandInfo = COMMANDS[$commandName];
+
+ if (isset($commandInfo['brief']) === TRUE) {
+ $brief = $commandInfo['brief'];
+ }
+ else {
+ $brief = $commandName;
+ }
+ }
+
+ $help .= sprintf(" %-18s%s\n", $commandName, $brief);
+ }
+
+ $help .= $helpEnd;
+
+ fwrite(STDOUT, $help);
+}
+
+/**
+ * Prints help information for a command.
+ *
+ * @param string $appName
+ * The name of this command-line application.
+ * @param string $commandName
+ * The name of the command.
+ */
+function printCommandHelp(string $appName, string $commandName) {
+ if (isset(COMMANDS[$commandName]) === FALSE) {
+ // Unknown command. Print nothing.
+ return;
+ }
+
+ $command = COMMANDS[$commandName];
+
+ //
+ // Print synopsis
+ // --------------
+ // Print a command-line template for the command.
+ $synopsis = "Usage: $appName [HOST_OPTIONS] ";
+ if (isset($command['synopsis']) === FALSE) {
+ $synopsis .= $commandName;
+ }
+ else {
+ $synopsis .= $command['synopsis'];
+ }
+
+ fwrite(STDOUT, "$synopsis\n");
+
+ //
+ // Print description
+ // -----------------
+ // Print a multi-line text description.
+ $description = "\n";
+ if (isset($command['description']) === TRUE) {
+ $description .= $command['description'] . "\n";
+ }
+
+ fwrite(STDOUT, $description);
+
+ //
+ // Print flags (if any)
+ // -----------
+ // Print a list of flags and their descriptions.
+ if (isset($command['flags']) === TRUE) {
+ fwrite(STDOUT, "\nOptions:\n");
+
+ // Get flag names then sort them.
+ $flagNames = array_keys($command['flags']);
+ sort($flagNames, (SORT_STRING | SORT_FLAG_CASE));
+
+ // Print them out.
+ $flagIndent = ' ';
+ $flagHelpIndent = ' ';
+
+ foreach ($flagNames as $flagName) {
+ $flagInfo = &$command['flags'][$flagName];
+
+ // Print the flag line, with synonyms.
+ $flagLine = "$flagIndent$flagName";
+ if (isset($flagInfo['synonyms']) === TRUE) {
+ foreach ($flagInfo['synonyms'] as $flagSynonym) {
+ $flagLine .= ", $flagSynonym";
+ }
+ }
+
+ fwrite(STDOUT, "$flagLine\n");
+
+ // Print the flag command.
+ if (isset($flagInfo['brief']) === TRUE) {
+ $helpLines = mb_split("\n", $flagInfo['brief']);
+ foreach ($helpLines as $helpLine) {
+ fwrite(STDOUT, "$flagHelpIndent$helpLine\n");
+ }
+ }
+ }
+ }
+
+ //
+ // Print command synonyms (if any)
+ // ----------------------
+ // Print a list of synonyms.
+ if (isset($command['synonyms']) === TRUE) {
+ $names = [$commandName];
+ foreach ($command['synonyms'] as $synonym) {
+ $names[] = $synonym;
+ }
+
+ sort($names, SORT_NATURAL);
+ $text = implode(', ', $names);
+
+ fwrite(STDOUT, "\nSynonyms:\n $text\n");
+ }
+}
+
+/**
+ * Prints the client side version to STDERR and exits.
+ */
+function printVersionAndExit(array $options) {
+ // Create a dummy server connection without a host or authentication.
+ // Then get the version of the client side only.
+ $dummyServer = new FolderShareConnect();
+ $options['format'] = 'text';
+ $text = runVersion($dummyServer, $options, FALSE);
+ fwrite(STDERR, $text);
+ exit(1);
+}
+
+/**
+ * Prints an error message to STDERR and exits.
+ *
+ * @param string $appPath
+ * The path to the application.
+ * @param string $message
+ * The error message.
+ */
+function printErrorAndExit(string $appPath, string $message) {
+ $appName = basename($appPath, '.php');
+ fwrite(STDERR, "$appName: $message\n");
+ exit(1);
+}
+
+/*--------------------------------------------------------------------
+ *
+ * Command-line parsing.
+ *
+ * These functions parse the command line.
+ *
+ *--------------------------------------------------------------------*/
+
+/**
+ * Parses the command line and returns values.
+ *
+ * The command line is parsed and its values validated and returned in
+ * an associative array.
+ *
+ * @param array $argv
+ * The array of command-line arguments.
+ */
+function parseCommandLine(array $argv) {
+ //
+ // Set initial values
+ // ------------------
+ // Get the application name and set up defaults for all values.
+ $options = [];
+ $appPath = $options['appPath'] = array_shift($argv);
+ $appName = $options['appName'] = basename($appPath, '.php');
+
+ // Connection options.
+ $options['host'] = '';
+ $options['username'] = '';
+ $options['password'] = '';
+ $options['apikey'] = '';
+ $options['masquerade'] = '';
+
+ // Flags.
+ $options['format'] = 'auto';
+ $options['verbose'] = FALSE;
+
+ // Command.
+ $options['command'] = [];
+ $options['paths'] = [];
+ $options['flags'] = [];
+
+ $showVersion = FALSE;
+
+ $usage = "\nUse '--help' for a list of commands and options.";
+
+ //
+ // Parse arguments
+ // ---------------
+ // Loop through the arguments and look for options and commands.
+ // When a command name is found, all further arguments are considered
+ // part of that command.
+ $n = count($argv);
+ for ($i = 0; $i < $n; ++$i) {
+ // The first argument that does not start with '-' is the command.
+ // Add it to the command's argument list, along with all further
+ // arguments. It is up to the command to parse them.
+ $dash = mb_substr($argv[$i], 0, 1);
+ if ($dash !== '-') {
+ // Command found!
+ $options['command'] = array_slice($argv, $i);
+ break;
+ }
+
+ // Otherwise, parse generic options relating to the connection
+ // or flags.
+ //
+ // Ignore one or two leading dashes while parsing options.
+ $dashes = mb_substr($argv[$i], 0, 2);
+ if ($dashes === '--') {
+ $arg = mb_substr($argv[$i], 2);
+ }
+ else {
+ $arg = mb_substr($argv[$i], 1);
+ }
+
+ switch ($arg) {
+ // Immediate response options.
+ case 'help':
+ // Print help information and stop.
+ printCommandLineHelp($appName);
+ exit(0);
+
+ case 'version':
+ // Print application version and stop.
+ $showVersion = TRUE;
+ break;
+
+ // Connection options.
+ case 'host':
+ if (($i + 1) >= $n) {
+ printErrorAndExit(
+ $appName,
+ "A host name is required after '--$arg'.$usage");
+ }
+
+ ++$i;
+ $options['host'] = $argv[$i];
+ break;
+
+ case 'user':
+ case 'username':
+ if (($i + 1) >= $n) {
+ printErrorAndExit(
+ $appName,
+ "A user name is required after '--$arg'.$usage");
+ }
+
+ ++$i;
+ $options['username'] = $argv[$i];
+ break;
+
+ case 'password':
+ if (($i + 1) >= $n) {
+ printErrorAndExit(
+ $appName,
+ "A password is required after '--$arg'.$usage");
+ }
+
+ ++$i;
+ $options['password'] = $argv[$i];
+ break;
+
+ case 'apikey':
+ if (($i + 1) >= $n) {
+ printErrorAndExit(
+ $appName,
+ "API key is required after '--$arg'.$usage");
+ }
+
+ ++$i;
+ $options['apikey'] = $argv[$i];
+ break;
+
+ case 'masquerade':
+ if (($i + 1) >= $n) {
+ printErrorAndExit(
+ $appName,
+ "A masqueraded user is required after '--$arg'.$usage");
+ }
+
+ ++$i;
+ $options['masquerade'] = $argv[$i];
+ break;
+
+ // Flags.
+ case 'verbose':
+ $options['verbose'] = TRUE;
+ break;
+
+ case 'format':
+ if (($i + 1) >= $n) {
+ printErrorAndExit(
+ $appName,
+ "A format name is required after '--$arg'.$usage");
+ }
+
+ ++$i;
+ $value = $argv[$i];
+ switch ($value) {
+ // Principal values.
+ case 'auto':
+ case 'full':
+ case 'keyvalue':
+ case 'linux':
+ case 'text':
+ break;
+
+ // Synonyms.
+ case 'all':
+ case 'raw':
+ $value = 'full';
+ break;
+
+ case 'kv':
+ $value = 'keyvalue';
+ break;
+
+ case 'macos':
+ case 'osx':
+ case 'unix':
+ case 'bsd':
+ $value = 'linux';
+ break;
+
+ case 'default':
+ $value = 'auto';
+ break;
+
+ default:
+ printErrorAndExit(
+ $appName,
+ "Unknown format name: '$value'.$usage");
+ break;
+ }
+
+ $options['format'] = $value;
+ break;
+
+ default:
+ // Otherwise the option is not generic.
+ $a = $argv[$i];
+ printErrorAndExit(
+ $appName,
+ "Unknown option: '--$a'.$usage");
+ break;
+ }
+ }
+
+ //
+ // Handle version
+ // --------------
+ // The --version flag can be used with or without a host.
+ //
+ // Without a host, just show the client side's versions.
+ //
+ // With a host, map --version to the 'version' command.
+ if ($showVersion === TRUE) {
+ if (empty($options['host']) === TRUE) {
+ printVersionAndExit($options);
+ }
+
+ // Override any additional command given and make it "version".
+ $options['command'] = ["version"];
+ }
+
+ //
+ // Validate
+ // --------
+ // Check that a host name has been given.
+ if (empty($options['host']) === TRUE) {
+ printErrorAndExit(
+ $appName,
+ "A host name is required. Use '--host hostname'.$usage");
+ }
+
+ return $options;
+}
+
+/**
+ * Validates a command and its flags.
+ *
+ * @param array $options
+ * The command-line options array.
+ * @param bool $exitOnError
+ * (optional, default = TRUE) When TRUE, on an error a message is output
+ * and the application exits. When FALSE, on an error a message is output
+ * and FALSE is returned.
+ *
+ * @return bool
+ * Returns TRUE on successful validation, and FALSE otherwise.
+ */
+function validateCommand(array &$options, bool $exitOnError = TRUE) {
+ //
+ // Recognize command
+ // -----------------
+ // Check if the command is recognized. It can be a primary command name
+ // or one of the synonyms.
+ $appName = $options['appName'];
+ if ($exitOnError === TRUE) {
+ $usage = "\nUse '--help' for a list of commands and options.";
+ }
+ else {
+ $usage = "\nType 'help' for a list of commands and options.\n";
+ }
+
+ if (isset($options['command']) === FALSE) {
+ if ($exitOnError === TRUE) {
+ printErrorAndExit(
+ $appName,
+ "Missing command.$usage");
+ }
+
+ print("Missing command.$usage");
+ return FALSE;
+ }
+
+ $commandName = reset($options['command']);
+ $found = FALSE;
+
+ if (isset(COMMANDS[$commandName]) === TRUE) {
+ $found = TRUE;
+ }
+ else {
+ // Look through the synonyms of all commands. If found, swap the
+ // command name from the synonym to the primary name.
+ foreach (COMMANDS as $name => &$info) {
+ if (isset($info['synonyms']) === TRUE) {
+ if (in_array($commandName, $info['synonyms']) === TRUE) {
+ $found = TRUE;
+ $commandName = $name;
+ $options['command'][0] = $name;
+ break;
+ }
+ }
+ }
+ }
+
+ if ($found === FALSE) {
+ if ($exitOnError === TRUE) {
+ printErrorAndExit(
+ $appName,
+ "Unknown command: '$commandName'.$usage");
+ }
+
+ print("Unknown command: '$commandName'.$usage");
+ return FALSE;
+ }
+
+ //
+ // Check implementation
+ // --------------------
+ // Make sure the command has a function! If it doesn't, the command is
+ // not yet implemented.
+ if (isset(COMMANDS[$commandName]['function']) === FALSE) {
+ if ($exitOnError === TRUE) {
+ printErrorAndExit(
+ $appName,
+ "$commandName is not supported yet.");
+ }
+
+ print("$commandName is not supported yet.\n");
+ return FALSE;
+ }
+
+ //
+ // Handle return formats
+ // ---------------------
+ // Map the 'auto' return type to the actual type.
+ if ($options['format'] === 'auto') {
+ $options['format'] = COMMANDS[$commandName]['defaultFormat'];
+ }
+
+ // Check that only valid return types were provided.
+ if (empty(COMMANDS[$commandName]['formats']) === FALSE &&
+ in_array($options['format'], COMMANDS[$commandName]['formats']) === FALSE) {
+ $rt = $options['format'];
+ if ($exitOnError === TRUE) {
+ printErrorAndExit(
+ $appName,
+ "Unsupported format name '$rt' for '$commandName'.$usage");
+ }
+
+ print("Unsupported format name '$rt' for '$commandName'.$usage");
+ return FALSE;
+ }
+
+ //
+ // Parse flags and paths
+ // ---------------------
+ // Check that the flags make sense.
+ $pathsAndFlags = parseCommandFlags(
+ $appName,
+ $commandName,
+ $options['command']);
+ if ($pathsAndFlags === FALSE) {
+ return FALSE;
+ }
+
+ $options['paths'] = $pathsAndFlags['paths'];
+ $options['flags'] = $pathsAndFlags['flags'];
+
+ return TRUE;
+}
+
+/**
+ * Parses flags for a specific command.
+ *
+ * An array arguments for a command is split into:
+ * - Flags that start with '-' or '--'.
+ * - Paths or other non-dash arguments.
+ *
+ * Flags are parsed for the command and errors generated for unrecognized
+ * flags.
+ *
+ * Flags with a single dash are presumed to be single-letter flags
+ * (e.g. "-a"). If multiple letters are given, the flags are expanded
+ * into multiple flags (e.g. "-abc" becomes "-a", "-b", and "-c).
+ *
+ * Flags with a double dash are presumed to be multi-letter word flags
+ * and are not changed.
+ *
+ * Flag synonyms are transparently mapped to their primary names (e.g.
+ * "--stuff" becomes "-s" if "-s" is the primary name).
+ *
+ * @param string $appName
+ * The name of the command-line application.
+ * @param string $commandName
+ * The name of the command.
+ * @param array $args
+ * The arguments array from the basic command-line parser.
+ *
+ * @return array|bool
+ * Returns an associative array with 'flags' and 'paths' keys whose
+ * values are each arrays. The 'paths' array contains a list of
+ * paths or non-flag arguments, in order. The 'flags' array contains
+ * a list of flags. Returns FALSE on error.
+ */
+function parseCommandFlags(string $appName, string $commandName, array $args) {
+ //
+ // Split args
+ // ----------
+ // Divide up the incoming arguments into paths and flags. A flag starts
+ // with a '-' or '--', while a path does not.
+ $paths = [];
+ $flags = [];
+
+ array_shift($args);
+ foreach ($args as $arg) {
+ // Look at the first character to decide if the argument is a flag
+ // or a path.
+ $dash = mb_substr($arg, 0, 1);
+ if ($dash === '-') {
+ $flags[] = $arg;
+ }
+ else {
+ $paths[] = $arg;
+ }
+ }
+
+ //
+ // Expand flags
+ // ------------
+ // For single-dash options with multiple letters, split the letters into
+ // separate flags. For instance "-abc" becomes "-a", "-b", and "-c".
+ // Leave double-dash options alone.
+ $expandedFlags = [];
+ foreach ($flags as $flag) {
+ // Don't expand double-dash flags.
+ $firstTwo = mb_substr($flag, 0, 2);
+ if ($firstTwo === '--') {
+ $expandedFlags[] = $flag;
+ continue;
+ }
+
+ // No need to expand flags that are one letter after '-'.
+ $len = mb_strlen($flag);
+ if ($len === 2) {
+ $expandedFlags[] = $flag;
+ }
+
+ for ($i = 1; $i < $len; ++$i) {
+ $f = mb_substr($flag, $i, 1);
+ $expandedFlags[] = '-' . $f;
+ }
+ }
+
+ $flags = $expandedFlags;
+ unset($expandedFlags);
+
+ //
+ // Simplify flags
+ // --------------
+ // For all flags, map synonyms to their primary flag name.
+ $flagsInfo = COMMANDS[$commandName]['flags'];
+
+ $simplifiedFlags = [];
+ foreach ($flags as $flag) {
+ if (isset($flagsInfo[$flag]) === TRUE) {
+ // The flag is a primary flag name.
+ $simplifiedFlags[] = $flag;
+ }
+ else {
+ $found = FALSE;
+ foreach ($flagsInfo as $flagName => &$flagInfo) {
+ if (isset($flagInfo['synonyms']) === TRUE) {
+ foreach ($flagInfo['synonyms'] as $synonymName) {
+ if ($flag === $synonymName) {
+ // Map the synonym to the primary flag name.
+ $simplifiedFlags[] = $flagName;
+ $found = TRUE;
+ break 2;
+ }
+ }
+ }
+ }
+
+ // If the flag was not found as a synonym of anything, then
+ // report an error.
+ if ($found === FALSE) {
+ print("$commandName: Unrecognized option \"$flag\".\n");
+ return FALSE;
+ }
+ }
+ }
+
+ return [
+ 'paths' => $paths,
+ 'flags' => $simplifiedFlags,
+ ];
+}
+
+/*--------------------------------------------------------------------
+ *
+ * Utilities.
+ *
+ *--------------------------------------------------------------------*/
+
+/**
+ * Waits for all items to be enabled.
+ *
+ * This function enters an infinite loop. On each pass, it checks one
+ * or more of the indicated remote paths to see if the corresponding
+ * item is enabled. If any item is not enabled, the pass completes
+ * and the process sleeps for several seconds before the next pass
+ * through the loop.
+ *
+ * If all of the items are enabled, then all pending operations on them
+ * are presumed to be complete and this function returns.
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param string $commandName
+ * The name of the command that called this function.
+ * @param string[] $remotePaths
+ * The remote file system paths of items to monitor.
+ * @param bool $verbose
+ * (optional, default = FALSE) When TRUE, print a "." after each check
+ * of the items. When FALSE, print nothing.
+ */
+function sync(
+ FolderShareConnect $server,
+ string $commandName,
+ array $remotePaths,
+ bool $verbose = FALSE) {
+
+ // Repeatedly get each entity and check if they are all enabled.
+ // If any one of them is not, wait and repeat.
+ while (TRUE) {
+ $anyDisabled = FALSE;
+ foreach ($remotePaths as $remotePath) {
+ try {
+ $response = $server->getFileOrFolder($remotePath);
+ if (isset($response['systemdisabled']) === TRUE &&
+ isset($response['systemdisabled'][0]) === TRUE &&
+ isset($response['systemdisabled'][0]['value']) === TRUE &&
+ $response['systemdisabled'][0]['value'] === TRUE) {
+ $anyDisabled = TRUE;
+ break;
+ }
+ }
+ catch (\Exception $e) {
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ if ($anyDisabled === FALSE) {
+ break;
+ }
+
+ if ($verbose === TRUE) {
+ print(".");
+ flush();
+ }
+
+ sleep(SYNC_WAIT_SECONDS);
+ }
+}
+
+/*--------------------------------------------------------------------
+ *
+ * Operations.
+ *
+ *--------------------------------------------------------------------*/
+
+/**
+ * Prints the name of the current user.
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ */
+function runWhoami(FolderShareConnect $server, array $options) {
+ $username = $server->getUserName();
+ if (empty($username) === TRUE) {
+ $username = ANONYMOUS;
+ }
+
+ printf("%s\n", $username);
+ return TRUE;
+}
+
+/**
+ * Prints the name of the current host.
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ */
+function runHostname(FolderShareConnect $server, array $options) {
+ printf("%s\n", $server->getHostName());
+ return TRUE;
+}
+
+/**
+ * Prints host configuration parameters.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | --help | Show help on command. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ */
+function runConfig(FolderShareConnect $server, array $options) {
+ //
+ // Execute
+ // -------
+ // Report configuration.
+ try {
+ $response = $server->getServerConfiguration();
+
+ if ($options['format'] === 'text') {
+ print(FolderShareFormat::formatAsConfiguration($response));
+ }
+ else {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Prints file, folder, and storage usage for a user.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | --help | Show help on command. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ */
+function runUsage(FolderShareConnect $server, array $options) {
+ //
+ // Execute
+ // -------
+ // Report usage.
+ try {
+ $response = $server->getUsage();
+
+ if ($options['format'] === 'text') {
+ print(FolderShareFormat::formatAsUsage($response));
+ }
+ else {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Prints software version numbers.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | --help | Show help on command. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection. If the connection is NULL, only the client
+ * side's version information is returned.
+ * @param array $options
+ * The command-line options array.
+ * @param bool $doConnect
+ * (optional, default = TRUE) When TRUE, a request is sent to the server
+ * to get its version information, which is then merged with information
+ * about the client API and PHP version. When FALSE, no request is sent
+ * and the returned array only includes the client API and PHP version.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ */
+function runVersion(
+ FolderShareConnect $server,
+ array $options,
+ bool $doConnect = TRUE) {
+ //
+ // Execute
+ // -------
+ // Report configuration.
+ try {
+ $response = $server->getVersion($doConnect);
+
+ if (is_array($response) === FALSE) {
+ print_r($response);
+ }
+ else {
+ if (isset($response['client']) === FALSE) {
+ $response['client'] = [];
+ }
+
+ // Add an application entry to the front of the client version list.
+ $response['client'] = array_merge(
+ [
+ 'application' => [
+ 'name' => NAME,
+ 'version' => VERSION,
+ ],
+ ],
+ $response['client']);
+
+ if ($options['format'] === 'text') {
+ print(FolderShareFormat::formatAsVersion($response));
+ }
+ else {
+ print_r($response);
+ }
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Prints file or folder status.
+ *
+ * This command emulates the Linux/macOS/BSD "stat" command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -t | Show a terse form of the information. |
+ * | --help | Show help on command. |
+ * | --terse| Same as -t. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * Linux "stat" supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -c | Use a custom output format. |
+ * | -f | Show file system status instead of file status. |
+ * | -L | Follow symbolic links. |
+ * | -t | Show a terse form of the information. |
+ * | --dereference | Same as -L. |
+ * | --filesystem | Same as -f. |
+ * | --format | Same as -c. |
+ * | --help | Show help on command. |
+ * | --terse | Same as -t. |
+ * | --version | Show version information. |
+ *
+ * BSD and macOS "stat" support the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -f | Use a custom output format. |
+ * | -F | Add special symbols on files and folders. |
+ * | -l | Same as "ls -lT". |
+ * | -L | Follow symbolic links. |
+ * | -n | Suppress newline after each item. |
+ * | -q | Suppress error messages. |
+ * | -r | Show raw information from the 'stat' syscall. |
+ * | -s | Show information in a shell compatible format. |
+ * | -t | Display time stamps in a custom output format. |
+ * | -x | Show a verbose output format similar to Linux. |
+ *
+ * BSD supports the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -H | Treat arguments as hex NFS file handles. |
+ *
+ * There is no POSIX "stat" command.
+ *
+ * The set of flags for Linux and macOS/BSD are entirely disjoint -
+ * there is nothing in common. Some flags, like -t, show up in both
+ * versions of "stat", but with different meanings.
+ */
+function runStat(FolderShareConnect $server, array $options) {
+ //
+ // Parse flags
+ // -----------
+ // Synonyms have already been mapped to primary flag names.
+ $terseMode = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-t':
+ $terseMode = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // If there are no paths given, default to '/'.
+ $remotePaths = $options['paths'];
+ if (empty($remotePaths) === TRUE) {
+ $remotePaths = ['/'];
+ }
+
+ //
+ // Execute
+ // -------
+ // Get entity information.
+ foreach ($remotePaths as $remotePath) {
+ try {
+ $response = $server->getFileOrFolder($remotePath);
+
+ if (empty($response) === FALSE) {
+ if ($options['format'] === 'linux') {
+ print(FolderShareFormat::formatAsLinuxStat($response, $terseMode));
+ }
+ elseif ($options['format'] === 'text') {
+ print(FolderShareFormat::formatAsText($response));
+ }
+ else {
+ print_r($response);
+ }
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Prints extended attributes values.
+ *
+ * This command does not emulate any particular Linux/macOS/BSD
+ * command, though it is named after the macOS "xattr" command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | --help | Show help on command. |
+ * | --ATTR | The name of an attribute. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ */
+function runXattr(FolderShareConnect $server, array $options) {
+ //
+ // Parse flags
+ // -----------
+ // Synonyms have already been mapped to primary flag names.
+ $attrs = [];
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '--disabled':
+ $attrs['systemdisabled'] = 'bool';
+ break;
+
+ case '--hidden':
+ $attrs['systemhidden'] = 'bool';
+ break;
+
+ case '--kind':
+ $attrs['kind'] = 'string';
+ break;
+
+ case '--mime':
+ $attrs['mime'] = 'string';
+ break;
+
+ case '--size':
+ $attrs['size'] = 'int';
+ break;
+ }
+ }
+
+ if (empty($attrs) === TRUE) {
+ // Default to all available attributes.
+ $attrs = [
+ 'systemdisabled' => 'bool',
+ 'systemhidden' => 'bool',
+ 'kind' => 'string',
+ 'mime' => 'string',
+ 'size' => 'int',
+ ];
+ }
+
+ $nAttrs = count($attrs);
+
+ //
+ // Validate
+ // --------
+ // If there are no paths given, default to '/'.
+ $remotePaths = $options['paths'];
+ if (empty($remotePaths) === TRUE) {
+ $remotePaths = ['/'];
+ }
+
+ //
+ // Execute
+ // -------
+ // Get entity information.
+ foreach ($remotePaths as $remotePath) {
+ try {
+ $response = $server->getFileOrFolder($remotePath);
+
+ foreach ($attrs as $attr => $cast) {
+ if (isset($response[$attr]) === TRUE &&
+ isset($response[$attr][0]) === TRUE &&
+ isset($response[$attr][0]['value']) === TRUE) {
+ $value = $response[$attr][0]['value'];
+ if ($nAttrs > 1) {
+ print("$attr: ");
+ }
+
+ switch ($cast) {
+ case 'bool':
+ if (empty($value) === TRUE || $value === FALSE) {
+ print("false\n");
+ }
+ else {
+ print("true\n");
+ }
+ break;
+
+ case 'string':
+ print((string) $value . "\n");
+ break;
+
+ case 'int':
+ print((int) $value . "\n");
+ break;
+ }
+ }
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Waits until a file or folder becomes enabled.
+ *
+ * This command does not emulate any particular Linux/macOS/BSD
+ * command, though it is very roughly modeled after the "sync"
+ * command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | --help | Show help on command. |
+ * | -v | Be verbose. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ */
+function runSync(FolderShareConnect $server, array $options) {
+ //
+ // Parse flags
+ // -----------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-v':
+ $verbose = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // If there are no paths given, default to '/'.
+ $remotePaths = $options['paths'];
+ if (empty($remotePaths) === TRUE) {
+ $remotePaths = ['/'];
+ }
+
+ //
+ // Execute
+ // -------
+ // Repeatedly get each entity and check if they are all enabled.
+ // If any one of them is not, wait and repeat.
+ sync($server, reset($options['command']), $remotePaths, $verbose);
+ return TRUE;
+}
+
+/**
+ * Prints a list of files and folders.
+ *
+ * This command emulates the Linux/macOS/BSD "ls" command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -d | List directories without recursion. |
+ * | -F | Add special symbols on files and folders. |
+ * | -i | Include the entity ID. |
+ * | -l | Show a long listing format. |
+ * | -R | Recursively list directories. |
+ * | -s | Include the size of each item. |
+ * | -S | Sort by size. |
+ * | -t | Sort by modification time. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * POSIX, Linux, macOS, and BSD "ls" support the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -1 | Force one-column output. |
+ * | -a | Show all entries, including "." entries. |
+ * | -c | Show last modified time. |
+ * | -C | Force multi-column output. |
+ * | -d | List directories without recursion. |
+ * | -f | Do not sort. Also turns on -a. |
+ * | -F | Add special symbols on files and folders. |
+ * | -g | Like -l, skip owner names (just show groups). |
+ * | -i | Include the inode number. |
+ * | -l | Show a long listing format. |
+ * | -L | Show info on symbolic link target. |
+ * | -o | Like -l, skip group names (just show owners). |
+ * | -q | Replace non-printing characters with ?. |
+ * | -r | Reverse the sort order. |
+ * | -R | Recursively list directories. |
+ * | -s | Include the size of each item. |
+ * | -t | Sort by modification time. |
+ * | -u | Sort by last access time. |
+ *
+ * Linux, macOS, and BSD support the following additional flags,
+ * but not POSIX:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -h | Simplify numbers with byte suffixes (e.g. MB). |
+ *
+ * Linux and macOS support the following additional flags, but
+ * not BSD:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -A | Same as -a, but skip "." and "..". |
+ * | -H | Follow symbolic links. |
+ * | -k | Show sizes in kilobytes, not blocks. |
+ * | -m | Full width output with comma separated entries. |
+ * | -n | Like -l, show numeric IDs. |
+ * | -p | Append '/' to directory names. |
+ * | -S | Sort by size. |
+ * | -x | Order values side-to-side, instead of up-and-down.|
+ *
+ * Linux supports the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -b | Show non-printing characters with C escape codes.|
+ * | -B | Ignore backup files ending with "~". |
+ * | -D | Generate output for Emacs dired mode. |
+ * | -G | With -l, skip group names. |
+ * | -I | Suppress entries matching a pattern. |
+ * | -N | Print raw names, including control characters. |
+ * | -Q | Enclose names in double-quotes. |
+ * | -T | Set tab stops. |
+ * | -U | Do not sort. |
+ * | -v | Use a natural sort for numbers in text. |
+ * | -w | Set output width. |
+ * | -X | Sort by file name extension. |
+ * | --all | Same as -a. |
+ * | --almost-all | Same as -A. |
+ * | --author | With -l, print the author of each file. |
+ * | --escape | Same as -b. |
+ * | --block-size | Show sizes in blocks. |
+ * | --classify | Same as -F. |
+ * | --color | Colorize output. |
+ * | --dereference| Same as -L. |
+ * | --dereference-command-line | Same as -H. |
+ * | --dereference-command-line-symlink-to-dir| Follow symbolic links to dirs.|
+ * | --directory | Same as -d. |
+ * | --dired | Same as -D. |
+ * | --file-type | Same as -F, but don't add '*' on executables.|
+ * | --format | Customize formatting. |
+ * | --full-time | Same as -l, but use full ISO times. |
+ * | --help | Show help on command. |
+ * | --hide | Hide entries that match a pattern. |
+ * | --hide-control-chars| Same as -q. |
+ * | --human-readable | Same as -h. |
+ * | --ignore | Same as -I. |
+ * | --ignore-backups | Same as -B. |
+ * | --indicator-style| Same as -p, --file-type, or -F. |
+ * | --inode | Same as -i. |
+ * | --literal | Same as -N. |
+ * | --no-group | Same as -G. |
+ * | --quote-name | Same as -Q. |
+ * | --quoting-style| Same as -Q but specify quoting. |
+ * | --numeric-uid-gid | Same as -n. |
+ * | --recursive | Same as -R. |
+ * | --reverse | Same as -r. |
+ * | --show-control-characters| Show non-printing characters as-is.|
+ * | --si | Same as -h, but use powers of 1000 not 1024.|
+ * | --size | Same as -s. |
+ * | --sort | Select the sort key. |
+ * | --tabsize | Same as -T. |
+ * | --time | Select different time value. |
+ * | --time-style | Select time formatting. |
+ * | --width | Same as -w. |
+ * | --version | Show version information. |
+ *
+ * macOS supports the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -@ | With -l, show extended attributes. |
+ * | -b | Show non-printing characters with octal escapes. |
+ * | -B | Same as -b, but use C escape codes too. |
+ * | -e | With -l, show ACLs. |
+ * | -G | Colorize output. |
+ * | -O | With -l, include file flags. |
+ * | -P | Show in on symbolic link, not target. |
+ * | -T | With -l, show full detailed time. |
+ * | -U | Sort by creation time. |
+ * | -v | Show non-printing characters as-is. |
+ * | -w | Force raw printing of non-printable characters. |
+ * | -W | Show whiteouts when scanning directories. |
+ */
+function runLs(FolderShareConnect $server, array $options) {
+ //
+ // Parse options
+ // -------------
+ // Synonyms have already been mapped to primary flag names.
+ $recurse = FALSE;
+ $formatFlags = [];
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-d':
+ $recurse = FALSE;
+ break;
+
+ case '-R':
+ $recurse = TRUE;
+ break;
+
+ case '-F':
+ case '-i':
+ case '-l':
+ case '-s':
+ case '-S':
+ case '-t':
+ $formatFlags[] = $flagName;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // If there are no paths given, default to '/'.
+ $remotePaths = $options['paths'];
+ if (empty($remotePaths) === TRUE) {
+ $remotePaths = ['/'];
+ }
+
+ if ($recurse === TRUE && $options['format'] !== 'linux') {
+ $commandName = reset($options['command']);
+ print("$commandName: Recursive lists must use a 'linux' output format.\n");
+ return FALSE;
+ }
+
+ //
+ // Execute
+ // -------
+ // Get a list of entities. Format the response using the flags, if any.
+ while (empty($remotePaths) === FALSE) {
+ $remotePath = array_shift($remotePaths);
+
+ if ($recurse === TRUE) {
+ print("$remotePath:\n");
+ }
+
+ // Get the list.
+ try {
+ switch ($remotePath) {
+ case '/':
+ case '/personal':
+ $response = $server->getRootItems('personal');
+ break;
+
+ case '/public':
+ $response = $server->getRootItems('public');
+ break;
+
+ default:
+ $response = $server->getDescendants($remotePath);
+ break;
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+
+ // Print the response.
+ if (empty($response) === FALSE) {
+ switch ($options['format']) {
+ case 'linux':
+ print(FolderShareFormat::formatAsLinuxLs($response, $formatFlags));
+ break;
+
+ case 'text':
+ print(FolderShareFormat::formatAsText($response));
+ break;
+
+ default:
+ print_r($response);
+ }
+ }
+
+ // Update the paths array by adding child paths to the front.
+ if ($recurse === TRUE) {
+ $recursePaths = [];
+ if (is_array($response) === TRUE) {
+ // Loop through the response and create a path for each folder item.
+ foreach ($response as $r) {
+ if (isset($r['path']) === TRUE && isset($r['kind']) === TRUE) {
+ $k = $r['kind'];
+ if ($k === 'folder') {
+ $recursePaths[] = $r['path'];
+ }
+ }
+ }
+ }
+
+ // Add recursion paths to the front of the path list.
+ // This causes us to do a depth-first traversal.
+ $remotePaths = ($recursePaths + $remotePaths);
+
+ if (empty($remotePaths) === FALSE) {
+ print("\n");
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Deletes files and folders.
+ *
+ * This command emulates the Linux/macOS/BSD "rm" command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -d | Remove directories as well as files. |
+ * | -f | Don't prompt for comfirmation and ignore errors. |
+ * | -R | Recursively delete directory trees (implies -d). |
+ * | -r | Same as -R. |
+ * | -v | Print the name of each item as it is deleted. |
+ * | --help | Show help on command. |
+ *
+ * By default, this method does not remove folders. Use "-d"
+ * to remove a file or folder.
+ *
+ * By default, this method does not recurse and will not remove
+ * folders that are not empty. Use "-r" to recurse.
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * POSIX, Linux, macOS, and BSD "rm" support the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -f | Don't prompt for comfirmation and ignore errors. |
+ * | -i | Interactively confirm each item before deletion. |
+ * | -R | Recursively delete directory trees (implies -d). |
+ * | -r | Same as -R. |
+ *
+ * Linux, macOS, and BSD "rm" support the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -v | Print the name of each item as it is deleted. |
+ *
+ * macOS and BSD "rm" support the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -d | Remove directories as well as files. |
+ * | -P | Overwrite regular files before deleting them. |
+ * | -W | Undelete white-out deleted files. |
+ *
+ * BSD "rm" supports the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -I | Interactively confirm deletes > 3 files or dirs. |
+ * | -x | Do not cross mount points during recursive delete.|
+ *
+ * All Linux versions of "rm" support the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -I | Interactively confirm deletes > 3 files or dirs. |
+ * | --force | Same as -f. |
+ * | --help | Show help on command. |
+ * | --interactive=WHEN | Same as -i and -I. |
+ * | --no-preserve-root | Don't prevent '/' delete. |
+ * | --one-file-system | Same as BSD -x. |
+ * | --preserve-root | Do prevent '/' delete. |
+ * | --recursive | Same as -R. |
+ * | --verbose | Same as -v. |
+ * | --version | Show version information. |
+ *
+ * RedHat Linux "rm" supports the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -d | Remove directories as well as files. |
+ * | --directory | Same as -d. |
+ *
+ * CentOS Linux "rm" supports the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -d | Remove directories as well as files. |
+ * | --dir | Same as -d. |
+ */
+function runRm(FolderShareConnect $server, array $options) {
+ //
+ // Parse flags
+ // -----------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+ $recurse = FALSE;
+ $fileOrFolder = FALSE;
+ $ignoreErrors = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-d':
+ $fileOrFolder = TRUE;
+ break;
+
+ case '-f':
+ $ignoreErrors = TRUE;
+ break;
+
+ case '-r':
+ $recurse = TRUE;
+ $fileOrFolder = TRUE;
+ break;
+
+ case '-v':
+ $verbose = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // There must be at least one path.
+ if (empty($options['paths']) === TRUE) {
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote file path.\n");
+ return FALSE;
+ }
+
+ //
+ // Execute
+ // -------
+ // Remove each of the indicated entities.
+ foreach ($options['paths'] as $remotePath) {
+ if ($verbose === TRUE) {
+ print("$remotePath\n");
+ }
+
+ try {
+ // There is normally no response.
+ if ($fileOrFolder === FALSE) {
+ $response = $server->deleteFile($remotePath);
+ }
+ else {
+ $response = $server->deleteFileOrFolder($remotePath, $recurse);
+ }
+
+ if (empty($response) === FALSE) {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ if ($ignoreErrors === TRUE) {
+ // The -f flag was given, which blocks does-not-exist error
+ // messages and setting the exit code to failure on messages.
+ continue;
+ }
+
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Deletes empty folders.
+ *
+ * This command emulates the Linux/macOS/BSD "rmdir" command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -v | Show the name of each item deleted. |
+ * | --help | Show help on command. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * POSIX, Linux, macOS, and BSD "rmdir" support the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -p | Remove every directory on the path. |
+ *
+ * Linux supports the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -v | Show the name of each item deleted. |
+ * | --help | Show help on command. |
+ * | --ignore-fail-on-non-empty | Ignore non-empty errors. |
+ * | --parents | Same as -p. |
+ * | --verbose | Same as -v. |
+ * | --version | Show version information. |
+ *
+ * BSD supports the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -v | Show the name of each item deleted. |
+ */
+function runRmdir(FolderShareConnect $server, array $options) {
+ //
+ // Parse flags
+ // -----------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-v':
+ $verbose = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // There must be at least one path.
+ if (empty($options['paths']) === TRUE) {
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote file path.\n");
+ return FALSE;
+ }
+
+ //
+ // Execute
+ // -------
+ // Remove each of the indicated entities.
+ foreach ($options['paths'] as $remotePath) {
+ if ($verbose === TRUE) {
+ print("$remotePath\n");
+ }
+
+ try {
+ // There is normally no response.
+ $response = $server->deleteFolder($remotePath);
+ if (empty($response) === FALSE) {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Creates a new root folder or subfolder.
+ *
+ * This command emulates the Linux/macOS/BSD "mkdir" command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -v | Show the name of each item deleted. |
+ * | --help | Show help on command. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * POSIX, Linux, macOS, and BSD "mkdir" support the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -m | Set the permissions mode for new directories. |
+ * | -p | Create parent directories too, if needed. |
+ * | -v | Show the name of each item created. |
+ *
+ * Linux has the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | --help | Show help on command. |
+ * | --mode | Same as -m. |
+ * | --parents | Same as -p. |
+ * | --verbose | Same as -v. |
+ * | --version | Show version information. |
+ */
+function runMkdir(FolderShareConnect $server, array $options) {
+ //
+ // Parse options
+ // -------------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-v':
+ $verbose = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // There must be at least one path.
+ if (empty($options['paths']) === TRUE) {
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote file path.\n");
+ return FALSE;
+ }
+
+ //
+ // Execute
+ // -------
+ // Create a series of directories.
+ //
+ // If the path is of the form /ROOT then create a root folder.
+ // Otherwise if the path is of the form /ROOT/MORE... then create
+ // a subfolder. This requires that we parse the path a bit here,
+ // before calling the server.
+ foreach ($options['paths'] as $remotePath) {
+ $indexOfLastSlash = mb_strrpos($remotePath, '/');
+ if ($indexOfLastSlash === FALSE) {
+ // No '/' was found at all! Since paths must start with a slash,
+ // this is an error.
+ $commandName = reset($options['command']);
+ print("$commandName: Folder paths must start with '/'.\n");
+ return FALSE;
+ }
+
+ if ($indexOfLastSlash === 0) {
+ // The path has only one '/' and it is at the start of the path.
+ // The path therefore names a new root folder to create.
+ //
+ // Remove the '/' before creating the root folder.
+ $name = mb_substr($remotePath, ($indexOfLastSlash + 1));
+ $parentPath = '/';
+ }
+ else {
+ // The path contains more than one '/'. Pull out the name after
+ // the last '/' and the path before it, then create a subfolder.
+ $name = mb_substr($remotePath, ($indexOfLastSlash + 1));
+ $parentPath = mb_substr($remotePath, 0, $indexOfLastSlash);
+ }
+
+ if ($verbose === TRUE) {
+ print("Create folder \"$remotePath\"\n");
+ }
+
+ try {
+ // There is normally no response.
+ if ($parentPath === '/') {
+ $response = $server->newRootFolder($name);
+ }
+ else {
+ $response = $server->newFolder($parentPath, $name);
+ }
+
+ if (empty($response) === FALSE) {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Moves a file or folder to a new location.
+ *
+ * This command emulates the Linux/macOS/BSD "mv" command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -n | Do not overwrite an existing file. |
+ * | -v | Show the name of each item moved. |
+ * | --help | Show help on command. |
+ * | --sync | Sync with server by waiting for move to complete.|
+ *
+ * This command always recursively copies folders.
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * POSIX, Linux, macOS, and BSD "mv" support the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -f | Don't prompt for comfirmation and ignore errors. |
+ * | -i | Interactively confirm each item before moving. |
+ *
+ * Linux, macOS, and BSD have the following additional flags, but
+ * not POSIX:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -n | Do not overwrite an existing file. |
+ * | -v | Show the name of each item moved. |
+ *
+ * BSD has the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -h | Do not follow target symbolic links. |
+ *
+ * Linux has the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -S | Override the usual backup suffix. |
+ * | -t | Move everything into the target directory. |
+ * | -T | Treat the target as a file, not a directory. |
+ * | -u | Only move if the source is newer. |
+ * | --force | Same as -f. |
+ * | --help | Show help on command. |
+ * | --interactive | Same as -i. |
+ * | --no-clobber | Same as -n. |
+ * | --no-target-directory | Same as -T. |
+ * | --suffix | Same as -S. |
+ * | --target-directory | Same as -t. |
+ * | --update | Same as -u. |
+ * | --verbose | Same as -v. |
+ * | --version | Show version information. |
+ */
+function runMv(FolderShareConnect $server, array $options) {
+ //
+ // Parse options
+ // -------------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+ $overwrite = TRUE;
+ $sync = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-n':
+ $overwrite = FALSE;
+ break;
+
+ case '-v':
+ $verbose = TRUE;
+ break;
+
+ case '--sync':
+ $sync = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // The command has one of two forms:
+ // - mv source destination
+ // - mv source1 source2 source3... destination
+ //
+ // The first form is a degenerate case of the second that supports
+ // optional renaming of the source. In that case the destination does
+ // not refer to an entity that exists, yet.
+ //
+ // However, only the server knows whether a destination exists. So
+ // we need to break this down into a loop of source-to-destination
+ // operations and watch for failure.
+ //
+ // Confirm there are at least two paths given.
+ switch (count($options['paths'])) {
+ case 0:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote file paths.\n");
+ return FALSE;
+
+ case 1:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote destination file path.\n");
+ return FALSE;
+
+ default:
+ $remotePaths = $options['paths'];
+ $destinationPath = array_pop($remotePaths);
+ break;
+ }
+
+ //
+ // Execute
+ // -------
+ // Move a series of items to the same destination. Abort on the first error.
+ foreach ($remotePaths as $remotePath) {
+ if ($verbose === TRUE) {
+ print("$remotePath\n");
+ }
+
+ try {
+ // There is normally no response.
+ $response = $server->move($remotePath, $destinationPath, $overwrite);
+ if (empty($response) === FALSE) {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ if ($sync === TRUE) {
+ sync($server, reset($options['command']), [$destinationPath], $verbose);
+ }
+
+ return TRUE;
+}
+
+/**
+ * Copies a file or folder to a new location.
+ *
+ * This command emulates the Linux/macOS/BSD "cp" command.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -n | Do not overwrite existing files. |
+ * | -v | Show the name of each item copied. |
+ * | --help | Show help on command. |
+ * | --sync | Sync with server by waiting for copy to complete.|
+ *
+ * This command always recursively copies folders.
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * POSIX, Linux, macOS, and BSD "cp" support the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -f | Don't prompt for comfirmation and ignore errors. |
+ * | -H | With -R, follow command line symbolic links. |
+ * | -i | Interactively confirm each item before copying. |
+ * | -L | With -R, follow recursive symbolic links. |
+ * | -P | With -R, don't follow symbolic links (default). |
+ * | -p | Preserve dates, permissions, and owner on copy. |
+ * | -R | Recursively copy trees. |
+ *
+ * Linux, macOS, and BSD have the following additional flags, but
+ * not POSIX:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -a | Same as -pPR. |
+ * | -n | Do not overwrite existing files. |
+ * | -v | Show the name of each item copied. |
+ *
+ * BSD has the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -l | Hard link files instead of copying. |
+ * | -x | Do not cross mount points during recursive copy. |
+ *
+ * macOS has the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -c | Copy files useing clonefile. |
+ * | -X | Do not copy extended attributes. |
+ *
+ * Linux has the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -b | Make a backup of each destination file. |
+ * | -d | Same as -P --preserve=links. |
+ * | -l | Hard link files instead of copying. |
+ * | -n | Do not overwrite existing files. |
+ * | -r | Same as -R. |
+ * | -s | Symbolic link files instead of copying. |
+ * | -S | Override backup file name suffix. |
+ * | -t | Specify destination (target) directory. |
+ * | -T | Treat destination as a file, not a directory. |
+ * | -u | Copy only if the source file is newer. |
+ * | -x | Do not cross mount points during recursive copy. |
+ * | --archive| Same as -a. |
+ * | --backup | Same as -b. |
+ * | --copy-contents | Copy special files when recursive. |
+ * | --dereference | Same as -L. |
+ * | --force | Same as -f. |
+ * | --help | Show help on command. |
+ * | --interactive | Same as -i. |
+ * | --link | Same as -l. |
+ * | --no-clobber | Same as -n. |
+ * | --no-dereference| Same as -P. |
+ * | --no-preserve | Don't preserve attributes on copy. |
+ * | --no-target-directory| Same as -T. |
+ * | --parents | Use full source file name under dir. |
+ * | --one-file-system| Same as -x. |
+ * | --preserve | Preserve specific attributes on copy. |
+ * | --recursive | Same as -R. |
+ * | --reflink | Control clones. |
+ * | --remove-directories| Remove existing destinations first. |
+ * | --sparse | Control sparse files. |
+ * | --strip-trailing-slashes| Remove last slashes in source. |
+ * | --symbolic-link | Same as -s. |
+ * | --suffix | Same as -S. |
+ * | --target-directory| Same as -t. |
+ * | --update | Same as -u. |
+ * | --verbose | Same as -v. |
+ * | --version | Show version information. |
+ */
+function runCp(FolderShareConnect $server, array $options) {
+ //
+ // Parse options
+ // -------------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+ $overwrite = TRUE;
+ $sync = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-n':
+ $overwrite = FALSE;
+ break;
+
+ case '-v':
+ $verbose = TRUE;
+ break;
+
+ case '--sync':
+ $sync = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // The command has one of two forms:
+ // - cp source destination
+ // - cp source1 source2 source3... destination
+ //
+ // The first form is a degenerate case of the second that supports
+ // optional renaming of the copy. In that case the destination does
+ // not refer to an entity that exists, yet.
+ //
+ // However, only the server knows whether a destination exists. So
+ // we need to break this down into a loop of source-to-destination
+ // operations and watch for failure.
+ //
+ // Confirm there are at least two paths given.
+ switch (count($options['paths'])) {
+ case 0:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote file paths.\n");
+ return FALSE;
+
+ case 1:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote destination file path.\n");
+ return FALSE;
+
+ default:
+ $remotePaths = $options['paths'];
+ $destinationPath = array_pop($remotePaths);
+ break;
+ }
+
+ //
+ // Execute
+ // -------
+ // Copy a series of items to the same destination. Abort on the first error.
+ foreach ($remotePaths as $remotePath) {
+ if ($verbose === TRUE) {
+ print("$remotePath\n");
+ }
+
+ try {
+ // There is normally no response.
+ $response = $server->copy($remotePath, $destinationPath, $overwrite);
+ if (empty($response) === FALSE) {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ if ($sync === TRUE) {
+ sync($server, reset($options['command']), [$destinationPath], $verbose);
+ }
+
+ return TRUE;
+}
+
+/**
+ * Updates file and folder field values.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -v | Show the name of each item modified. |
+ * | --help | Show help on command. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * There is no standard Linux/macOS/BSD command for changing file metadata.
+ *
+ * The Linux 'getfattr' and 'setfattr' commands get and set arbitrary file
+ * metadata:
+ * - getfattr -d files...
+ * Print all metadata.
+ *
+ * - getfattr -n fieldname files...
+ * Print the metadata for the selected field.
+ *
+ * - setfattr -n fieldname -v fieldvalue files...
+ * Write the metadata for the selected field.
+ *
+ * - setfattr -x fieldname files...
+ * Delete the metadata for the selected field.
+ *
+ * The Linux commands support the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -e | Set text encoding. |
+ * | -h | Apply to link, not target of link. |
+ * | -m | Only show fields that match a pattern. |
+ * | -R | Recurse into directories. |
+ * | -L | Follow symbolic links. |
+ * | -P | Do not follow symbolic links. |
+ * | --absolute-name | Don't remove leading '/'. |
+ * | --dump | Same as -d. |
+ * | --encoding| Same as -e. |
+ * | --help | Show help on command. |
+ * | --logical | Same as -L. |
+ * | --match | Same as -m. |
+ * | --no-dereference| Same as -h. |
+ * | --only-values | Only print out field values. |
+ * | --physical| Same as -P. |
+ * | --recursive| Same as -r. |
+ * | --version | Show version information. |
+ *
+ * The macOS 'xattr' command gets, sets, and deletes arbitrary file metadata:
+ * - xattr files...
+ * Print all metadata.
+ *
+ * - xattr -p fieldname files...
+ * Print the metadata for the selected field.
+ *
+ * - xattr -w fieldname fieldvalue files...
+ * Write the metadata for the selected field.
+ *
+ * - xattr -d fieldname files...
+ * Delete the metadata for the selected field.
+ *
+ * - xattr -c files...
+ * Clear all metadata.
+ *
+ * The macOS commands support the following additional flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -l | Long form that shows field names and values. |
+ * | -r | Recurse into directories. |
+ * | -s | Apply to link, not target of link. |
+ * | -v | Show the name of each item modified. |
+ * | -x | Print output in hex. |
+ *
+ * The BSD 'getextattr', 'lsextattr', 'rmextattr', and 'setextattr' commands
+ * get, list, delete, and set arbitrary file metadata:
+ * - lsextattr namespace files...
+ * Print all metadata. 'namespace' is either 'user' or 'system'.
+ *
+ * - getextattr namespace fieldname files...
+ * Print the metadata for the selected field.
+ *
+ * - setextattr namespace fieldname fieldvalue files...
+ * Write the metadata for the selected field.
+ *
+ * - rmextattr namespace fieldname files...
+ * Delete the metadata for the selected field.
+ *
+ * The BSD commands support the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -f | Don't prompt for comfirmation and ignore errors. |
+ * | -h | Apply to link, not target of link. |
+ * | -i | Read attributes from stdin. |
+ * | -n | NULL terminate output data. |
+ * | -q | Be quiet, don't print path name or errors. |
+ * | -s | Convert non-printing characters to escapes. |
+ * | -x | Print output in hex. |
+ *
+ * This FolderShare "update" supports a structure similar to the Linux,
+ * macOS, and BSD commands, but not identical to any of them.
+ *
+ * - FolderShare does not have arbitrary metadata - the fields have
+ * to be predefined by the FolderShare module, by third-party
+ * modules, or by the site administrator. So Linux/macOS/BSD-style
+ * operations to create or delete metadata fields are not meaningful.
+ *
+ * - The 'stat' command already outputs all entity fields. All we need
+ * here is a command to update them.
+ *
+ * - The Linux, macOS, and BSD commands all have a form that gives
+ * the name of a field, the value, and a list of files. We use
+ * that same form.
+ */
+function runUpdate(FolderShareConnect $server, array $options) {
+ //
+ // Parse options
+ // -------------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-v':
+ $verbose = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // The command must have at least:
+ // - A field name.
+ // - A field value.
+ // - A path.
+ //
+ // All further items are paths for additional entities.
+ switch (count($options['paths'])) {
+ case 0:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing field name, value, and remote file path.\n");
+ return FALSE;
+
+ case 1:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing field value and remote file path.\n");
+ return FALSE;
+
+ case 2:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote file path.\n");
+ return FALSE;
+
+ case 3:
+ $remotePaths = $options['paths'];
+ $fieldName = array_shift($remotePaths);
+ $fieldValue = array_shift($remotePaths);
+ break;
+
+ default:
+ $commandName = reset($options['command']);
+ print("$commandName: Too many arguments.\n");
+ return FALSE;
+ }
+
+ //
+ // Execute
+ // -------
+ // Change a series of items.
+ foreach ($remotePaths as $remotePath) {
+ if ($verbose === TRUE) {
+ print("$remotePath\n");
+ }
+
+ try {
+ // There is normally no response.
+ $response = $server->update($remotePath, $fieldName, $fieldValue);
+ if (empty($response) === FALSE) {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Downloads a file and folder.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -v | Show the name of each item modified. |
+ * | --help | Show help on command. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * There is no standard Linux/macOS/BSD command for downloading a file.
+ */
+function runDownload(FolderShareConnect $server, array $options) {
+ //
+ // Parse options
+ // -------------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-v':
+ $verbose = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // The command must have exactly two paths. The first is a path to a local
+ // file and the second is a path to a server parent folder.
+ switch (count($options['paths'])) {
+ case 0:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote file path.\n");
+ return FALSE;
+
+ case 1:
+ // One path: Remote. Use "." as local path.
+ $remotePath = $options['paths'][0];
+ $localPath = ".";
+ break;
+
+ case 2:
+ // Two paths: Remote & Local.
+ $remotePath = $options['paths'][0];
+ $localPath = $options['paths'][1];
+ break;
+
+ default:
+ $commandName = reset($options['command']);
+ print("$commandName: Too many file paths.\n");
+ return FALSE;
+ }
+
+ //
+ // Execute
+ // -------
+ // Download the file.
+ try {
+ if ($verbose === TRUE) {
+ print("Download \"$remotePath\" to \"$localPath\"\n");
+ }
+
+ // The new local file's path is the response. Ignore it.
+ $server->download($remotePath, $localPath);
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Uploads a local file or folder.
+ *
+ * This method supports the following flags:
+ * | Flag | Meaning |
+ * | ------ | ------------------------------------------------ |
+ * | -v | Show the name of each item modified. |
+ * | --help | Show help on command. |
+ *
+ * @param \FolderShare\FolderShareConnect $server
+ * The server connection.
+ * @param array $options
+ * The command-line options array.
+ *
+ * @return bool
+ * Returns TRUE on success, and FALSE on failure. On failure an error
+ * message has already been output.
+ *
+ * @internal
+ * There is no standard Linux/macOS/BSD command for uploading a file.
+ */
+function runUpload(FolderShareConnect $server, array $options) {
+ //
+ // Parse options
+ // -------------
+ // Synonyms have already been mapped to primary flag names.
+ $verbose = FALSE;
+
+ foreach ($options['flags'] as $flagName) {
+ switch ($flagName) {
+ case '-v':
+ $verbose = TRUE;
+ break;
+ }
+ }
+
+ //
+ // Validate
+ // --------
+ // The command must have exactly two paths. The first is a path to a local
+ // file and the second is a path to a server parent folder.
+ switch (count($options['paths'])) {
+ case 0:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing local and remote file paths.\n");
+ return FALSE;
+
+ case 1:
+ $commandName = reset($options['command']);
+ print("$commandName: Missing remote file path.\n");
+ return FALSE;
+
+ case 2:
+ $localPath = $options['paths'][0];
+ $remotePath = $options['paths'][1];
+ break;
+
+ default:
+ $commandName = reset($options['command']);
+ print("$commandName: Too many file paths.\n");
+ return FALSE;
+ }
+
+ //
+ // Execute
+ // -------
+ // Upload the file or folder.
+ try {
+ // There is normally no response.
+ $response = $server->upload(
+ $localPath,
+ $remotePath,
+ ($verbose === TRUE) ? 'uploadCallback' : NULL);
+ if (empty($response) === FALSE) {
+ print_r($response);
+ }
+ }
+ catch (\Exception $e) {
+ $commandName = reset($options['command']);
+ print("$commandName: " . $e->getMessage() . "\n");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Print verbose information about each upload.
+ *
+ * @param string $operation
+ * The operation being performed. Either "upload" or "newfolder".
+ * @param string $localPath
+ * The local absolute path to a file to upload.
+ * @param string $remotePath
+ * The remote path for the new file or folder.
+ */
+function uploadCallback(string $operation, string $localPath, string $remotePath) {
+ if ($operation === 'upload') {
+ print("Upload \"$localPath\" to \"$remotePath\"\n");
+ }
+ else {
+ print("Create folder \"$remotePath\"\n");
+ }
+}
+
+/*--------------------------------------------------------------------
+ *
+ * Execute!
+ *
+ * Run the application.
+ *
+ *--------------------------------------------------------------------*/
+
+//
+// Parse the command line. There may not be a command.
+//
+$options = parseCommandLine($argv);
+$appName = $options['appName'];
+
+
+//
+// Open server connection
+// ----------------------
+// If no user name was provided, assume anonymous access to the site.
+// If a user name was given, but no password, prompt for the password.
+//
+// Then use the given host and open a server connection and set the return
+// format and verbosity level.
+try {
+ if ($options['masquerade'] !== '' && $options['apikey'] !== '') {
+ $options['username'] = $options['masquerade'];
+ }
+ elseif (empty($options['username']) === FALSE &&
+ $options['username'] !== ANONYMOUS) {
+ if (empty($options['password']) === TRUE) {
+ // Prompt for password.
+ print("Password: ");
+ $options['password'] = stream_get_line(STDIN, 1024, PHP_EOL);
+ }
+ }
+ elseif ($options['username'] === ANONYMOUS) {
+ $options['username'] = '';
+ }
+
+ // Open server connection.
+ $server = new FolderShareConnect();
+
+ // Enable/disable verbosity.
+ if ($options['verbose'] === TRUE) {
+ $server->setVerbose(TRUE);
+ }
+
+ // Set the host.
+ $server->setHostName($options['host']);
+
+ // Log in.
+ //
+ // Provide the user name and password to log in. This typically triggers
+ // a connection. It will fail if the host, user name, or password are bad,
+ // or if there are problems communicating with the host.
+ if ($server->login($options['username'], $options['password'], $options['apikey'], $options['masquerade']) === FALSE) {
+ printErrorAndExit($appName, "Login failed.");
+ }
+}
+catch (\Exception $e) {
+ printErrorAndExit($appName, $e->getMessage());
+}
+
+//
+// Execute single command
+// ----------------------
+// If a command was given on the command line, execute it.
+if (empty($options['command']) === FALSE) {
+ //
+ // Validate command
+ // ----------------
+ // If a command is given, it must be valid.
+ // - Check if the command is a primary name or a synonym.
+ // - Check that there is a function for the command.
+ // - Check that the return format is supported.
+ // - Check that the given flags make sense.
+ if (empty($options['command']) === FALSE) {
+ validateCommand($options);
+ }
+
+ if (in_array('--help', $options['flags']) === TRUE) {
+ printCommandHelp($appName, $options['command'][0]);
+ exit(0);
+ }
+
+ // Set the preferred return format.
+ if ($options['format'] === 'linux' || $options['format'] === 'text') {
+ $server->setReturnFormat('keyvalue');
+ }
+ else {
+ $server->setReturnFormat($options['format']);
+ }
+
+ try {
+ //
+ // Dispatch
+ // --------
+ // Invoke the command.
+ $commandName = reset($options['command']);
+ $status = COMMANDS[$commandName]['function']($server, $options);
+ exit(($status === TRUE) ? 0 : 1);
+ }
+ catch (\Exception $e) {
+ printErrorAndExit($appName, $e->getMessage());
+ }
+}
+
+//
+// Execute multiple commands
+// -------------------------
+// Enter a prompt loop and execute commands as given.
+$username = $server->getUserName();
+if ($options['masquerade'] !== '' && $options['apikey'] !== '') {
+ $username = $options['masquerade'];
+}
+if (empty($username) === TRUE) {
+ $username = ANONYMOUS;
+}
+
+printf("Foldershare shell.\nLogged in as '%s'.\nType 'help' for a list of commands, 'quit' or 'exit' to exit.\n", $username);
+
+while (TRUE) {
+ try {
+ //
+ // Prompt
+ // ------
+ // Get a single line of input.
+ $prompt = "$appName> ";
+ if (PHP_OS === 'WINNT') {
+ $line = stream_get_line(STDIN, 1024, PHP_EOL);
+ }
+ else {
+ $line = readline($prompt);
+ }
+
+ if ($line === FALSE) {
+ // EOF.
+ break;
+ }
+
+ if (empty($line) === TRUE) {
+ continue;
+ }
+
+ if (PHP_OS !== 'WINNT') {
+ readline_add_history($line);
+ }
+
+ //
+ // Split into arguments
+ // --------------------
+ // Quotes require special handling to keep values together as a single
+ // argument.
+ $parts = mb_split('["\']', $line);
+ $inQuoted = FALSE;
+ $args = [];
+ foreach ($parts as $part) {
+ if ($inQuoted === TRUE) {
+ // This part is a quoted string, with the quotes removed. Do not
+ // trim or change in any way.
+ $args[] = $part;
+ $inQuoted = FALSE;
+ }
+ else {
+ // The part is one or more non-quoted arguments. Remove redundant
+ // white space, then split into arguments at white space boundaries.
+ foreach (mb_split(' ', mb_ereg_replace('\s+', ' ', $part)) as $p) {
+ if (empty($p) === FALSE) {
+ $args[] = $p;
+ }
+ }
+
+ $inQuoted = TRUE;
+ }
+ }
+
+ // Build an options array.
+ $localOptions = $options;
+ $localOptions['command'] = $args;
+
+ $commandName = reset($localOptions['command']);
+ switch ($commandName) {
+ case 'help':
+ printPromptHelp($appName);
+ continue 2;
+
+ case 'quit':
+ case 'exit':
+ exit(0);
+ }
+
+ //
+ // Validate command
+ // ----------------
+ // If a command is given, it must be valid.
+ // - Check if the command is a primary name or a synonym.
+ // - Check that there is a function for the command.
+ // - Check that the return format is supported.
+ // - Check that the given flags make sense.
+ if (validateCommand($localOptions, FALSE) === FALSE) {
+ continue;
+ }
+
+ $commandName = reset($localOptions['command']);
+
+ if (in_array('--help', $localOptions['flags']) === TRUE) {
+ printCommandHelp($appName, $commandName);
+ continue;
+ }
+
+ // Set the preferred return format.
+ if ($localOptions['format'] === 'linux' ||
+ $localOptions['format'] === 'text') {
+ $server->setReturnFormat('keyvalue');
+ }
+ else {
+ $server->setReturnFormat($localOptions['format']);
+ }
+
+ //
+ // Dispatch
+ // --------
+ // Invoke the command.
+ COMMANDS[$commandName]['function']($server, $localOptions);
+ }
+ catch (\Exception $e) {
+ print($e->getMessage());
+ }
+}

Added: genappalpha/languages/html5/seedmelab/syncfilesdirs.php
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ genappalpha/languages/html5/seedmelab/syncfilesdirs.php Tue Jul 23 13:28:37 2019 (r1703)
@@ -0,0 +1,552 @@
+<?php
+ ;
+
+$debug = 0;
+
+$notes = <<<__EOD
+ usage: php $argv[0] {-r} --user username --project project_name
+ copies data from fs to seedmelab
+ option:
+ -r remove files and directories present on seedmelab but not present on fs
+
+__EOD;
+
+$options = getopt(
+ "r"
+ ,[
+ "user:"
+ ,"project:"
+ ]
+ );
+
+# debugecho( json_encode( $options, JSON_PRETTY_PRINT ), 0 );
+
+if ( !isset( $options[ "user" ] ) || !strlen( $options[ "user" ] ) ) {
+ echo $notes;
+ exit( 1 );
+}
+
+$results = (object) [];
+
+$user = $options[ "user" ];
+$project = $options[ "project" ];
+
+# setup variables
+
+$secrets = json_decode( file_get_contents( "__secrets__" ) );
+
+if ( $secrets == NULL ) {
+ $results->_message = [ "icon" => "toast.png",
+ "text" => "<p>Could not load configuration information to setup seedmelab execution.</p>"
+ . "<p>This is a configuration error which should be forwarded to the site administrator.</p>"
+ . "<p>seedmelab synchronization will not work this is fixed.</p>"
+ ];
+ $results->error = "seedmelab configuration failed";
+ $results->_status = 'failed';
+ echo json_encode( $results );
+ exit();
+}
+
+if ( !isset( $secrets->seedmelab ) ) {
+ $results->_message = [ "icon" => "toast.png",
+ "text" => "<p>Configuration information missing 'seedmelab' definition.</p>"
+ . "<p>This is a configuration error which should be forwarded to the site administrator.</p>"
+ . "<p>seedmelab synchronization will not work this is fixed.</p>"
+ ];
+ $results->error = "Configuration missing 'seedmelab' section";
+ $results->_status = 'failed';
+ echo json_encode( $results );
+ exit();
+}
+
+if ( !isset( $secrets->seedmelab->apikey ) ) {
+ $results->_message = [ "icon" => "toast.png",
+ "text" => "<p>Configuration information missing 'seedmelab:apikey' definition.</p>"
+ . "<p>This is a configuration error which should be forwarded to the site administrator.</p>"
+ . "<p>seedmelab synchronization will not work this is fixed.</p>"
+ ];
+ $results->error = "Configuration missing 'seedmelab:apikey' definition";
+ $results->_status = 'failed';
+ echo json_encode( $results );
+ exit();
+}
+
+if ( !isset( $secrets->seedmelab->host ) ) {
+ $results->_message = [ "icon" => "toast.png",
+ "text" => "<p>Configuration information missing 'seedmelab:host' definition.</p>"
+ . "<p>This is a configuration error which should be forwarded to the site administrator.</p>"
+ . "<p>seedmelab synchronization will not work this is fixed.</p>"
+ ];
+ $results->error = "Configuration missing 'seedmelab:host' definition";
+ $results->_status = 'failed';
+ echo json_encode( $results );
+ exit();
+}
+
+$apikey = $secrets->seedmelab->apikey;
+$host = $secrets->seedmelab->host;
+
+$fs = "__docroot:html5__/__application__/results/users/$user";
+
+# setup foldershare process
+
+$process = proc_open("__docroot:html5__/__application__/seedmelab/foldershare --host $host --masquerade $user --apikey $apikey",
+ [
+ [ "pipe","r" ],
+ [ "pipe","w" ],
+ [ "pipe","w" ]
+ ],
+ $pipes);
+
+if ( !is_resource($process) ) {
+
+ echo "Error, process is not resource\n";
+ exit;
+}
+
+stream_set_blocking( $pipes[0], false );
+stream_set_blocking( $pipes[1], false );
+stream_set_blocking( $pipes[2], false );
+
+# setup command stack to extract seedmelab file listing
+
+$towrite = [];
+$waitfor = [];
+$responses = "";
+$lastcmd = "";
+$waitone = 0;
+$g_info =
+ [
+ "seedmelab" => [
+ "files" => []
+ ,"dirs" => []
+ ]
+ ,"fs" => [
+ "files" => []
+ ,"dirs" => []
+ ]
+ ];
+
+
+function debugecho ( $str, $level = 1 ) {
+ global $debug;
+ if ( $debug >= $level ) {
+ echo $str . "\n";
+ }
+}
+
+# process individual commands
+
+function nextcmd() {
+ global $cmds;
+ global $towrite;
+ global $waitfor;
+ global $responses;
+ global $lastcmd;
+ global $waitone;
+ global $g_info;
+
+ debugecho( json_encode( $cmds, JSON_PRETTY_PRINT ) );
+
+ if ( strlen( $lastcmd ) ) {
+ if ( $waitone ) {
+ $waitone--;
+ } else {
+ $basedir = preg_replace( '/^(ls -l|stat)\s*/', '', $lastcmd );
+ if ( substr( $basedir, 0, 1 ) == "'" &&
+ substr( $basedir, -1, 1 ) == "'" ) {
+ $basedir = substr( $basedir, 1 );
+ $basedir = substr( $basedir, 0, -1 );
+ }
+ $basedir .= "/";
+ debugecho( "last command was $lastcmd, responses:--\n" . $responses . "\n--" );
+ debugecho( "basedir is '$basedir'" );
+ switch( substr( $lastcmd, 0, 3 ) ) {
+ case "ls " : {
+ $files = explode( "\n", $responses );
+ array_shift( $files );
+ array_pop( $files );
+ debugecho( "files:--\n" . implode( "\n", $files ) . "\n--" );
+ # convert to filenames list
+ $files = preg_replace( '/^.*\w{3}\s\d{2}\s\w{4}\s\d\d:\d\d (.*)$/',
+ $basedir . '$1',
+ $files );
+ debugecho( "files after replace:--\n" . implode( "\n", $files ) . "\n--" );
+ # push commands
+ foreach ( $files as $value ) {
+ $cmds[] = [ "runcmd" => "stat '$value'" ];
+ $cmds[] = [ "waitfor" => "foldershare> " ];
+ }
+ }
+ break;
+
+ case "sta" : {
+ # check response FileType:
+ # if Directory, push ls -l of it
+ $basedir = substr( $basedir, 0, -1 );
+ preg_match( '/\s+FileType:\s(\w+)\s/', $responses, $matches );
+ $fileType = $matches[ 1 ];
+ # we could add data to this entry, e.g. timestamp, size etc
+ preg_match( '/\s+Size:\s(\d+)\s/', $responses, $matches );
+ $size = $matches[ 1 ];
+ preg_match( '/Modify:\s(.{18,19} \d{4})/', $responses, $matches );
+ $modify = $matches[ 1 ];
+ preg_match( '/Create:\s(.{18,19} \d{4})/', $responses, $matches );
+ $create = $matches[ 1 ];
+ $info = [
+ "size" => $size
+ ,"create" => $create
+ ,"modify" => $modify
+ ];
+
+ debugecho( "for $basedir filetype is $fileType" );
+ switch( $fileType ) {
+ case "Directory" : {
+ $cmds[] = [ "runcmd" => "ls -l '$basedir'" ];
+ $cmds[] = [ "waitfor" => "foldershare> " ];
+ $info[ "depth" ] = count( explode( "/", $basedir ) ) - 2;
+ array_push( $g_info[ "seedmelab" ][ "dirs" ], [ $basedir => $info ] );
+ }
+ break;
+
+ case "Regular" : {
+ array_push( $g_info[ "seedmelab" ][ "files" ], [ $basedir => $info ] );
+ }
+ break;
+
+ default : {
+ echo "unsupported fileType $fileType\n";
+ exit( 4 );
+ }
+ break;
+ }
+ }
+ break;
+
+ case "rm " : # remove file, nothing to respond to (could check errors?)
+ break;
+
+ case "rmd" : # remove directory, nothing to respond to (could check errors?)
+ break;
+
+ case "mkd" : # create directory, nothing to respond to (could check errors?)
+ break;
+
+ case "put" : # upload file, nothing to respond to (could check errors?)
+ break;
+
+ default : {
+ echo "unsupported lastcmd '$lastcmd'\n";
+ exit( 3 );
+ }
+ break;
+ }
+ $lastcmd = "";
+ }
+ }
+
+ debugecho( "next cmd" );
+ if ( !count( $cmds ) ) {
+ return 0;
+ }
+
+ $cmd = array_shift( $cmds );
+ foreach ( $cmd as $key => $value ) {
+ debugecho( "key $key value $value" );
+ switch ( $key ) {
+ case "waitfor" : {
+ array_push( $waitfor, $value );
+ }
+ break;
+ case "runcmd" : {
+ $responses = "";
+ array_push( $towrite, $value );
+ $lastcmd = $value;
+ $waitone++;
+ }
+ break;
+ default : {
+ debugecho( "Unsupported command type '$key'" );
+ exit( 2 );
+ }
+ break;
+ }
+ }
+ return 1;
+}
+
+# command loop
+
+function process_cmds () {
+ global $pipes;
+ global $waitfor;
+ global $towrite;
+ global $responses;
+ global $process;
+
+ # are we setup?
+
+ if ( !nextcmd() ) {
+ echo "Empty command stack!\n";
+ exit;
+ }
+
+ # process until cmds exhausted
+
+ do {
+ debugecho( "select loop" );
+
+ $read = [
+ $pipes[ 2 ]
+ ];
+
+ if ( count( $waitfor ) ) {
+ array_push( $read, $pipes[ 1 ] );
+ }
+
+ if ( count( $towrite ) ) {
+ $write = [
+ $pipes[ 0 ]
+ ];
+ } else {
+ $write = [];
+ }
+ $except = [];
+
+ $retval = stream_select( $read, $write, $except, 1 );
+ debugecho( "select loop return value : $retval" );
+ if ( !$retval ) {
+ $status = proc_get_status( $process );
+ if ( !$status[ 'running' ] ) {
+ echo "Error, process died\n";
+ exit;
+ }
+ continue;
+ }
+
+ # there are streams to process
+ if ( in_array( $pipes[ 1 ], $read ) ) {
+ $responses .= stream_get_contents( $pipes[ 1 ] );
+ if ( strpos( $responses, $waitfor[ 0 ] ) ) {
+ array_shift( $waitfor );
+ if ( !nextcmd() ) {
+ return;
+ }
+ }
+ }
+
+ if ( in_array( $pipes[ 2 ], $read ) ) {
+ echo "Error returned by process:" . stream_get_contents( $pipes[ 2 ] );
+ exit( 1 );
+ }
+
+ if ( in_array( $pipes[ 0 ], $write ) ) {
+ fwrite( $pipes[ 0 ], array_shift( $towrite ) . "\n");
+ if ( !nextcmd() ) {
+ return;
+ }
+ }
+ } while ( 1 );
+}
+
+# get local fs
+
+function scan_fs( $dir = "" ) {
+ global $g_info;
+ global $fs;
+
+ $use_dir = $fs;
+ if ( strlen( $dir ) ) {
+ $use_dir = $fs . "/" . $dir;
+ }
+
+ $sdir = scandir( $use_dir );
+ foreach ( $sdir as $key => $value ) {
+ if ( !in_array( $value, [ ".", ".." ] ) ) {
+ $name = $dir . "/" . $value;
+ $stat = stat( $use_dir . "/" . $value );
+ $info =
+ [
+ "size" => $stat[ "size" ]
+ ,"create" => $stat[ "ctime" ]
+ ,"modify" => $stat[ "mtime" ]
+ ];
+
+ if ( is_dir( $use_dir . "/" . $value ) ) {
+ array_push( $g_info[ "fs" ][ "dirs" ], [ $name => $info ] );
+ scan_fs( $name );
+ } else {
+ array_push( $g_info[ "fs" ][ "files" ], [ $name => $info ] );
+ }
+ }
+ }
+}
+
+function local_fs() {
+ debugecho( "local_fs", 0 );
+ scan_fs();
+}
+
+function info_array( $source, $type ) {
+ global $g_info;
+
+ $result = [];
+
+ foreach ( $g_info[ $source ][ $type ] as $key => $value ) {
+ $result[] = implode( "", array_keys( $value ) );
+ }
+ return $result;
+}
+
+function remove_from_seedmelab_nonexistent() {
+ global $cmds;
+
+ debugecho( "remove_from_seedmelab_nonexistent", 0 );
+
+ $seedmelabfiles = info_array( "seedmelab", "files" );
+ $seedmelabdirs = info_array( "seedmelab", "dirs" );
+ $fsfiles = info_array( "fs", "files" );
+ $fsdirs = info_array( "fs", "dirs" );
+
+ $removefiles = array_diff( $seedmelabfiles, $fsfiles );
+ $removedirs = array_reverse( array_diff( $seedmelabdirs, $fsdirs ) );
+
+ debugecho( "remove files:\n" . implode( "\n", $removefiles ), 0 );
+ debugecho( "remove dirs:\n" . implode( "\n", $removedirs ), 0 );
+
+ foreach ( $removefiles as $value ) {
+ $cmds[] = [ "runcmd" => "rm '$value'" ];
+ $cmds[] = [ "waitfor" => "foldershare> " ];
+ }
+
+ foreach ( $removedirs as $value ) {
+ $cmds[] = [ "runcmd" => "rmdir '$value'" ];
+ $cmds[] = [ "waitfor" => "foldershare> " ];
+ }
+
+ debugecho( "remove commands:\n" . json_encode( $cmds, JSON_PRETTY_PRINT ), 0 );
+}
+
+function create_directories_on_seedmelab() {
+ global $cmds;
+
+ debugecho( "create_directories_on_seedmelab", 0 );
+
+ $seedmelabdirs = info_array( "seedmelab", "dirs" );
+ $fsdirs = info_array( "fs", "dirs" );
+
+ $createdirs = array_diff( $fsdirs, $seedmelabdirs );
+
+ debugecho( "createdirs dirs:\n" . implode( "\n", $createdirs ), 0 );
+
+ foreach ( $createdirs as $value ) {
+ $cmds[] = [ "runcmd" => "mkdir '$value'" ];
+ $cmds[] = [ "waitfor" => "foldershare> " ];
+ }
+
+ debugecho( "create directories commands:\n" . json_encode( $cmds, JSON_PRETTY_PRINT ), 0 );
+}
+
+function upload_files_to_seedmelab() {
+ global $cmds;
+ global $fs;
+
+ debugecho( "upload_files_to_seedmelab", 0 );
+
+ $seedmelabfiles = info_array( "seedmelab", "files" );
+ $fsfiles = info_array( "fs", "files" );
+
+ $uploadfiles = array_diff( $fsfiles, $seedmelabfiles );
+
+ debugecho( "upload files:\n" . implode( "\n", $uploadfiles ), 0 );
+
+ foreach ( $uploadfiles as $value ) {
+ $dest = preg_replace( "/\/[^\/]+$/", "", $value );
+ $cmds[] = [ "runcmd" => "put '${fs}$value' '$dest'" ];
+ $cmds[] = [ "waitfor" => "foldershare> " ];
+ }
+
+ debugecho( "upload files commands:\n" . json_encode( $cmds, JSON_PRETTY_PRINT ), 0 );
+}
+
+# work thru the stages
+# stages are
+# 1: get seedmelab fs info
+# 2: get local fs info
+# 3: optionally remove nonexistent on fs from seedmelab
+# 4: create directories not present on seedmelab
+# 5: upload files not present on seedmelab or differeing in size
+
+$startatstage = 1;
+$finishatstage = 5;
+
+function stage_loop() {
+ global $g_info;
+ global $finishatstage;
+ global $startatstage;
+ global $cmds;
+ global $options;
+
+ for ( $stage = $startatstage; $stage <= $finishatstage; $stage++ ) {
+ debugecho( "stage $stage", 0 );
+
+ switch ( $stage ) {
+ case 1 : { # getseedmelab fs info
+ $cmds = [
+ [ "waitfor" => "foldershare> " ]
+ ,[ "runcmd" => "ls -l" ]
+ ,[ "waitfor" => "foldershare> " ]
+ ];
+ process_cmds();
+ }
+ break;
+
+ case 2 : { # get local fs info
+ local_fs();
+ }
+ break;
+
+ case 3 : { # optionally remove nonexistent on fs from seedmelab
+ if ( isset( $options[ "r" ] ) ) {
+ remove_from_seedmelab_nonexistent();
+ if ( count( $cmds ) ) {
+ process_cmds();
+ }
+ } else {
+ debugecho( "stage skipped", 0 );
+ }
+ }
+ break;
+
+ case 4 : { # create directories on seedmelab
+ create_directories_on_seedmelab();
+ if ( count( $cmds ) ) {
+ process_cmds();
+ }
+ }
+ break;
+
+ case 5 : { # upload files to seedmelab
+ upload_files_to_seedmelab();
+ if ( count( $cmds ) ) {
+ process_cmds();
+ }
+ }
+ break;
+
+ default : { # invalid stage
+ debugecho( "stage invalid", 0 );
+ }
+ break;
+ }
+
+ }
+
+ debugecho( "stage loop done", 0 );
+ debugecho( json_encode( $g_info, JSON_PRETTY_PRINT ), 1 );
+
+ exit;
+}
+
+stage_loop();
+

Join genapp-commits@groups.io to automatically receive all group messages.